aboutsummaryrefslogtreecommitdiffhomepage
path: root/pkgs/server/src/routes/auth.ts
diff options
context:
space:
mode:
Diffstat (limited to 'pkgs/server/src/routes/auth.ts')
-rw-r--r--pkgs/server/src/routes/auth.ts351
1 files changed, 163 insertions, 188 deletions
diff --git a/pkgs/server/src/routes/auth.ts b/pkgs/server/src/routes/auth.ts
index a2e6c8e..e1f7ebb 100644
--- a/pkgs/server/src/routes/auth.ts
+++ b/pkgs/server/src/routes/auth.ts
@@ -5,11 +5,15 @@ import {
refreshTokenSchema,
} from "@kioku/shared";
import * as argon2 from "argon2";
-import { and, eq, gt } from "drizzle-orm";
import { Hono } from "hono";
import { sign } from "hono/jwt";
-import { db, refreshTokens, users } from "../db";
import { Errors } from "../middleware";
+import {
+ type RefreshTokenRepository,
+ refreshTokenRepository,
+ type UserRepository,
+ userRepository,
+} from "../repositories";
const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
@@ -26,199 +30,170 @@ function hashToken(token: string): string {
return createHash("sha256").update(token).digest("hex");
}
-const auth = new Hono();
-
-auth.post("/register", async (c) => {
- const body = await c.req.json();
-
- const parsed = createUserSchema.safeParse(body);
- if (!parsed.success) {
- throw Errors.validationError(parsed.error.issues[0]?.message);
- }
-
- const { username, password } = parsed.data;
-
- // Check if username already exists
- const existingUser = await db
- .select({ id: users.id })
- .from(users)
- .where(eq(users.username, username))
- .limit(1);
-
- if (existingUser.length > 0) {
- throw Errors.conflict("Username already exists", "USERNAME_EXISTS");
- }
-
- // Hash password with Argon2
- const passwordHash = await argon2.hash(password);
-
- // Create user
- const [newUser] = await db
- .insert(users)
- .values({
- username,
- passwordHash,
- })
- .returning({
- id: users.id,
- username: users.username,
- createdAt: users.createdAt,
- });
+export interface AuthDependencies {
+ userRepo: UserRepository;
+ refreshTokenRepo: RefreshTokenRepository;
+}
- return c.json({ user: newUser }, 201);
-});
+export function createAuthRouter(deps: AuthDependencies) {
+ const { userRepo, refreshTokenRepo } = deps;
+ const auth = new Hono();
-auth.post("/login", async (c) => {
- const body = await c.req.json();
-
- const parsed = loginSchema.safeParse(body);
- if (!parsed.success) {
- throw Errors.validationError(parsed.error.issues[0]?.message);
- }
-
- const { username, password } = parsed.data;
-
- // Find user by username
- const [user] = await db
- .select({
- id: users.id,
- username: users.username,
- passwordHash: users.passwordHash,
- })
- .from(users)
- .where(eq(users.username, username))
- .limit(1);
-
- if (!user) {
- throw Errors.unauthorized(
- "Invalid username or password",
- "INVALID_CREDENTIALS",
- );
- }
-
- // Verify password
- const isPasswordValid = await argon2.verify(user.passwordHash, password);
- if (!isPasswordValid) {
- throw Errors.unauthorized(
- "Invalid username or password",
- "INVALID_CREDENTIALS",
- );
- }
-
- // Generate JWT access token
- const now = Math.floor(Date.now() / 1000);
- const accessToken = await sign(
- {
- sub: user.id,
- iat: now,
- exp: now + ACCESS_TOKEN_EXPIRES_IN,
- },
- JWT_SECRET,
- );
-
- // Generate refresh token
- const refreshToken = generateRefreshToken();
- const tokenHash = hashToken(refreshToken);
- const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRES_IN * 1000);
-
- // Store refresh token in database
- await db.insert(refreshTokens).values({
- userId: user.id,
- tokenHash,
- expiresAt,
- });
+ auth.post("/register", async (c) => {
+ const body = await c.req.json();
- return c.json({
- accessToken,
- refreshToken,
- user: {
- id: user.id,
- username: user.username,
- },
+ const parsed = createUserSchema.safeParse(body);
+ if (!parsed.success) {
+ throw Errors.validationError(parsed.error.issues[0]?.message);
+ }
+
+ const { username, password } = parsed.data;
+
+ // Check if username already exists
+ const exists = await userRepo.existsByUsername(username);
+ if (exists) {
+ throw Errors.conflict("Username already exists", "USERNAME_EXISTS");
+ }
+
+ // Hash password with Argon2
+ const passwordHash = await argon2.hash(password);
+
+ // Create user
+ const newUser = await userRepo.create({ username, passwordHash });
+
+ return c.json({ user: newUser }, 201);
});
-});
-auth.post("/refresh", async (c) => {
- const body = await c.req.json();
-
- const parsed = refreshTokenSchema.safeParse(body);
- if (!parsed.success) {
- throw Errors.validationError(parsed.error.issues[0]?.message);
- }
-
- const { refreshToken } = parsed.data;
- const tokenHash = hashToken(refreshToken);
-
- // Find valid refresh token
- const [storedToken] = await db
- .select({
- id: refreshTokens.id,
- userId: refreshTokens.userId,
- expiresAt: refreshTokens.expiresAt,
- })
- .from(refreshTokens)
- .where(
- and(
- eq(refreshTokens.tokenHash, tokenHash),
- gt(refreshTokens.expiresAt, new Date()),
- ),
- )
- .limit(1);
-
- if (!storedToken) {
- throw Errors.unauthorized(
- "Invalid or expired refresh token",
- "INVALID_REFRESH_TOKEN",
+ auth.post("/login", async (c) => {
+ const body = await c.req.json();
+
+ const parsed = loginSchema.safeParse(body);
+ if (!parsed.success) {
+ throw Errors.validationError(parsed.error.issues[0]?.message);
+ }
+
+ const { username, password } = parsed.data;
+
+ // Find user by username
+ const user = await userRepo.findByUsername(username);
+
+ if (!user) {
+ throw Errors.unauthorized(
+ "Invalid username or password",
+ "INVALID_CREDENTIALS",
+ );
+ }
+
+ // Verify password
+ const isPasswordValid = await argon2.verify(user.passwordHash, password);
+ if (!isPasswordValid) {
+ throw Errors.unauthorized(
+ "Invalid username or password",
+ "INVALID_CREDENTIALS",
+ );
+ }
+
+ // Generate JWT access token
+ const now = Math.floor(Date.now() / 1000);
+ const accessToken = await sign(
+ {
+ sub: user.id,
+ iat: now,
+ exp: now + ACCESS_TOKEN_EXPIRES_IN,
+ },
+ JWT_SECRET,
);
- }
-
- // Get user info
- const [user] = await db
- .select({
- id: users.id,
- username: users.username,
- })
- .from(users)
- .where(eq(users.id, storedToken.userId))
- .limit(1);
-
- if (!user) {
- throw Errors.unauthorized("User not found", "USER_NOT_FOUND");
- }
-
- // Delete old refresh token (rotation)
- await db.delete(refreshTokens).where(eq(refreshTokens.id, storedToken.id));
-
- // Generate new access token
- const now = Math.floor(Date.now() / 1000);
- const accessToken = await sign(
- {
- sub: user.id,
- iat: now,
- exp: now + ACCESS_TOKEN_EXPIRES_IN,
- },
- JWT_SECRET,
- );
-
- // Generate new refresh token (rotation)
- const newRefreshToken = generateRefreshToken();
- const newTokenHash = hashToken(newRefreshToken);
- const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRES_IN * 1000);
-
- await db.insert(refreshTokens).values({
- userId: user.id,
- tokenHash: newTokenHash,
- expiresAt,
+
+ // Generate refresh token
+ const refreshToken = generateRefreshToken();
+ const tokenHash = hashToken(refreshToken);
+ const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRES_IN * 1000);
+
+ // Store refresh token in database
+ await refreshTokenRepo.create({
+ userId: user.id,
+ tokenHash,
+ expiresAt,
+ });
+
+ return c.json({
+ accessToken,
+ refreshToken,
+ user: {
+ id: user.id,
+ username: user.username,
+ },
+ });
});
- return c.json({
- accessToken,
- refreshToken: newRefreshToken,
- user: {
- id: user.id,
- username: user.username,
- },
+ auth.post("/refresh", async (c) => {
+ const body = await c.req.json();
+
+ const parsed = refreshTokenSchema.safeParse(body);
+ if (!parsed.success) {
+ throw Errors.validationError(parsed.error.issues[0]?.message);
+ }
+
+ const { refreshToken } = parsed.data;
+ const tokenHash = hashToken(refreshToken);
+
+ // Find valid refresh token
+ const storedToken = await refreshTokenRepo.findValidToken(tokenHash);
+
+ if (!storedToken) {
+ throw Errors.unauthorized(
+ "Invalid or expired refresh token",
+ "INVALID_REFRESH_TOKEN",
+ );
+ }
+
+ // Get user info
+ const user = await userRepo.findById(storedToken.userId);
+
+ if (!user) {
+ throw Errors.unauthorized("User not found", "USER_NOT_FOUND");
+ }
+
+ // Delete old refresh token (rotation)
+ await refreshTokenRepo.deleteById(storedToken.id);
+
+ // Generate new access token
+ const now = Math.floor(Date.now() / 1000);
+ const accessToken = await sign(
+ {
+ sub: user.id,
+ iat: now,
+ exp: now + ACCESS_TOKEN_EXPIRES_IN,
+ },
+ JWT_SECRET,
+ );
+
+ // Generate new refresh token (rotation)
+ const newRefreshToken = generateRefreshToken();
+ const newTokenHash = hashToken(newRefreshToken);
+ const expiresAt = new Date(Date.now() + REFRESH_TOKEN_EXPIRES_IN * 1000);
+
+ await refreshTokenRepo.create({
+ userId: user.id,
+ tokenHash: newTokenHash,
+ expiresAt,
+ });
+
+ return c.json({
+ accessToken,
+ refreshToken: newRefreshToken,
+ user: {
+ id: user.id,
+ username: user.username,
+ },
+ });
});
-});
-export { auth };
+ return auth;
+}
+
+// Default auth router with real repositories for production use
+export const auth = createAuthRouter({
+ userRepo: userRepository,
+ refreshTokenRepo: refreshTokenRepository,
+});