mirror of
https://github.com/fosrl/pangolin.git
synced 2025-07-31 08:04:54 +02:00
started integrating auth with lucia
This commit is contained in:
parent
a33a8d7367
commit
fc5dca136f
20 changed files with 1341 additions and 61 deletions
50
server/auth/index.ts
Normal file
50
server/auth/index.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import { Lucia, TimeSpan } from "lucia";
|
||||
import { DrizzleSQLiteAdapter } from "@lucia-auth/adapter-drizzle";
|
||||
import db from "@server/db";
|
||||
import { sessions, users } from "@server/db/schema";
|
||||
import environment from "@server/environment";
|
||||
|
||||
const adapter = new DrizzleSQLiteAdapter(db, sessions, users);
|
||||
|
||||
export const lucia = new Lucia(adapter, {
|
||||
getUserAttributes: (attributes) => {
|
||||
return {
|
||||
username: attributes.username,
|
||||
};
|
||||
},
|
||||
// getSessionAttributes: (attributes) => {
|
||||
// return {
|
||||
// country: attributes.country,
|
||||
// };
|
||||
// },
|
||||
sessionCookie: {
|
||||
name: "session",
|
||||
expires: false, // session cookies have very long lifespan (2 years)
|
||||
attributes: {
|
||||
secure: environment.ENVIRONMENT === "prod",
|
||||
sameSite: "strict",
|
||||
// domain: "example.com"
|
||||
},
|
||||
},
|
||||
sessionExpiresIn: new TimeSpan(2, "w"),
|
||||
});
|
||||
|
||||
export default lucia;
|
||||
|
||||
// IMPORTANT!
|
||||
declare module "lucia" {
|
||||
interface Register {
|
||||
Lucia: typeof lucia;
|
||||
DatabaseUserAttributes: DatabaseUserAttributes;
|
||||
DatabaseSessionAttributes: DatabaseSessionAttributes;
|
||||
}
|
||||
}
|
||||
|
||||
interface DatabaseUserAttributes {
|
||||
username: string;
|
||||
passwordHash: string;
|
||||
}
|
||||
|
||||
interface DatabaseSessionAttributes {
|
||||
// country: string;
|
||||
}
|
78
server/auth/login.ts
Normal file
78
server/auth/login.ts
Normal file
|
@ -0,0 +1,78 @@
|
|||
import { verify } from "@node-rs/argon2";
|
||||
import lucia from "@server/auth";
|
||||
import db from "@server/db";
|
||||
import { users } from "@server/db/schema";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import response from "@server/utils/response";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import { z } from "zod";
|
||||
import { fromError } from "zod-validation-error";
|
||||
|
||||
export const loginBodySchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string(),
|
||||
});
|
||||
|
||||
export async function login(req: Request, res: Response, next: NextFunction) {
|
||||
const parsedBody = loginBodySchema.safeParse(req.body);
|
||||
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const { email, password } = parsedBody.data;
|
||||
|
||||
const existingUserRes = await db
|
||||
.select()
|
||||
.from(users)
|
||||
.where(eq(users.email, email));
|
||||
if (!existingUserRes || !existingUserRes.length) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"A user with that email address does not exist",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const existingUser = existingUserRes[0];
|
||||
|
||||
const validPassword = await verify(existingUser.passwordHash, password, {
|
||||
memoryCost: 19456,
|
||||
timeCost: 2,
|
||||
outputLen: 32,
|
||||
parallelism: 1,
|
||||
});
|
||||
if (!validPassword) {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500)); // delay to prevent brute force attacks
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"The password you entered is incorrect",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const session = await lucia.createSession(existingUser.id, {});
|
||||
res.appendHeader(
|
||||
"Set-Cookie",
|
||||
lucia.createSessionCookie(session.id).serialize(),
|
||||
);
|
||||
|
||||
return res.status(HttpCode.OK).send(
|
||||
response<null>({
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "Logged in successfully",
|
||||
status: HttpCode.OK,
|
||||
}),
|
||||
);
|
||||
}
|
93
server/auth/signup.ts
Normal file
93
server/auth/signup.ts
Normal file
|
@ -0,0 +1,93 @@
|
|||
import { NextFunction, Request, Response } from "express";
|
||||
import db from "@server/db";
|
||||
import { hash } from "@node-rs/argon2";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { z } from "zod";
|
||||
import { generateId } from "lucia";
|
||||
import { users } from "@server/db/schema";
|
||||
import lucia from "@server/auth";
|
||||
import { fromError } from "zod-validation-error";
|
||||
import createHttpError from "http-errors";
|
||||
import response from "@server/utils/response";
|
||||
import { SqliteError } from "better-sqlite3";
|
||||
|
||||
export const signupBodySchema = z.object({
|
||||
email: z.string().email(),
|
||||
password: z
|
||||
.string()
|
||||
.min(8, { message: "Password must be at least 8 characters long" })
|
||||
.max(31, { message: "Password must be at most 31 characters long" })
|
||||
.regex(/^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).*$/, {
|
||||
message: `Your password must meet the following conditions:
|
||||
- At least one uppercase English letter.
|
||||
- At least one lowercase English letter.
|
||||
- At least one digit.
|
||||
- At least one special character.`,
|
||||
}),
|
||||
});
|
||||
|
||||
export type SignUpBody = z.infer<typeof signupBodySchema>;
|
||||
|
||||
export async function signup(req: Request, res: Response, next: NextFunction) {
|
||||
const parsedBody = signupBodySchema.safeParse(req.body);
|
||||
|
||||
if (!parsedBody.success) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
fromError(parsedBody.error).toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const { email, password } = parsedBody.data;
|
||||
|
||||
const passwordHash = await hash(password, {
|
||||
// recommended minimum parameters
|
||||
memoryCost: 19456,
|
||||
timeCost: 2,
|
||||
outputLen: 32,
|
||||
parallelism: 1,
|
||||
});
|
||||
const userId = generateId(15);
|
||||
|
||||
try {
|
||||
await db.insert(users).values({
|
||||
id: userId,
|
||||
email: email,
|
||||
passwordHash,
|
||||
});
|
||||
|
||||
const session = await lucia.createSession(userId, {});
|
||||
res.appendHeader(
|
||||
"Set-Cookie",
|
||||
lucia.createSessionCookie(session.id).serialize(),
|
||||
);
|
||||
|
||||
return res.status(HttpCode.OK).send(
|
||||
response<null>({
|
||||
data: null,
|
||||
success: true,
|
||||
error: false,
|
||||
message: "User created successfully",
|
||||
status: HttpCode.OK,
|
||||
}),
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof SqliteError && e.code === "SQLITE_CONSTRAINT_UNIQUE") {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.BAD_REQUEST,
|
||||
"A user with that email address already exists",
|
||||
),
|
||||
);
|
||||
} else {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.INTERNAL_SERVER_ERROR,
|
||||
"Failed to create user",
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue