aboutsummaryrefslogtreecommitdiffhomepage
path: root/pkgs/server/src/routes
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2025-12-03 06:05:33 +0900
committernsfisis <nsfisis@gmail.com>2025-12-04 23:26:34 +0900
commitdbcfe8a98e46e15fe951e5e98c68fd6ac8bde1b3 (patch)
tree5780b2d0b0f32735477ff8f1f3308720d059ce67 /pkgs/server/src/routes
parent20f159abf06599bc8902a445be21d4c085d82ede (diff)
downloadkioku-dbcfe8a98e46e15fe951e5e98c68fd6ac8bde1b3.tar.gz
kioku-dbcfe8a98e46e15fe951e5e98c68fd6ac8bde1b3.tar.zst
kioku-dbcfe8a98e46e15fe951e5e98c68fd6ac8bde1b3.zip
refactor(auth): introduce repository pattern for database access
Add repository types and implementations to abstract database operations, improving testability and separation of concerns. The auth routes now use dependency injection with UserRepository and RefreshTokenRepository interfaces, making tests simpler by mocking interfaces instead of Drizzle query builders. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Diffstat (limited to 'pkgs/server/src/routes')
-rw-r--r--pkgs/server/src/routes/auth.test.ts293
-rw-r--r--pkgs/server/src/routes/auth.ts351
2 files changed, 267 insertions, 377 deletions
diff --git a/pkgs/server/src/routes/auth.test.ts b/pkgs/server/src/routes/auth.test.ts
index 7de04f5..34eb2b6 100644
--- a/pkgs/server/src/routes/auth.test.ts
+++ b/pkgs/server/src/routes/auth.test.ts
@@ -1,60 +1,12 @@
import { Hono } from "hono";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { errorHandler } from "../middleware";
-import { auth } from "./auth";
-
-vi.mock("../db", () => {
- const mockUsers: Array<{
- id: string;
- username: string;
- passwordHash: string;
- createdAt: Date;
- }> = [];
-
- return {
- db: {
- select: vi.fn(() => ({
- from: vi.fn(() => ({
- where: vi.fn(() => ({
- limit: vi.fn(() =>
- Promise.resolve(
- mockUsers.filter((u) => u.username === "existinguser"),
- ),
- ),
- })),
- })),
- })),
- insert: vi.fn(() => ({
- values: vi.fn((data: { username: string; passwordHash: string }) => ({
- returning: vi.fn(() => {
- const newUser = {
- id: "test-uuid-123",
- username: data.username,
- createdAt: new Date("2024-01-01T00:00:00Z"),
- };
- mockUsers.push({ ...newUser, passwordHash: data.passwordHash });
- return Promise.resolve([newUser]);
- }),
- })),
- })),
- delete: vi.fn(() => ({
- where: vi.fn(() => Promise.resolve(undefined)),
- })),
- },
- users: {
- id: "id",
- username: "username",
- createdAt: "created_at",
- },
- refreshTokens: {
- id: "id",
- userId: "user_id",
- tokenHash: "token_hash",
- expiresAt: "expires_at",
- createdAt: "created_at",
- },
- };
-});
+import type {
+ RefreshTokenRepository,
+ UserPublic,
+ UserRepository,
+} from "../repositories";
+import { createAuthRouter } from "./auth";
vi.mock("argon2", () => ({
hash: vi.fn((password: string) => Promise.resolve(`hashed_${password}`)),
@@ -63,6 +15,23 @@ vi.mock("argon2", () => ({
),
}));
+function createMockUserRepo(): UserRepository {
+ return {
+ findByUsername: vi.fn(),
+ existsByUsername: vi.fn(),
+ create: vi.fn(),
+ findById: vi.fn(),
+ };
+}
+
+function createMockRefreshTokenRepo(): RefreshTokenRepository {
+ return {
+ findValidToken: vi.fn(),
+ create: vi.fn(),
+ deleteById: vi.fn(),
+ };
+}
+
interface RegisterResponse {
user?: {
id: string;
@@ -90,15 +59,30 @@ interface LoginResponse {
describe("POST /register", () => {
let app: Hono;
+ let mockUserRepo: ReturnType<typeof createMockUserRepo>;
+ let mockRefreshTokenRepo: ReturnType<typeof createMockRefreshTokenRepo>;
beforeEach(() => {
vi.clearAllMocks();
+ mockUserRepo = createMockUserRepo();
+ mockRefreshTokenRepo = createMockRefreshTokenRepo();
+ const auth = createAuthRouter({
+ userRepo: mockUserRepo,
+ refreshTokenRepo: mockRefreshTokenRepo,
+ });
app = new Hono();
app.onError(errorHandler);
app.route("/api/auth", auth);
});
it("creates a new user with valid credentials", async () => {
+ vi.mocked(mockUserRepo.existsByUsername).mockResolvedValue(false);
+ vi.mocked(mockUserRepo.create).mockResolvedValue({
+ id: "test-uuid-123",
+ username: "testuser",
+ createdAt: new Date("2024-01-01T00:00:00Z"),
+ } as UserPublic);
+
const res = await app.request("/api/auth/register", {
method: "POST",
headers: { "Content-Type": "application/json" },
@@ -115,6 +99,11 @@ describe("POST /register", () => {
username: "testuser",
createdAt: "2024-01-01T00:00:00.000Z",
});
+ expect(mockUserRepo.existsByUsername).toHaveBeenCalledWith("testuser");
+ expect(mockUserRepo.create).toHaveBeenCalledWith({
+ username: "testuser",
+ passwordHash: "hashed_securepassword12345",
+ });
});
it("returns 422 for invalid username", async () => {
@@ -148,15 +137,7 @@ describe("POST /register", () => {
});
it("returns 409 for existing username", async () => {
- const { db } = await import("../db");
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- vi.mocked(db.select).mockReturnValueOnce({
- from: vi.fn().mockReturnValue({
- where: vi.fn().mockReturnValue({
- limit: vi.fn().mockResolvedValue([{ id: "existing-id" }]),
- }),
- }),
- } as unknown as ReturnType<typeof db.select>);
+ vi.mocked(mockUserRepo.existsByUsername).mockResolvedValue(true);
const res = await app.request("/api/auth/register", {
method: "POST",
@@ -175,34 +156,29 @@ describe("POST /register", () => {
describe("POST /login", () => {
let app: Hono;
+ let mockUserRepo: ReturnType<typeof createMockUserRepo>;
+ let mockRefreshTokenRepo: ReturnType<typeof createMockRefreshTokenRepo>;
beforeEach(() => {
vi.clearAllMocks();
+ mockUserRepo = createMockUserRepo();
+ mockRefreshTokenRepo = createMockRefreshTokenRepo();
+ const auth = createAuthRouter({
+ userRepo: mockUserRepo,
+ refreshTokenRepo: mockRefreshTokenRepo,
+ });
app = new Hono();
app.onError(errorHandler);
app.route("/api/auth", auth);
});
it("returns access token for valid credentials", async () => {
- const { db } = await import("../db");
- vi.mocked(db.select).mockReturnValueOnce({
- from: vi.fn().mockReturnValue({
- where: vi.fn().mockReturnValue({
- limit: vi.fn().mockResolvedValue([
- {
- id: "user-uuid-123",
- username: "testuser",
- passwordHash: "hashed_correctpassword",
- },
- ]),
- }),
- }),
- } as unknown as ReturnType<typeof db.select>);
-
- // Mock the insert call for refresh token
- vi.mocked(db.insert).mockReturnValueOnce({
- values: vi.fn().mockResolvedValue(undefined),
- } as unknown as ReturnType<typeof db.insert>);
+ vi.mocked(mockUserRepo.findByUsername).mockResolvedValue({
+ id: "user-uuid-123",
+ username: "testuser",
+ passwordHash: "hashed_correctpassword",
+ });
+ vi.mocked(mockRefreshTokenRepo.create).mockResolvedValue(undefined);
const res = await app.request("/api/auth/login", {
method: "POST",
@@ -223,17 +199,15 @@ describe("POST /login", () => {
id: "user-uuid-123",
username: "testuser",
});
+ expect(mockRefreshTokenRepo.create).toHaveBeenCalledWith({
+ userId: "user-uuid-123",
+ tokenHash: expect.any(String),
+ expiresAt: expect.any(Date),
+ });
});
it("returns 401 for non-existent user", async () => {
- const { db } = await import("../db");
- vi.mocked(db.select).mockReturnValueOnce({
- from: vi.fn().mockReturnValue({
- where: vi.fn().mockReturnValue({
- limit: vi.fn().mockResolvedValue([]),
- }),
- }),
- } as unknown as ReturnType<typeof db.select>);
+ vi.mocked(mockUserRepo.findByUsername).mockResolvedValue(undefined);
const res = await app.request("/api/auth/login", {
method: "POST",
@@ -250,20 +224,11 @@ describe("POST /login", () => {
});
it("returns 401 for incorrect password", async () => {
- const { db } = await import("../db");
- vi.mocked(db.select).mockReturnValueOnce({
- from: vi.fn().mockReturnValue({
- where: vi.fn().mockReturnValue({
- limit: vi.fn().mockResolvedValue([
- {
- id: "user-uuid-123",
- username: "testuser",
- passwordHash: "hashed_correctpassword",
- },
- ]),
- }),
- }),
- } as unknown as ReturnType<typeof db.select>);
+ vi.mocked(mockUserRepo.findByUsername).mockResolvedValue({
+ id: "user-uuid-123",
+ username: "testuser",
+ passwordHash: "hashed_correctpassword",
+ });
const res = await app.request("/api/auth/login", {
method: "POST",
@@ -325,55 +290,34 @@ interface RefreshResponse {
describe("POST /refresh", () => {
let app: Hono;
+ let mockUserRepo: ReturnType<typeof createMockUserRepo>;
+ let mockRefreshTokenRepo: ReturnType<typeof createMockRefreshTokenRepo>;
beforeEach(() => {
vi.clearAllMocks();
+ mockUserRepo = createMockUserRepo();
+ mockRefreshTokenRepo = createMockRefreshTokenRepo();
+ const auth = createAuthRouter({
+ userRepo: mockUserRepo,
+ refreshTokenRepo: mockRefreshTokenRepo,
+ });
app = new Hono();
app.onError(errorHandler);
app.route("/api/auth", auth);
});
it("returns new tokens for valid refresh token", async () => {
- const { db } = await import("../db");
-
- // Mock finding valid refresh token
- vi.mocked(db.select).mockReturnValueOnce({
- from: vi.fn().mockReturnValue({
- where: vi.fn().mockReturnValue({
- limit: vi.fn().mockResolvedValue([
- {
- id: "token-id-123",
- userId: "user-uuid-123",
- expiresAt: new Date(Date.now() + 86400000), // expires in 1 day
- },
- ]),
- }),
- }),
- } as unknown as ReturnType<typeof db.select>);
-
- // Mock finding user
- vi.mocked(db.select).mockReturnValueOnce({
- from: vi.fn().mockReturnValue({
- where: vi.fn().mockReturnValue({
- limit: vi.fn().mockResolvedValue([
- {
- id: "user-uuid-123",
- username: "testuser",
- },
- ]),
- }),
- }),
- } as unknown as ReturnType<typeof db.select>);
-
- // Mock delete old token
- vi.mocked(db.delete).mockReturnValueOnce({
- where: vi.fn().mockResolvedValue(undefined),
- } as unknown as ReturnType<typeof db.delete>);
-
- // Mock insert new token
- vi.mocked(db.insert).mockReturnValueOnce({
- values: vi.fn().mockResolvedValue(undefined),
- } as unknown as ReturnType<typeof db.insert>);
+ vi.mocked(mockRefreshTokenRepo.findValidToken).mockResolvedValue({
+ id: "token-id-123",
+ userId: "user-uuid-123",
+ expiresAt: new Date(Date.now() + 86400000),
+ });
+ vi.mocked(mockUserRepo.findById).mockResolvedValue({
+ id: "user-uuid-123",
+ username: "testuser",
+ });
+ vi.mocked(mockRefreshTokenRepo.deleteById).mockResolvedValue(undefined);
+ vi.mocked(mockRefreshTokenRepo.create).mockResolvedValue(undefined);
const res = await app.request("/api/auth/refresh", {
method: "POST",
@@ -393,19 +337,18 @@ describe("POST /refresh", () => {
id: "user-uuid-123",
username: "testuser",
});
+ expect(mockRefreshTokenRepo.deleteById).toHaveBeenCalledWith(
+ "token-id-123",
+ );
+ expect(mockRefreshTokenRepo.create).toHaveBeenCalledWith({
+ userId: "user-uuid-123",
+ tokenHash: expect.any(String),
+ expiresAt: expect.any(Date),
+ });
});
it("returns 401 for invalid refresh token", async () => {
- const { db } = await import("../db");
-
- // Mock no token found
- vi.mocked(db.select).mockReturnValueOnce({
- from: vi.fn().mockReturnValue({
- where: vi.fn().mockReturnValue({
- limit: vi.fn().mockResolvedValue([]),
- }),
- }),
- } as unknown as ReturnType<typeof db.select>);
+ vi.mocked(mockRefreshTokenRepo.findValidToken).mockResolvedValue(undefined);
const res = await app.request("/api/auth/refresh", {
method: "POST",
@@ -421,16 +364,7 @@ describe("POST /refresh", () => {
});
it("returns 401 for expired refresh token", async () => {
- const { db } = await import("../db");
-
- // Mock no valid (non-expired) token found (empty result because expiry check in query)
- vi.mocked(db.select).mockReturnValueOnce({
- from: vi.fn().mockReturnValue({
- where: vi.fn().mockReturnValue({
- limit: vi.fn().mockResolvedValue([]),
- }),
- }),
- } as unknown as ReturnType<typeof db.select>);
+ vi.mocked(mockRefreshTokenRepo.findValidToken).mockResolvedValue(undefined);
const res = await app.request("/api/auth/refresh", {
method: "POST",
@@ -446,31 +380,12 @@ describe("POST /refresh", () => {
});
it("returns 401 when user not found", async () => {
- const { db } = await import("../db");
-
- // Mock finding valid refresh token
- vi.mocked(db.select).mockReturnValueOnce({
- from: vi.fn().mockReturnValue({
- where: vi.fn().mockReturnValue({
- limit: vi.fn().mockResolvedValue([
- {
- id: "token-id-123",
- userId: "deleted-user-id",
- expiresAt: new Date(Date.now() + 86400000),
- },
- ]),
- }),
- }),
- } as unknown as ReturnType<typeof db.select>);
-
- // Mock user not found
- vi.mocked(db.select).mockReturnValueOnce({
- from: vi.fn().mockReturnValue({
- where: vi.fn().mockReturnValue({
- limit: vi.fn().mockResolvedValue([]),
- }),
- }),
- } as unknown as ReturnType<typeof db.select>);
+ vi.mocked(mockRefreshTokenRepo.findValidToken).mockResolvedValue({
+ id: "token-id-123",
+ userId: "deleted-user-id",
+ expiresAt: new Date(Date.now() + 86400000),
+ });
+ vi.mocked(mockUserRepo.findById).mockResolvedValue(undefined);
const res = await app.request("/api/auth/refresh", {
method: "POST",
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,
+});