First Commit
Build and Push Auth Portal Docker Image / build-and-push (push) Successful in 5m26s
Build and Push Auth Portal Docker Image / create-release (push) Skipped

This commit is contained in:
Denozordec
2026-07-18 13:27:37 +07:00
commit bac95bdb2e
154 changed files with 26610 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
import { defineConfig } from 'drizzle-kit'
export default defineConfig({
schema: './src/schema/index.ts',
out: './migrations',
dialect: 'sqlite',
dbCredentials: {
url: process.env.DATABASE_URL?.replace(/^sqlite:/, '') ?? './data/app.db',
},
})
+30
View File
@@ -0,0 +1,30 @@
{
"name": "@authportal/db",
"version": "0.0.0",
"private": true,
"type": "module",
"files": ["dist", "migrations", "package.json"],
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
}
},
"scripts": {
"build": "tsup src/index.ts --format esm --dts",
"dev": "tsup src/index.ts --format esm --dts --watch",
"db:generate": "drizzle-kit generate",
"db:push": "drizzle-kit push"
},
"dependencies": {
"@authportal/shared": "workspace:*",
"better-sqlite3": "^11.10.0",
"drizzle-orm": "^0.44.2"
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.13",
"drizzle-kit": "^0.31.1",
"tsup": "^8.5.0",
"typescript": "^5.8.3"
}
}
+74
View File
@@ -0,0 +1,74 @@
import Database from 'better-sqlite3'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import * as schema from './schema/index.js'
export type Sqlite = Database.Database
export type AppDb = ReturnType<typeof drizzle<typeof schema>>
export function resolveDatabasePath(databaseUrl: string): string {
return databaseUrl.startsWith('sqlite:')
? databaseUrl.slice('sqlite:'.length)
: databaseUrl
}
export function createDb(databaseUrl: string): { db: AppDb; sqlite: Sqlite } {
const path = resolveDatabasePath(databaseUrl)
const sqlite = new Database(path)
sqlite.pragma('journal_mode = WAL')
sqlite.pragma('foreign_keys = ON')
sqlite.pragma('synchronous = NORMAL')
const db = drizzle(sqlite, { schema })
return { db, sqlite }
}
export function createMemoryDb(): { db: AppDb; sqlite: Sqlite } {
const sqlite = new Database(':memory:')
sqlite.pragma('foreign_keys = ON')
const db = drizzle(sqlite, { schema })
return { db, sqlite }
}
export function migrateSchema(sqlite: Sqlite): void {
sqlite.exec(`
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY NOT NULL,
email TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
password_hash TEXT NOT NULL,
is_admin INTEGER NOT NULL DEFAULT 0,
disabled INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS user_apps (
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
app_id TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS user_permissions (
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
permission TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS refresh_sessions (
id TEXT PRIMARY KEY NOT NULL,
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash TEXT NOT NULL,
expires_at TEXT NOT NULL,
revoked_at TEXT,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_user_apps_user ON user_apps(user_id);
CREATE INDEX IF NOT EXISTS idx_user_permissions_user ON user_permissions(user_id);
CREATE INDEX IF NOT EXISTS idx_refresh_sessions_user ON refresh_sessions(user_id);
`)
}
export function healthCheck(sqlite: Sqlite): void {
sqlite.prepare('SELECT 1').get()
}
export * from './schema/index.js'
export * from './users.js'
+37
View File
@@ -0,0 +1,37 @@
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
id: text('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name').notNull(),
passwordHash: text('password_hash').notNull(),
isAdmin: integer('is_admin', { mode: 'boolean' }).notNull().default(false),
disabled: integer('disabled', { mode: 'boolean' }).notNull().default(false),
createdAt: text('created_at').notNull(),
updatedAt: text('updated_at').notNull(),
})
export const userApps = sqliteTable('user_apps', {
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
appId: text('app_id').notNull(),
})
export const userPermissions = sqliteTable('user_permissions', {
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
permission: text('permission').notNull(),
})
export const refreshSessions = sqliteTable('refresh_sessions', {
id: text('id').primaryKey(),
userId: text('user_id')
.notNull()
.references(() => users.id, { onDelete: 'cascade' }),
tokenHash: text('token_hash').notNull(),
expiresAt: text('expires_at').notNull(),
revokedAt: text('revoked_at'),
createdAt: text('created_at').notNull(),
})
+157
View File
@@ -0,0 +1,157 @@
import { eq } from 'drizzle-orm'
import { createHash, randomUUID } from 'node:crypto'
import type { AppDb } from './index.js'
import {
refreshSessions,
userApps,
userPermissions,
users,
} from './schema/index.js'
export type UserRow = typeof users.$inferSelect
export function hashToken(token: string): string {
return createHash('sha256').update(token).digest('hex')
}
export function listUsers(db: AppDb): UserRow[] {
return db.select().from(users).all()
}
export function getUserById(db: AppDb, id: string): UserRow | undefined {
return db.select().from(users).where(eq(users.id, id)).get()
}
export function getUserByEmail(
db: AppDb,
email: string,
): UserRow | undefined {
return db
.select()
.from(users)
.where(eq(users.email, email.toLowerCase()))
.get()
}
export function getUserApps(db: AppDb, userId: string): string[] {
return db
.select()
.from(userApps)
.where(eq(userApps.userId, userId))
.all()
.map((r) => r.appId)
}
export function getUserPermissions(db: AppDb, userId: string): string[] {
return db
.select()
.from(userPermissions)
.where(eq(userPermissions.userId, userId))
.all()
.map((r) => r.permission)
}
export function createUser(
db: AppDb,
input: {
email: string
name: string
passwordHash: string
isAdmin: boolean
},
): UserRow {
const now = new Date().toISOString()
const id = randomUUID()
db.insert(users)
.values({
id,
email: input.email.toLowerCase(),
name: input.name,
passwordHash: input.passwordHash,
isAdmin: input.isAdmin,
disabled: false,
createdAt: now,
updatedAt: now,
})
.run()
return getUserById(db, id)!
}
export function updateUser(
db: AppDb,
id: string,
patch: {
email?: string
name?: string
passwordHash?: string
isAdmin?: boolean
disabled?: boolean
},
): UserRow | undefined {
const existing = getUserById(db, id)
if (!existing) return undefined
const now = new Date().toISOString()
db.update(users)
.set({
email: patch.email?.toLowerCase() ?? existing.email,
name: patch.name ?? existing.name,
passwordHash: patch.passwordHash ?? existing.passwordHash,
isAdmin: patch.isAdmin ?? existing.isAdmin,
disabled: patch.disabled ?? existing.disabled,
updatedAt: now,
})
.where(eq(users.id, id))
.run()
return getUserById(db, id)
}
export function deleteUser(db: AppDb, id: string): boolean {
const result = db.delete(users).where(eq(users.id, id)).run()
return result.changes > 0
}
export function setUserAccess(
db: AppDb,
userId: string,
apps: string[],
permissions: string[],
): void {
db.delete(userApps).where(eq(userApps.userId, userId)).run()
db.delete(userPermissions).where(eq(userPermissions.userId, userId)).run()
for (const appId of apps) {
db.insert(userApps).values({ userId, appId }).run()
}
for (const permission of permissions) {
db.insert(userPermissions).values({ userId, permission }).run()
}
db.update(users)
.set({ updatedAt: new Date().toISOString() })
.where(eq(users.id, userId))
.run()
}
export function createRefreshSession(
db: AppDb,
userId: string,
rawToken: string,
expiresAt: Date,
): void {
db.insert(refreshSessions)
.values({
id: randomUUID(),
userId,
tokenHash: hashToken(rawToken),
expiresAt: expiresAt.toISOString(),
revokedAt: null,
createdAt: new Date().toISOString(),
})
.run()
}
export function revokeRefreshSession(db: AppDb, rawToken: string): void {
const now = new Date().toISOString()
db.update(refreshSessions)
.set({ revokedAt: now })
.where(eq(refreshSessions.tokenHash, hashToken(rawToken)))
.run()
}
+13
View File
@@ -0,0 +1,13 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"declaration": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"]
}