First Commit
This commit is contained in:
@@ -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',
|
||||
},
|
||||
})
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
@@ -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(),
|
||||
})
|
||||
@@ -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()
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"declaration": true,
|
||||
"outDir": "dist",
|
||||
"rootDir": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
Reference in New Issue
Block a user