ReviewOS

also looking at this

stacks/postline

Single-user auth: login, first-run registration, page + API gates

#11
Merged glennmichael123 wants to merge feat/auth into main
9 files +254 -22
app/Actions/Postline/RegisterFirstUserAction.tsadded+63-0
Changes to app/Actions/Postline/RegisterFirstUserAction.ts
@@ -0,0 +1,63 @@
1import type { RequestInstance } from '@stacksjs/types'
2import { Action } from '@stacksjs/actions'
3import { register } from '@stacksjs/auth'
4import { db } from '@stacksjs/database'
5import { response } from '@stacksjs/router'
6
7const database = db as any
8
9/**
10 * Postline is a single-user workspace: the register endpoint only works
11 * while no account exists, so a deployed instance can't be signed up on
12 * by strangers.
13 */
14export default new Action({
15 name: 'Postline Register First User',
16 description: 'Create the workspace account — only available on first run.',
17 method: 'POST',
18
19 async handle(request: RequestInstance) {
20 const existing = await database
21 .selectFrom('users')
22 .select(['id'])
23 .executeTakeFirst()
24
25 if (existing) {
26 return response.json({
27 ok: false,
28 error: 'This workspace already has an account. Sign in instead.',
29 }, { status: 403 })
30 }
31
32 const email = String(request.get('email') || '').trim()
33 const password = String(request.get('password') || '')
34 const name = String(request.get('name') || '').trim()
35
36 if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
37 return response.json({ ok: false, error: 'Email must be a valid email address.' }, { status: 422 })
38 if (password.length < 6 || password.length > 255)
39 return response.json({ ok: false, error: 'Password must be between 6 and 255 characters.' }, { status: 422 })
40 if (name.length < 2 || name.length > 255)
41 return response.json({ ok: false, error: 'Name must be between 2 and 255 characters.' }, { status: 422 })
42
43 try {
44 const result = await register({ email, password, name })
45 if (!result)
46 throw new Error('Registration failed.')
47
48 return response.json({
49 ok: true,
50 data: {
51 access_token: (result as any).token,
52 token_type: 'Bearer',
53 },
54 })
55 }
56 catch (error) {
57 return response.json({
58 ok: false,
59 error: error instanceof Error ? error.message : String(error),
60 }, { status: 422 })
61 }
62 },
63})