aboutsummaryrefslogtreecommitdiffhomepage
path: root/pkgs/server/src/routes/auth.ts
blob: ed497b17066f986e6cf24ddc93888ac8b5fbb5ab (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
import { createUserSchema, loginSchema } from "@kioku/shared";
import * as argon2 from "argon2";
import { eq } from "drizzle-orm";
import { Hono } from "hono";
import { sign } from "hono/jwt";
import { db, users } from "../db";
import { Errors } from "../middleware";

const JWT_SECRET = process.env.JWT_SECRET;
if (!JWT_SECRET) {
	throw new Error("JWT_SECRET environment variable is required");
}
const ACCESS_TOKEN_EXPIRES_IN = 60 * 15; // 15 minutes

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,
		});

	return c.json({ user: newUser }, 201);
});

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,
	);

	return c.json({
		accessToken,
		user: {
			id: user.id,
			username: user.username,
		},
	});
});

export { auth };