feat(csms): add deployment script and database schema for user authentication

This commit is contained in:
2025-11-17 03:34:51 +08:00
parent 09c5ca1d3f
commit 86595c7639
21 changed files with 2377 additions and 16 deletions

View File

@@ -0,0 +1,71 @@
import { pgTable, text, timestamp, boolean } from "drizzle-orm/pg-core";
export const user = pgTable("user", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
emailVerified: boolean("email_verified").default(false).notNull(),
image: text("image"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
username: text("username").unique(),
displayUsername: text("display_username"),
role: text("role").default("user"),
});
export const session = pgTable("session", {
id: text("id").primaryKey(),
expiresAt: timestamp("expires_at").notNull(),
token: text("token").notNull().unique(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
ipAddress: text("ip_address"),
userAgent: text("user_agent"),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
});
export const account = pgTable("account", {
id: text("id").primaryKey(),
accountId: text("account_id").notNull(),
providerId: text("provider_id").notNull(),
userId: text("user_id")
.notNull()
.references(() => user.id, { onDelete: "cascade" }),
accessToken: text("access_token"),
refreshToken: text("refresh_token"),
idToken: text("id_token"),
accessTokenExpiresAt: timestamp("access_token_expires_at"),
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
scope: text("scope"),
password: text("password"),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
});
export const verification = pgTable("verification", {
id: text("id").primaryKey(),
identifier: text("identifier").notNull(),
value: text("value").notNull(),
expiresAt: timestamp("expires_at").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at")
.defaultNow()
.$onUpdate(() => /* @__PURE__ */ new Date())
.notNull(),
});
export const jwks = pgTable("jwks", {
id: text("id").primaryKey(),
publicKey: text("public_key").notNull(),
privateKey: text("private_key").notNull(),
createdAt: timestamp("created_at").notNull(),
});

View File

@@ -0,0 +1 @@
export * from './auth-schema.ts'

View File

@@ -1,4 +1,4 @@
import pkg from '../../../package.json' with { type: 'json' }
import 'dotenv/config'
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import { createNodeWebSocket } from '@hono/node-ws'
@@ -6,25 +6,48 @@ import { getConnInfo } from '@hono/node-server/conninfo'
import { cors } from 'hono/cors'
import { logger } from 'hono/logger'
import { showRoutes } from 'hono/dev'
import { auth } from './lib/auth.ts'
const app = new Hono()
const app = new Hono<{
Variables: {
user: typeof auth.$Infer.Session.user | null
session: typeof auth.$Infer.Session.session | null
}
}>()
const { injectWebSocket, upgradeWebSocket } = createNodeWebSocket({ app })
app.use(logger())
app.use('/ocpp', cors({
origin: '*',
allowMethods: ['GET', 'POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
exposeHeaders: ['Content-Length'],
credentials: true,
}))
app.use('*', async (c, next) => {
const session = await auth.api.getSession({ headers: c.req.raw.headers })
if (!session) {
c.set('user', null)
c.set('session', null)
await next()
return
}
c.set('user', session.user)
c.set('session', session.session)
await next()
})
app.use(
'/api/auth/*',
cors({
origin: '*',
allowMethods: ['GET', 'POST', 'OPTIONS'],
allowHeaders: ['Content-Type', 'Authorization'],
exposeHeaders: ['Content-Length'],
credentials: true,
}),
)
app.on(['POST', 'GET'], '/api/auth/*', (c) => auth.handler(c.req.raw))
app.get('/', (c) => {
return c.json({
platform: 'Helios CSMS',
version: pkg.version,
message: 'ok',
})
})
@@ -35,7 +58,9 @@ app.get(
return {
onOpen(evt, ws) {
const connInfo = getConnInfo(c)
console.log(`New connection from ${connInfo.remote.address}:${connInfo.remote.port}`)
console.log(
`New connection from ${connInfo.remote.address}:${connInfo.remote.port}`,
)
},
onMessage(evt, ws) {
console.log(`Received message: ${evt.data}`)

27
apps/csms/src/lib/auth.ts Normal file
View File

@@ -0,0 +1,27 @@
import { betterAuth } from 'better-auth'
import { drizzleAdapter } from 'better-auth/adapters/drizzle'
import { useDrizzle } from './db.js'
import * as schema from '@/db/schema.ts'
import { bearer, jwt, username } from 'better-auth/plugins'
export const auth = betterAuth({
database: drizzleAdapter(useDrizzle(), {
provider: 'pg',
schema: {
...schema,
},
}),
user: {
additionalFields: {
role: {
type: 'string',
defaultValue: 'user',
input: false,
},
},
},
emailAndPassword: {
enabled: true,
},
plugins: [username(), bearer(), jwt()],
})

24
apps/csms/src/lib/db.ts Normal file
View File

@@ -0,0 +1,24 @@
import { drizzle } from 'drizzle-orm/node-postgres'
import { Pool } from 'pg'
import * as schema from '@/db/schema.js'
let pgPoolInstance: Pool | null = null
let drizzleInstance: ReturnType<typeof drizzle> | null = null
export const useDrizzle = () => {
if (!pgPoolInstance || !drizzleInstance) {
pgPoolInstance = new Pool({
connectionString: process.env.DATABASE_CONNECTION_STRING,
})
drizzleInstance = drizzle({ client: pgPoolInstance, schema })
}
return drizzleInstance
}
export const closeDrizzle = () => {
if (pgPoolInstance) {
pgPoolInstance.end()
pgPoolInstance = null
drizzleInstance = null
}
}

View File