Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesPull RequestsActionsSecurityInsightsSettings
✨ AI
More
Blame · Line-by-line history

auth.tsx

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

auth.tsxBlame326 lines · 1 contributor
06d5ffeClaude1/**
2 * Auth routes — register, login, logout (web + API).
3 */
4
5import { Hono } from "hono";
6import { setCookie, deleteCookie } from "hono/cookie";
7import { eq } from "drizzle-orm";
8import { db } from "../db";
9import { users, sessions } from "../db/schema";
10import {
11 hashPassword,
12 verifyPassword,
13 generateSessionToken,
14 sessionCookieOptions,
15 sessionExpiry,
16} from "../lib/auth";
17import { Layout } from "../views/layout";
18import type { AuthEnv } from "../middleware/auth";
19
20const auth = new Hono<AuthEnv>();
21
22// --- Web UI ---
23
24auth.get("/register", (c) => {
25 const error = c.req.query("error");
26 return c.html(
27 <Layout title="Register">
28 <div class="auth-container">
29 <h2>Create account</h2>
30 {error && <div class="auth-error">{decodeURIComponent(error)}</div>}
31 <form method="POST" action="/register">
32 <div class="form-group">
33 <label for="username">Username</label>
34 <input
35 type="text"
36 id="username"
37 name="username"
38 required
39 pattern="^[a-zA-Z0-9_-]+$"
40 minLength={2}
41 maxLength={39}
42 placeholder="your-username"
43 autocomplete="username"
44 />
45 </div>
46 <div class="form-group">
47 <label for="email">Email</label>
48 <input
49 type="email"
50 id="email"
51 name="email"
52 required
53 placeholder="you@example.com"
54 autocomplete="email"
55 />
56 </div>
57 <div class="form-group">
58 <label for="password">Password</label>
59 <input
60 type="password"
61 id="password"
62 name="password"
63 required
64 minLength={8}
65 placeholder="Min 8 characters"
66 autocomplete="new-password"
67 />
68 </div>
69 <button type="submit" class="btn btn-primary">
70 Create account
71 </button>
72 </form>
73 <p class="auth-switch">
74 Already have an account? <a href="/login">Sign in</a>
75 </p>
76 </div>
77 </Layout>
78 );
79});
80
81auth.post("/register", async (c) => {
82 const body = await c.req.parseBody();
83 const username = String(body.username || "").trim();
84 const email = String(body.email || "").trim();
85 const password = String(body.password || "");
86
87 if (!username || !email || !password) {
88 return c.redirect("/register?error=All+fields+are+required");
89 }
90
91 if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
92 return c.redirect(
93 "/register?error=Username+may+only+contain+letters%2C+numbers%2C+hyphens+and+underscores"
94 );
95 }
96
97 if (password.length < 8) {
98 return c.redirect("/register?error=Password+must+be+at+least+8+characters");
99 }
100
101 // Check existing
102 const [existingUser] = await db
103 .select()
104 .from(users)
105 .where(eq(users.username, username))
106 .limit(1);
107 if (existingUser) {
108 return c.redirect("/register?error=Username+already+taken");
109 }
110
111 const [existingEmail] = await db
112 .select()
113 .from(users)
114 .where(eq(users.email, email))
115 .limit(1);
116 if (existingEmail) {
117 return c.redirect("/register?error=Email+already+registered");
118 }
119
120 const passwordHash = await hashPassword(password);
121
122 const [user] = await db
123 .insert(users)
124 .values({ username, email, passwordHash })
125 .returning();
126
127 // Create session
128 const token = generateSessionToken();
129 await db.insert(sessions).values({
130 userId: user.id,
131 token,
132 expiresAt: sessionExpiry(),
133 });
134
135 setCookie(c, "session", token, sessionCookieOptions());
136
137 const redirect = c.req.query("redirect") || "/";
138 return c.redirect(redirect);
139});
140
141auth.get("/login", (c) => {
142 const error = c.req.query("error");
143 const redirect = c.req.query("redirect") || "";
144 return c.html(
145 <Layout title="Sign in">
146 <div class="auth-container">
147 <h2>Sign in</h2>
148 {error && <div class="auth-error">{decodeURIComponent(error)}</div>}
149 <form
150 method="POST"
151 action={`/login${redirect ? `?redirect=${encodeURIComponent(redirect)}` : ""}`}
152 >
153 <div class="form-group">
154 <label for="username">Username or email</label>
155 <input
156 type="text"
157 id="username"
158 name="username"
159 required
160 placeholder="username or email"
161 autocomplete="username"
162 />
163 </div>
164 <div class="form-group">
165 <label for="password">Password</label>
166 <input
167 type="password"
168 id="password"
169 name="password"
170 required
171 placeholder="Password"
172 autocomplete="current-password"
173 />
174 </div>
175 <button type="submit" class="btn btn-primary">
176 Sign in
177 </button>
178 </form>
179 <p class="auth-switch">
180 New to gluecron? <a href="/register">Create an account</a>
181 </p>
182 </div>
183 </Layout>
184 );
185});
186
187auth.post("/login", async (c) => {
188 const body = await c.req.parseBody();
189 const identifier = String(body.username || "").trim();
190 const password = String(body.password || "");
191 const redirect = c.req.query("redirect") || "/";
192
193 if (!identifier || !password) {
194 return c.redirect("/login?error=All+fields+are+required");
195 }
196
197 // Find user by username or email
198 const isEmail = identifier.includes("@");
199 const [user] = await db
200 .select()
201 .from(users)
202 .where(
203 isEmail
204 ? eq(users.email, identifier)
205 : eq(users.username, identifier)
206 )
207 .limit(1);
208
209 if (!user) {
210 return c.redirect("/login?error=Invalid+credentials");
211 }
212
213 const valid = await verifyPassword(password, user.passwordHash);
214 if (!valid) {
215 return c.redirect("/login?error=Invalid+credentials");
216 }
217
218 const token = generateSessionToken();
219 await db.insert(sessions).values({
220 userId: user.id,
221 token,
222 expiresAt: sessionExpiry(),
223 });
224
225 setCookie(c, "session", token, sessionCookieOptions());
226 return c.redirect(redirect);
227});
228
229auth.get("/logout", async (c) => {
230 deleteCookie(c, "session", { path: "/" });
231 return c.redirect("/");
232});
233
234// --- API ---
235
236auth.post("/api/auth/register", async (c) => {
237 const body = await c.req.json<{
238 username: string;
239 email: string;
240 password: string;
241 }>();
242
243 if (!body.username || !body.email || !body.password) {
244 return c.json({ error: "username, email, and password are required" }, 400);
245 }
246
247 if (!/^[a-zA-Z0-9_-]+$/.test(body.username)) {
248 return c.json({ error: "Invalid username" }, 400);
249 }
250
251 if (body.password.length < 8) {
252 return c.json({ error: "Password must be at least 8 characters" }, 400);
253 }
254
255 const [existing] = await db
256 .select()
257 .from(users)
258 .where(eq(users.username, body.username))
259 .limit(1);
260 if (existing) {
261 return c.json({ error: "Username already taken" }, 409);
262 }
263
264 const passwordHash = await hashPassword(body.password);
265 const [user] = await db
266 .insert(users)
267 .values({
268 username: body.username,
269 email: body.email,
270 passwordHash,
271 })
272 .returning();
273
274 const token = generateSessionToken();
275 await db.insert(sessions).values({
276 userId: user.id,
277 token,
278 expiresAt: sessionExpiry(),
279 });
280
281 return c.json(
282 {
283 user: { id: user.id, username: user.username, email: user.email },
284 token,
285 },
286 201
287 );
288});
289
290auth.post("/api/auth/login", async (c) => {
291 const body = await c.req.json<{ username: string; password: string }>();
292
293 if (!body.username || !body.password) {
294 return c.json({ error: "username and password are required" }, 400);
295 }
296
297 const isEmail = body.username.includes("@");
298 const [user] = await db
299 .select()
300 .from(users)
301 .where(
302 isEmail
303 ? eq(users.email, body.username)
304 : eq(users.username, body.username)
305 )
306 .limit(1);
307
308 if (!user) return c.json({ error: "Invalid credentials" }, 401);
309
310 const valid = await verifyPassword(body.password, user.passwordHash);
311 if (!valid) return c.json({ error: "Invalid credentials" }, 401);
312
313 const token = generateSessionToken();
314 await db.insert(sessions).values({
315 userId: user.id,
316 token,
317 expiresAt: sessionExpiry(),
318 });
319
320 return c.json({
321 user: { id: user.id, username: user.username, email: user.email },
322 token,
323 });
324});
325
326export default auth;