feat: implement auth system, passkeys, and user management
Add PostgreSQL and Redis integration for users and sessions Implement password and WebAuthn passkey login flows Add Docker stack, super-admin seeding, and protected routes
This commit is contained in:
106
server/utils/db-init.ts
Normal file
106
server/utils/db-init.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { randomUUID } from 'node:crypto'
|
||||
|
||||
import { DEFAULT_USER_PASSWORD } from '~~/shared/auth'
|
||||
|
||||
import { hashPassword } from './password'
|
||||
import { getSqlClient } from './postgres'
|
||||
|
||||
let databaseReadyPromise: Promise<void> | null = null
|
||||
|
||||
export async function ensureDatabaseReady() {
|
||||
if (!databaseReadyPromise) {
|
||||
databaseReadyPromise = initializeDatabase()
|
||||
}
|
||||
|
||||
return databaseReadyPromise
|
||||
}
|
||||
|
||||
async function initializeDatabase() {
|
||||
const sql = getSqlClient()
|
||||
|
||||
await sql`
|
||||
create table if not exists users (
|
||||
id text primary key,
|
||||
username text not null unique,
|
||||
full_name text not null,
|
||||
phone_number text,
|
||||
role text not null check (role in ('super_admin', 'staff')),
|
||||
password_hash text not null,
|
||||
must_change_password boolean not null default true,
|
||||
is_active boolean not null default true,
|
||||
created_by text references users(id) on delete set null,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
last_login_at timestamptz
|
||||
)
|
||||
`
|
||||
|
||||
await sql`
|
||||
alter table users
|
||||
add column if not exists phone_number text
|
||||
`
|
||||
|
||||
await sql`
|
||||
create table if not exists user_passkeys (
|
||||
id text primary key,
|
||||
user_id text not null references users(id) on delete cascade,
|
||||
credential_id text not null unique,
|
||||
public_key text not null,
|
||||
counter bigint not null default 0,
|
||||
device_type text not null check (device_type in ('singleDevice', 'multiDevice')),
|
||||
backed_up boolean not null default false,
|
||||
transports jsonb not null default '[]'::jsonb,
|
||||
label text not null,
|
||||
created_at timestamptz not null default now(),
|
||||
last_used_at timestamptz
|
||||
)
|
||||
`
|
||||
|
||||
await sql`
|
||||
create index if not exists user_passkeys_user_id_idx
|
||||
on user_passkeys (user_id)
|
||||
`
|
||||
|
||||
const [existingSuperAdmin] = await sql<{ id: string }[]>`
|
||||
select id
|
||||
from users
|
||||
where username = 'xiaomai'
|
||||
limit 1
|
||||
`
|
||||
|
||||
if (!existingSuperAdmin) {
|
||||
const passwordHash = await hashPassword(DEFAULT_USER_PASSWORD)
|
||||
|
||||
await sql`
|
||||
insert into users (
|
||||
id,
|
||||
username,
|
||||
full_name,
|
||||
role,
|
||||
password_hash,
|
||||
must_change_password,
|
||||
is_active,
|
||||
created_by
|
||||
)
|
||||
values (
|
||||
${randomUUID()},
|
||||
'xiaomai',
|
||||
'Xiaomai',
|
||||
'super_admin',
|
||||
${passwordHash},
|
||||
true,
|
||||
true,
|
||||
null
|
||||
)
|
||||
`
|
||||
}
|
||||
|
||||
await sql`
|
||||
update users
|
||||
set
|
||||
phone_number = '601157753558',
|
||||
updated_at = now()
|
||||
where username = 'xiaomai'
|
||||
and (phone_number is null or phone_number = '')
|
||||
`
|
||||
}
|
||||
Reference in New Issue
Block a user