In this task, you will add authorization to the Posts API. You will enforce authorization rules to allow users to create, update, and delete posts and comments only if they are authenticated and are the creators of the posts or comments. You will also associate posts and comments with the users who created them and implement authorization guards to enforce these rules.
We start by creating an auth
middleware that will be responsible for extracting the session cookie from the request headers, validating the session, and setting the user
and session
variables in the context.
Create a new file api/src/middleware/auth.ts
with the following content:
import type { Context, Next } from "hono";
import { lucia } from "../db/auth";
export const auth = async (c: Context, next: Next) => {
const cookie = c.req.header("Cookie") ?? "";
const sessionId = lucia.readSessionCookie(cookie);
if (!sessionId) {
c.set("user", null);
c.set("session", null);
return next();
}
const { session, user } = await lucia.validateSession(sessionId);
// console.log("auth middleware", { session, user }); // Uncomment this line to debug
if (!session) {
const blankSessionCookie = lucia.createBlankSessionCookie();
c.header("Set-Cookie", blankSessionCookie.serialize(), {
append: true,
});
}
if (session && session.fresh) {
const sessionCookie = lucia.createSessionCookie(session.id);
c.header("Set-Cookie", sessionCookie.serialize(), {
append: true,
});
}
c.set("session", session);
c.set("user", user);
return next();
};
Let’s break down the code above:
Context
and Next
types from Hono and the lucia
instance from the auth
module.auth
middleware function that takes the context c
and the next
function as arguments.Cookie
header.readSessionCookie
method from the lucia
instance.user
and session
variables in the context to null
and call the next
function.validateSession
method from the lucia
instance.session
and user
variables in the context and call the next
function.If we apply this middleware to all routes, it will ensure that the user
and session
variables are set in the context for each request. This will allow us to check if a user is authenticated and retrieve the user’s information from the context.
Since we are adding session
and user
variables to the context, we need to update the Context
type to include these variables. We’ll do this in the next step.
Context
typeLet’s create a new file api/src/lib/context.ts
with the following content:
import type { Env } from "hono";
import type { Session, User } from "lucia";
export interface Context extends Env {
Variables: {
user: User | null;
session: Session | null;
};
}
In the code above:
Env
type from Hono and the Session
and User
types from the lucia
module.Context
interface that extends the Env
interface.Variables
object that contains the user
and session
variables with types User | null
and Session | null
, respectively.Next, we need to let Hono know about this new Context
type. We’ll do this in the next step.
To take advantage of Hono’s improved support for context-aware middleware, we need to update all instances where a Hono instance is created to use the Context
type.
For example, in the api/src/app.ts
file, you’ll want to modify the code as follows:
// Other imports
+ import type { Context } from "./lib/context.js";
- const app = new Hono();
+ const app = new Hono<Context>();
// The rest of the file
Notice that we’ve added the <Context>
type parameter to the Hono
instance creation. This tells Hono to use the Context
type we defined earlier. You’ll need to make similar changes in other files where Hono instances are created.
Once you’ve updated all instances of Hono creation, you are ready to apply the auth
middleware to all routes. We’ll do this in the next step.
auth
middleware to all routesTo apply the auth
middleware to all routes, you can use the app.use
method in the api/src/app.ts
file. Add the following line to the file, before defining any routes:
app.use("/*", auth);
This line tells Hono to apply the auth
middleware to all routes. The /*
pattern matches all routes, ensuring that the auth
middleware is executed for every request.
With the auth
middleware in place, you can now access the user
and session
variables in your route handlers. We will try this out in the next step.
session
variables in a route handlerLet’s update the POST /sign-out
route in the api/src/routes/auth.ts
file to access the session
variables from the context:
authRoutes.post("/sign-out", async (c) => {
const session = c.get("session");
if (!session) {
throw new HTTPException(401, { message: "No session found" });
}
await lucia.invalidateSession(session.id);
const sessionCookie = lucia.createBlankSessionCookie();
c.header("Set-Cookie", sessionCookie.serialize()); // Remove the session cookie from the client
return c.json({ message: "You have been signed out!" });
});
In the code above:
session
variable from the context using c.get("session")
.invalidateSession
method from the lucia
instance.Now that we can access the session
and user
variables in the context, we can use them to implement authorization for the Posts API. We’ll do this in the next steps.
Instead of checking for the session
variable in each route handler, we can create another middleware, authGuard
, that will check if the user is authenticated and throw an HTTP 401 exception if not.
Create a new file api/src/middleware/auth-guard.ts
with the following content:
import type { Context, Next } from "hono";
import { HTTPException } from "hono/http-exception";
export const authGuard = async (c: Context, next: Next) => {
const session = c.get("session");
if (!session) {
throw new HTTPException(401, { message: "Unauthorized" });
}
return next();
};
In the code above:
Context
and Next
types from Hono and the HTTPException
class from the hono/http-exception
module.authGuard
middleware function that takes the context c
and the next
function as arguments.session
variable from the context using c.get("session")
.next
function to proceed to the next middleware or route handler.With the authGuard
middleware in place, we can now apply it to the routes that require authentication. We’ll do this in the next step.
authGuard
middleware to the routesAdd the authGuard
middleware to the POST /posts
route in api/src/routes/posts.ts
:
// Other imports
import { authGuard } from "../middleware/auth-guard";
const postRoutes = new Hono<Context>();
// Get all posts with optional sorting, filtering, searching, and pagination
postRoutes.get(
"/posts",
authGuard, // 👈 Look here
zValidator("query", queryParamsSchema),
async (c) => {
// No changes needed
},
);
// Get a single post by id
postRoutes.get(
"/posts/:id",
authGuard, // 👈 Look here
zValidator("param", getPostSchema),
async (c) => {
// No changes needed
},
);
// Delete a post by id
postRoutes.delete(
"/posts/:id",
authGuard, // 👈 Look here
zValidator("param", getPostSchema),
async (c) => {
// No changes needed
},
);
// Create a new post
postRoutes.post(
"/posts",
authGuard, // 👈 Look here
zValidator("json", createPostSchema),
async (c) => {
// No changes needed
},
);
// Update a post by id
postRoutes.patch(
"/posts/:id",
authGuard, // 👈 Look here
zValidator("param", getPostSchema),
zValidator("json", updatePostSchema),
async (c) => {
// No changes needed
},
);
export default postRoutes;
Notice that we added the authGuard
middleware to the POST /posts
route. This ensures that only authenticated users can create new posts. You can apply the authGuard
middleware to other routes that require authentication in a similar manner.
Open Postman and try performing any CRUD operations on posts without being signed in. You should receive an HTTP 401 Unauthorized response.
If you want to adjust the time to expire the session, you can do so in api/src/db/auth.ts
:
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
secure: process.env.NODE_ENV === "production",
sameSite: "none",
},
},
// Look here 👇
sessionExpiresIn: new TimeSpan(1, "m"), // 1 minutes
});
The sessionExpiresIn
option accepts a TimeSpan
object that specifies the duration after which the session will expire. You can adjust the duration as needed.
As part of our authorization plan, we need to associate posts and comments with the users who created them. This will allow us to enforce authorization rules such as allowing users to delete only their own posts and comments.
To associate posts and comments with users, we need to add a userId
column to the posts
and comments
tables in the database schema.
Open the api/src/db/schema.ts
file and update the posts
and comments
tables as follows:
export const posts = sqliteTable("posts", {
id: integer("id").primaryKey({ autoIncrement: true }),
content: text("content").notNull(),
date: integer("date", { mode: "timestamp" }).notNull(),
userId: integer("user_id") // 👈 Look here
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
});
export const comments = sqliteTable("comments", {
id: integer("id").primaryKey({ autoIncrement: true }),
content: text("content").notNull(),
date: integer("date", { mode: "timestamp" }).notNull(),
postId: integer("post_id")
.notNull()
.references(() => posts.id, { onDelete: "cascade" }),
userId: integer("user_id") // 👈 Look here
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
});
Notice we added a userId
column to both the posts
and comments
tables. This column references the id
column in the users
table, establishing a relationship between the posts/comments and the users who created them. Moreover, we added a cascade
option to the references
method to ensure that when a user is deleted, their associated posts and comments are also deleted.
Now that we have associated posts and comments with users, we can enforce authorization rules based on the user who created them. We’ll do this in the next steps. But first, we need to update the database!
Delete the sqlite.db
file and run the following command to recreate the database:
pnpm db:push
We should also seed the database with some sample data to test the authorization rules. We’ll do this in the next step.
To test the authorization rules, we need some sample data in the database. We’ll seed the database with sample users, posts, and comments.
Update the api/src/db/seed.ts
file with the following content:
import { db, connection } from "./index";
import { comments, posts, users } from "./schema";
import { faker } from "@faker-js/faker";
import { sql } from "drizzle-orm";
import { hash } from "@node-rs/argon2";
async function seed() {
console.log("Seeding the database...");
// Clean the tables
console.log("Cleaning existing data...");
await db.delete(comments);
await db.delete(posts);
await db.delete(users);
// Reset the auto-increment counters
await db.run(
sql`DELETE FROM sqlite_sequence WHERE name IN ('posts', 'comments', 'users')`,
);
console.log("Inserting new seed data...");
const sampleKeywords = [
"technology",
"innovation",
"design",
"development",
"programming",
"software",
"hardware",
"AI",
"machine learning",
"data science",
"cloud computing",
"cybersecurity",
];
// Create 10 sample users
const sampleUsers = [];
for (let i = 1; i <= 10; i++) {
const user = await db
.insert(users)
.values({
name: faker.person.fullName(),
username: `user-${i}`,
password_hash: await hash(`pass-${i}`),
})
.returning()
.get();
sampleUsers.push(user);
}
// Insert 100 sample posts
for (let i = 1; i <= 100; i++) {
const randomKeywords = faker.helpers.arrayElements(sampleKeywords, {
min: 1,
max: 3,
});
const content = `Post #${i} ${randomKeywords.join(" ")}`;
const randomUser = faker.helpers.arrayElement(sampleUsers);
const post = await db
.insert(posts)
.values({
content,
date: faker.date.recent({
days: 5,
}),
userId: randomUser.id,
})
.returning()
.get();
// Insert 1-20 comments for each post
const numComments = faker.number.int({ min: 1, max: 20 });
for (let j = 1; j <= numComments; j++) {
const randomKeywords = faker.helpers.arrayElements(sampleKeywords, {
min: 1,
max: 3,
});
const randomCommenter = faker.helpers.arrayElement(sampleUsers);
await db.insert(comments).values({
content: `Comment #${j} for post #${i} ${randomKeywords.join(" ")}`,
date: faker.date.recent({
days: 3,
}),
postId: post.id,
userId: randomCommenter.id,
});
}
}
console.log("Seeding completed successfully.");
}
seed()
.catch((e) => {
console.error("Seeding failed:");
console.error(e);
})
.finally(() => {
connection.close();
});
Notice that we added sample users, posts, and comments to the database. Each post is associated with a random user, and each comment is associated with a random user and post. This will allow us to test the authorization rules based on the user who created the post or comment.
To seed the database with the sample data, run the following command:
pnpm db:seed
You can inspect the database to verify that the sample data has been inserted correctly. Open Drizzle Studio to view the data:
pnpm db:studio
With the sample data in place, we can now enforce authorization rules based on the user who created the posts and comments. We’ll do this in the next steps.
The general strategy for enforcing authorization rules is to check if the user is the creator of the post or comment before allowing them to perform certain actions. We’ll start by implementing authorization rules for posts.
Update the POST /posts
route in the api/src/routes/posts.ts
file to associate the new post with the authenticated user:
// Create a new post
postRoutes.post(
"/posts",
authGuard,
zValidator("json", createPostSchema),
async (c) => {
const { content } = c.req.valid("json");
const user = c.get("user"); // Get the authenticated user from the context
const newPost = await db
.insert(posts)
.values({
content,
date: new Date(),
userId: user!.id, // Associate the post with the authenticated user
})
.returning()
.get();
return c.json(newPost);
},
);
Notice that we added the userId: user!.id
line to associate the new post with the authenticated user. This ensures that the user who created the post is correctly recorded in the database.
Update the DELETE /posts/:id
route in the api/src/routes/posts.ts
file to enforce authorization rules:
// Delete a post by id
postRoutes.delete(
"/posts/:id",
authGuard,
zValidator("param", getPostSchema),
async (c) => {
const { id } = c.req.valid("param");
const user = c.get("user");
const post = await db.select().from(posts).where(eq(posts.id, id)).get();
if (!post) {
throw new HTTPException(404, { message: "Post not found" });
}
if (post.userId !== user!.id) {
throw new HTTPException(403, {
message: "Unauthorized to delete this post",
});
}
const deletedPost = await db
.delete(posts)
.where(eq(posts.id, id))
.returning()
.get();
return c.json(deletedPost);
},
);
Notice that we added a check to ensure that the user who created the post is the same as the authenticated user. If not, we throw an HTTP 403 Forbidden exception, indicating that the user is unauthorized to delete the post.
Update the PATCH /posts/:id
route in the api/src/routes/posts.ts
file to enforce authorization rules:
// Update a post by id
postRoutes.patch(
"/posts/:id",
authGuard,
zValidator("param", getPostSchema),
zValidator("json", updatePostSchema),
async (c) => {
const { id } = c.req.valid("param");
const { content } = c.req.valid("json");
const user = c.get("user");
const post = await db.select().from(posts).where(eq(posts.id, id)).get();
if (!post) {
throw new HTTPException(404, { message: "Post not found" });
}
if (post.userId !== user!.id) {
throw new HTTPException(403, {
message: "Unauthorized to update this post",
});
}
const updatedPost = await db
.update(posts)
.set({ content })
.where(eq(posts.id, id))
.returning()
.get();
return c.json(updatedPost);
},
);
Notice that we added a check to ensure that the user who created the post is the same as the authenticated user. If not, we throw an HTTP 403 Forbidden exception, indicating that the user is unauthorized to update the post.
As part of our authroization strategy, we want to allow users to view posts even if they are not signed in. We’ll remove the authGuard
middleware from the GET /posts/:id
route in the api/src/routes/posts.ts
file:
// Get a single post by id
postRoutes.get(
"/posts/:id",
// authGuard, // 👈 Remove this line
zValidator("param", getPostSchema),
async (c) => {
const { id } = c.req.valid("param");
const post = await db
.select({
id: posts.id,
content: posts.content,
date: posts.date,
author: {
id: users.id,
name: users.name,
username: users.username,
},
})
.from(posts)
.leftJoin(users, eq(posts.userId, users.id))
.where(eq(posts.id, id))
.get();
if (!post) {
throw new HTTPException(404, { message: "Post not found" });
}
return c.json(post);
},
);
Notice that we removed the authGuard
middleware from the route. This allows users to view posts even if they are not signed in.
Moreover, we use a leftJoin
to include the author information in the response. This allows us to display the author’s name and username along with the post content.
In relational databases, a join operation is used to combine rows from two or more tables based on a related column between them. In this case, we are performing a leftJoin
operation to combine the posts
and users
tables based on the userId
column in the posts
table and the id
column in the users
table.
The equivalent SQL query for the join operation would look like this:
SELECT posts.id, posts.content, posts.date, users.id, users.name, users.username
FROM posts
LEFT JOIN users ON posts.userId = users.id
WHERE posts.id = :id
There are different types of join operations, such as INNER JOIN
, LEFT JOIN
, RIGHT JOIN
, and FULL JOIN
, each with its own behavior. In this case, we are using a LEFT JOIN
to include all posts, even if there is no corresponding user record (although this should not happen in practice).
It is beyond the scope of this guide to cover all the nuances of join operations, but understanding the basics of joins is essential for working with relational databases.
Similar to the GET /posts/:id
route, we’ll remove the authGuard
middleware from the GET /posts
route in the api/src/routes/posts.ts
file. We also perform a leftJoin
operation to include the author information in the response. Moreover, we allow users to search for posts by the author’s username. To that aim, we should first update the input schema in api/src/validators/schemas.ts
to include the username
field:
export const queryParamsSchema = z.object({
sort: z.enum(["asc", "desc"]).optional(),
search: z.string().optional(),
page: z.coerce.number().int().positive().optional(),
limit: z.coerce.number().int().positive().optional(),
username: z.string().optional(), // 👈 Look here
});
Next, update the GET /posts
route in the api/src/routes/posts.ts
file:
// Get all posts with optional sorting, filtering, searching, and pagination
postRoutes.get(
"/posts",
// authGuard, // 👈 Remove this line
zValidator("query", queryParamsSchema),
async (c) => {
const {
sort,
search,
page = 1,
limit = 10,
username,
} = c.req.valid("query");
const whereClause: (SQL | undefined)[] = [];
if (search) {
whereClause.push(like(posts.content, `%${search}%`));
}
if (username) {
const user = await db
.select()
.from(users)
.where(eq(users.username, username))
.get();
if (!user) {
throw new HTTPException(404, { message: "User not found" });
}
whereClause.push(eq(posts.userId, user.id));
}
const orderByClause: SQL[] = [];
if (sort === "desc") {
orderByClause.push(desc(posts.date));
} else if (sort === "asc") {
orderByClause.push(asc(posts.date));
}
const offset = (page - 1) * limit;
const [allPosts, [{ totalCount }]] = await Promise.all([
db
.select({
id: posts.id,
content: posts.content,
date: posts.date,
author: {
id: users.id,
name: users.name,
username: users.username,
},
})
.from(posts)
.leftJoin(users, eq(posts.userId, users.id))
.where(and(...whereClause))
.orderBy(...orderByClause)
.limit(limit)
.offset(offset),
db
.select({ totalCount: count() })
.from(posts)
.where(and(...whereClause)),
]);
return c.json({
data: allPosts,
page,
limit,
total: totalCount,
});
},
);
Notice if the username
query parameter is provided, we look up the user by the username and filter the posts by the user’s id
. This allows users to search for posts by a specific author’s username.
Similar to posts, we need to enforce authorization rules for comments. We’ll start by implementing authorization rules for comments.
Update the POST /posts/:postId/comments
route in the api/src/routes/comments.ts
file to associate the new comment with the authenticated user:
// Create a new comment for a post
commentRoutes.post(
"/posts/:postId/comments",
authGuard,
zValidator("param", getCommentsSchema),
zValidator("json", createCommentSchema),
async (c) => {
const { postId } = c.req.valid("param");
const { content } = c.req.valid("json");
const user = c.get("user"); // Get the authenticated user from the context
// Check if the post exists
const post = await db.select().from(posts).where(eq(posts.id, postId)).get();
if (!post) {
throw new HTTPException(404, { message: "Post not found" });
}
const newComment = await db
.insert(comments)
.values({
content,
date: new Date(),
postId,
userId: user!.id, // Associate the comment with the authenticated user
})
.returning()
.get();
return c.json(newComment);
},
);
Notice that we added the userId: user!.id
line to associate the new comment with the authenticated user. This ensures that the user who created the comment is correctly recorded in the database.
Update the DELETE /posts/:postId/comments/:commentId
route in the api/src/routes/comments.ts
file to enforce authorization rules:
// Delete a comment by id for a post
commentRoutes.delete(
"/posts/:postId/comments/:commentId",
authGuard,
zValidator("param", getCommentSchema),
async (c) => {
const { postId, commentId } = c.req.valid("param");
const user = c.get("user");
const comment = await db
.select()
.from(comments)
.where(and(eq(comments.id, commentId), eq(comments.postId, postId)))
.get();
if (!comment) {
throw new HTTPException(404, { message: "Comment not found" });
}
if (comment.userId !== user!.id) {
throw new HTTPException(403, {
message: "Unauthorized to delete this comment",
});
}
const deletedComment = await db
.delete(comments)
.where(and(eq(comments.id, commentId), eq(comments.postId, postId)))
.returning()
.get();
return c.json(deletedComment);
},
);
Notice that we added a check to ensure that the user who created the comment is the same as the authenticated user. If not, we throw an HTTP 403 Forbidden exception, indicating that the user is unauthorized to delete the comment.
Update the PATCH /posts/:postId/comments/:commentId
route in the api/src/routes/comments.ts
file to enforce authorization rules:
// Update a comment by id for a post
commentRoutes.patch(
"/posts/:postId/comments/:commentId",
authGuard,
zValidator("param", getCommentSchema),
zValidator("json", updateCommentSchema),
async (c) => {
const { postId, commentId } = c.req.valid("param");
const { content } = c.req.valid("json");
const user = c.get("user");
const comment = await db
.select()
.from(comments)
.where(and(eq(comments.id, commentId), eq(comments.postId, postId)))
.get();
if (!comment) {
throw new HTTPException(404, { message: "Comment not found" });
}
if (comment.userId !== user!.id) {
throw new HTTPException(403, {
message: "Unauthorized to update this comment",
});
}
const updatedComment = await db
.update(comments)
.set({ content })
.where(and(eq(comments.id, commentId), eq(comments.postId, postId)))
.returning()
.get();
return c.json(updatedComment);
},
);
Notice that we added a check to ensure that the user who created the comment is the same as the authenticated user. If not, we throw an HTTP 403 Forbidden exception, indicating that the user is unauthorized to update the comment.
Unlike posts, we will not allow users to view comments without being signed in. We’ll keep the authGuard
middleware in the GET /posts/:postId/comments/:commentId
route in the api/src/routes/comments.ts
file:
// Get a single comment by id for a post
commentRoutes.get(
"/posts/:postId/comments/:commentId",
authGuard,
zValidator("param", getCommentSchema),
async (c) => {
const { postId, commentId } = c.req.valid("param");
const comment = await db
.select({
id: comments.id,
content: comments.content,
date: comments.date,
author: {
id: users.id,
name: users.name,
username: users.username,
},
})
.from(comments)
.leftJoin(users, eq(comments.userId, users.id))
.where(and(eq(comments.id, commentId), eq(comments.postId, postId)))
.get();
if (!comment) {
throw new HTTPException(404, { message: "Comment not found" });
}
return c.json(comment);
},
);
Notice that we kept the authGuard
middleware in the route to ensure that users are signed in before viewing comments. This allows us to enforce authorization rules based on the authenticated user.
Moreover, we use a leftJoin
to include the author information in the response. This allows us to display the author’s name and username along with the comment content.
Similar to the GET /posts/:postId/comments/:commentId
route, we’ll keep the authGuard
middleware in the GET /posts/:postId/comments
route in the api/src/routes/comments.ts
file. We also perform a leftJoin
operation to include the author information in the response. Moreover, we allow users to search for comments by the author’s username.
Update GET /posts/:postId/comments/
route in the api/src/routes/comments.ts
file:
// Get all comments for a post
commentRoutes.get(
"/posts/:postId/comments",
authGuard,
zValidator("param", getCommentsSchema),
zValidator("query", queryParamsSchema),
async (c) => {
const { postId } = c.req.valid("param");
const { sort, search, page = 1, limit = 10, username } = c.req.valid("query");
const whereClause: (SQL | undefined)[] = [];
whereClause.push(eq(comments.postId, postId));
if (search) {
whereClause.push(like(comments.content, `%${search}%`));
}
if (username) {
const user = await db
.select()
.from(users)
.where(eq(users.username, username))
.get();
if (!user) {
throw new HTTPException(404, { message: "User not found" });
}
whereClause.push(eq(comments.userId, user.id));
}
const orderByClause: SQL[] = [];
if (sort === "desc") {
orderByClause.push(desc(comments.date));
} else if (sort === "asc") {
orderByClause.push(asc(comments.date));
}
const offset = (page - 1) * limit;
const [allComments, [{ totalCount }]] = await Promise.all([
db
.select({
id: comments.id,
content: comments.content,
date: comments.date,
author: {
id: users.id,
name: users.name,
username: users.username,
},
})
.from(comments)
.leftJoin(users, eq(comments.userId, users.id))
.where(and(...whereClause))
.orderBy(...orderByClause)
.limit(limit)
.offset(offset),
db
.select({ totalCount: count() })
.from(comments)
.where(and(...whereClause)),
]);
return c.json({
data: allComments,
page,
limit,
total: totalCount,
});
},
);
Notice that we kept the authGuard
middleware in the route to ensure that users are signed in before viewing comments. Moreover, if the username
query parameter is provided, we look up the user by the username and filter the comments by the user’s id
. This allows users to search for comments by a specific author’s username.
With the authorization rules in place, you can now test the API to ensure that users can only perform actions on posts and comments that they created. Here are some test scenarios you can try:
username
query parameter. You should be able to view posts by the specified author.In this task, you learned how to implement authorization in a RESTful API using Hono, Lucia, and SQLite. You enforced primary authorization rules by checking if a user is authenticated. You also implemented secondary authorization rules by associating posts and comments with users and enforcing rules based on the user who created them. By following these steps, you should have a good understanding of how to implement authorization in a RESTful API using Hono and Lucia.