feat(csms): add deployment script and database schema for user authentication
This commit is contained in:
16
apps/csms/deploy/Dockerfile
Normal file
16
apps/csms/deploy/Dockerfile
Normal file
@@ -0,0 +1,16 @@
|
||||
FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制应用文件
|
||||
COPY package.json .
|
||||
COPY index.js .
|
||||
|
||||
# 安装依赖
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3001
|
||||
|
||||
# 启动应用
|
||||
CMD ["node", "index.js"]
|
||||
12
apps/csms/deploy/docker-compose.yml
Normal file
12
apps/csms/deploy/docker-compose.yml
Normal file
@@ -0,0 +1,12 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
csms:
|
||||
build: .
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
# env_file:
|
||||
# - .env
|
||||
restart: unless-stopped
|
||||
1
apps/csms/deploy/index.js
Normal file
1
apps/csms/deploy/index.js
Normal file
@@ -0,0 +1 @@
|
||||
import{serve as c}from"@hono/node-server";import{Hono as a}from"hono";import{createNodeWebSocket as p}from"@hono/node-ws";import{getConnInfo as i}from"@hono/node-server/conninfo";import{cors as m}from"hono/cors";import{logger as l}from"hono/logger";import{showRoutes as g}from"hono/dev";import{readFileSync as d}from"fs";import{dirname as f,join as u}from"path";import{fileURLToPath as h}from"url";var S=f(h(import.meta.url)),s="1.0.0";try{let o=u(S,"../../package.json");s=JSON.parse(d(o,"utf-8")).version||"1.0.0"}catch{}var r=new a,{injectWebSocket:k,upgradeWebSocket:v}=p({app:r});r.use(l());r.use("/ocpp",m({origin:"*",allowMethods:["GET","POST","OPTIONS"],allowHeaders:["Content-Type","Authorization"],exposeHeaders:["Content-Length"],credentials:!0}));r.get("/",o=>o.json({platform:"Helios CSMS",version:s,message:"ok"}));r.get("/ocpp",v(o=>({onOpen(e,t){let n=i(o);console.log(`New connection from ${n.remote.address}:${n.remote.port}`)},onMessage(e,t){console.log(`Received message: ${e.data}`),t.send(`Echo: ${e.data}`)},onClose(e,t){console.log("Connection closed: ",e.code,e.reason)}})));g(r,{verbose:!0});var w=c({fetch:r.fetch,port:3001},o=>{console.log(`Server is running on http://localhost:${o.port}`)});k(w);
|
||||
13
apps/csms/deploy/package.json
Normal file
13
apps/csms/deploy/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "csms",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.6",
|
||||
"@hono/node-ws": "^1.2.0",
|
||||
"better-auth": "^1.3.34",
|
||||
"dotenv": "^17.2.3",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"hono": "^4.10.6",
|
||||
"pg": "^8.16.3"
|
||||
}
|
||||
}
|
||||
10
apps/csms/drizzle.config.ts
Normal file
10
apps/csms/drizzle.config.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { defineConfig } from 'drizzle-kit'
|
||||
|
||||
export default defineConfig({
|
||||
out: './drizzle',
|
||||
schema: './src/db/schema.ts',
|
||||
dialect: 'postgresql',
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_CONNECTION_STRING!,
|
||||
},
|
||||
})
|
||||
51
apps/csms/drizzle/0000_real_garia.sql
Normal file
51
apps/csms/drizzle/0000_real_garia.sql
Normal file
@@ -0,0 +1,51 @@
|
||||
CREATE TABLE "account" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"account_id" text NOT NULL,
|
||||
"provider_id" text NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"access_token" text,
|
||||
"refresh_token" text,
|
||||
"id_token" text,
|
||||
"access_token_expires_at" timestamp,
|
||||
"refresh_token_expires_at" timestamp,
|
||||
"scope" text,
|
||||
"password" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "session" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
"token" text NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp NOT NULL,
|
||||
"ip_address" text,
|
||||
"user_agent" text,
|
||||
"user_id" text NOT NULL,
|
||||
CONSTRAINT "session_token_unique" UNIQUE("token")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "user" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"email_verified" boolean DEFAULT false NOT NULL,
|
||||
"image" text,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL,
|
||||
"role" text DEFAULT 'user',
|
||||
CONSTRAINT "user_email_unique" UNIQUE("email")
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE "verification" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"identifier" text NOT NULL,
|
||||
"value" text NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
"created_at" timestamp DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;
|
||||
3
apps/csms/drizzle/0001_gorgeous_invisible_woman.sql
Normal file
3
apps/csms/drizzle/0001_gorgeous_invisible_woman.sql
Normal file
@@ -0,0 +1,3 @@
|
||||
ALTER TABLE "user" ADD COLUMN "username" text;--> statement-breakpoint
|
||||
ALTER TABLE "user" ADD COLUMN "display_username" text;--> statement-breakpoint
|
||||
ALTER TABLE "user" ADD CONSTRAINT "user_username_unique" UNIQUE("username");
|
||||
333
apps/csms/drizzle/meta/0000_snapshot.json
Normal file
333
apps/csms/drizzle/meta/0000_snapshot.json
Normal file
@@ -0,0 +1,333 @@
|
||||
{
|
||||
"id": "a406c0d9-3511-445d-a09c-aa5a409730d7",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'user'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
352
apps/csms/drizzle/meta/0001_snapshot.json
Normal file
352
apps/csms/drizzle/meta/0001_snapshot.json
Normal file
@@ -0,0 +1,352 @@
|
||||
{
|
||||
"id": "0f080c45-a0f2-44c8-ad7b-9587335710d8",
|
||||
"prevId": "a406c0d9-3511-445d-a09c-aa5a409730d7",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"tables": {
|
||||
"public.account": {
|
||||
"name": "account",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"account_id": {
|
||||
"name": "account_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"provider_id": {
|
||||
"name": "provider_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"access_token": {
|
||||
"name": "access_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token": {
|
||||
"name": "refresh_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"id_token": {
|
||||
"name": "id_token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"access_token_expires_at": {
|
||||
"name": "access_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"refresh_token_expires_at": {
|
||||
"name": "refresh_token_expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"scope": {
|
||||
"name": "scope",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"password": {
|
||||
"name": "password",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"account_user_id_user_id_fk": {
|
||||
"name": "account_user_id_user_id_fk",
|
||||
"tableFrom": "account",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.session": {
|
||||
"name": "session",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"token": {
|
||||
"name": "token",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"ip_address": {
|
||||
"name": "ip_address",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_agent": {
|
||||
"name": "user_agent",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"user_id": {
|
||||
"name": "user_id",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {
|
||||
"session_user_id_user_id_fk": {
|
||||
"name": "session_user_id_user_id_fk",
|
||||
"tableFrom": "session",
|
||||
"tableTo": "user",
|
||||
"columnsFrom": [
|
||||
"user_id"
|
||||
],
|
||||
"columnsTo": [
|
||||
"id"
|
||||
],
|
||||
"onDelete": "cascade",
|
||||
"onUpdate": "no action"
|
||||
}
|
||||
},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"session_token_unique": {
|
||||
"name": "session_token_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"token"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.user": {
|
||||
"name": "user",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"name": {
|
||||
"name": "name",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email": {
|
||||
"name": "email",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"email_verified": {
|
||||
"name": "email_verified",
|
||||
"type": "boolean",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": false
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"username": {
|
||||
"name": "username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"display_username": {
|
||||
"name": "display_username",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": false,
|
||||
"default": "'user'"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {
|
||||
"user_email_unique": {
|
||||
"name": "user_email_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"email"
|
||||
]
|
||||
},
|
||||
"user_username_unique": {
|
||||
"name": "user_username_unique",
|
||||
"nullsNotDistinct": false,
|
||||
"columns": [
|
||||
"username"
|
||||
]
|
||||
}
|
||||
},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
},
|
||||
"public.verification": {
|
||||
"name": "verification",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"identifier": {
|
||||
"name": "identifier",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"value": {
|
||||
"name": "value",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"expires_at": {
|
||||
"name": "expires_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"created_at": {
|
||||
"name": "created_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
},
|
||||
"updated_at": {
|
||||
"name": "updated_at",
|
||||
"type": "timestamp",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "now()"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {},
|
||||
"uniqueConstraints": {},
|
||||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"isRLSEnabled": false
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"sequences": {},
|
||||
"roles": {},
|
||||
"policies": {},
|
||||
"views": {},
|
||||
"_meta": {
|
||||
"columns": {},
|
||||
"schemas": {},
|
||||
"tables": {}
|
||||
}
|
||||
}
|
||||
20
apps/csms/drizzle/meta/_journal.json
Normal file
20
apps/csms/drizzle/meta/_journal.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1763319356169,
|
||||
"tag": "0000_real_garia",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "7",
|
||||
"when": 1763319683557,
|
||||
"tag": "0001_gorgeous_invisible_woman",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
53
apps/csms/esbuild.config.js
Normal file
53
apps/csms/esbuild.config.js
Normal file
@@ -0,0 +1,53 @@
|
||||
import esbuild from 'esbuild'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
|
||||
const isProduction = process.env.NODE_ENV === 'production'
|
||||
|
||||
// 读取 package.json 中的所有依赖
|
||||
const packageJson = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
|
||||
const allDeps = {
|
||||
...packageJson.dependencies,
|
||||
...packageJson.devDependencies,
|
||||
}
|
||||
const externalModules = Object.keys(allDeps)
|
||||
|
||||
const config = {
|
||||
entryPoints: ['src/index.ts'],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'esnext',
|
||||
format: 'esm',
|
||||
outfile: 'dist/index.js',
|
||||
external: externalModules,
|
||||
sourcemap: !isProduction,
|
||||
minify: isProduction,
|
||||
define: {
|
||||
'process.env.NODE_ENV': `"${process.env.NODE_ENV || 'development'}"`,
|
||||
},
|
||||
}
|
||||
|
||||
async function build() {
|
||||
try {
|
||||
console.log('Building with esbuild...')
|
||||
await esbuild.build(config)
|
||||
|
||||
// 生成 package.json
|
||||
const packageJson = { type: 'module' }
|
||||
fs.mkdirSync(path.dirname('dist/package.json'), { recursive: true })
|
||||
fs.writeFileSync('dist/package.json', JSON.stringify(packageJson, null, 2))
|
||||
|
||||
console.log('✓ Build complete!')
|
||||
console.log(` Entry: ${config.entryPoints[0]}`)
|
||||
console.log(` Output: ${config.outfile}`)
|
||||
console.log(` Mode: ${isProduction ? 'production' : 'development'}`)
|
||||
console.log(
|
||||
` Size: ${(fs.statSync(config.outfile).size / 1024).toFixed(1)}KB`,
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('✗ Build failed:', error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
build()
|
||||
1174
apps/csms/package-lock.json
generated
Normal file
1174
apps/csms/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -3,17 +3,32 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "tsx watch src/index.ts",
|
||||
"build": "tsc",
|
||||
"start": "node dist/index.js"
|
||||
"build": "node esbuild.config.js",
|
||||
"build:prod": "NODE_ENV=production node esbuild.config.js",
|
||||
"start": "node dist/index.js",
|
||||
"deploy": "node scripts/deploy.js",
|
||||
"deploy:docker": "node scripts/deploy.js --docker",
|
||||
"db:gen:auth": "npx @better-auth/cli generate --output src/db/auth-schema.ts",
|
||||
"db:gen": "drizzle-kit generate",
|
||||
"db:migrate": "drizzle-kit migrate",
|
||||
"db:push": "drizzle-kit push"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hono/node-server": "^1.19.6",
|
||||
"@hono/node-ws": "^1.2.0",
|
||||
"hono": "^4.10.6"
|
||||
"better-auth": "^1.3.34",
|
||||
"dotenv": "^17.2.3",
|
||||
"drizzle-orm": "^0.44.7",
|
||||
"hono": "^4.10.6",
|
||||
"pg": "^8.16.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@better-auth/cli": "^1.3.34",
|
||||
"@types/node": "^20.11.17",
|
||||
"tsx": "^4.7.1",
|
||||
"@types/pg": "^8.15.6",
|
||||
"drizzle-kit": "^0.31.7",
|
||||
"esbuild": "^0.27.0",
|
||||
"tsx": "^4.20.6",
|
||||
"typescript": "^5.8.3"
|
||||
}
|
||||
}
|
||||
153
apps/csms/scripts/deploy.js
Executable file
153
apps/csms/scripts/deploy.js
Executable file
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 部署助手脚本
|
||||
*
|
||||
* 使用:
|
||||
* node scripts/deploy.js # 标准部署(生产构建 + 依赖)
|
||||
* node scripts/deploy.js --docker # Docker 镜像
|
||||
* node scripts/deploy.js --help # 显示帮助
|
||||
*/
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { execSync } from 'child_process'
|
||||
|
||||
const args = process.argv.slice(2)
|
||||
const help = args.includes('--help') || args.includes('-h')
|
||||
const isDocker = args.includes('--docker')
|
||||
const outDir = args.includes('--out') ? args[args.indexOf('--out') + 1] : 'dist'
|
||||
|
||||
if (help) {
|
||||
console.log(`
|
||||
Usage: node scripts/deploy.js [options]
|
||||
|
||||
Options:
|
||||
--docker 生成 Dockerfile
|
||||
--out PATH 输出目录(默认: dist)
|
||||
--help 显示此帮助信息
|
||||
|
||||
Examples:
|
||||
node scripts/deploy.js 生产构建
|
||||
node scripts/deploy.js --out build 输出到 build 目录
|
||||
node scripts/deploy.js --docker 生成 Docker 配置
|
||||
`)
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
console.log('🚀 开始部署流程...\n')
|
||||
|
||||
try {
|
||||
// 1. 生产构建
|
||||
console.log('📦 构建应用...')
|
||||
execSync('npm run build:prod', { stdio: 'inherit', cwd: process.cwd() })
|
||||
|
||||
// 2. 创建部署目录
|
||||
console.log('\n📁 准备部署目录...')
|
||||
const deployDir = path.join(process.cwd(), 'deploy')
|
||||
|
||||
if (fs.existsSync(deployDir)) {
|
||||
fs.rmSync(deployDir, { recursive: true })
|
||||
}
|
||||
fs.mkdirSync(deployDir, { recursive: true })
|
||||
|
||||
// 3. 复制必要文件
|
||||
console.log('📋 复制文件...')
|
||||
|
||||
// 复制构建输出
|
||||
fs.copyFileSync(
|
||||
path.join(process.cwd(), 'dist/index.js'),
|
||||
path.join(deployDir, 'index.js'),
|
||||
)
|
||||
fs.copyFileSync(
|
||||
path.join(process.cwd(), 'dist/package.json'),
|
||||
path.join(deployDir, 'package.json'),
|
||||
)
|
||||
|
||||
// 复制 package.json(用于 npm install)
|
||||
const srcPkg = JSON.parse(fs.readFileSync('package.json', 'utf-8'))
|
||||
const deployPkg = {
|
||||
name: srcPkg.name,
|
||||
version: srcPkg.version,
|
||||
type: 'module',
|
||||
dependencies: srcPkg.dependencies,
|
||||
}
|
||||
fs.writeFileSync(
|
||||
path.join(deployDir, 'package.json'),
|
||||
JSON.stringify(deployPkg, null, 2),
|
||||
)
|
||||
|
||||
// 复制 .env 模板
|
||||
if (fs.existsSync('.env.example')) {
|
||||
fs.copyFileSync('.env.example', path.join(deployDir, '.env.example'))
|
||||
}
|
||||
|
||||
console.log(`✅ 部署文件已生成到 ${path.relative(process.cwd(), deployDir)}/`)
|
||||
|
||||
// 4. 可选:生成 Docker 配置
|
||||
if (isDocker) {
|
||||
console.log('\n🐳 生成 Docker 配置...')
|
||||
|
||||
const dockerfile = `FROM node:22-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制应用文件
|
||||
COPY package.json .
|
||||
COPY index.js .
|
||||
|
||||
# 安装依赖
|
||||
RUN npm ci --omit=dev
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 3001
|
||||
|
||||
# 启动应用
|
||||
CMD ["node", "index.js"]
|
||||
`
|
||||
|
||||
const dockerCompose = `version: '3.8'
|
||||
|
||||
services:
|
||||
csms:
|
||||
build: .
|
||||
ports:
|
||||
- "3001:3001"
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
# env_file:
|
||||
# - .env
|
||||
restart: unless-stopped
|
||||
`
|
||||
|
||||
fs.writeFileSync(path.join(deployDir, 'Dockerfile'), dockerfile)
|
||||
fs.writeFileSync(path.join(deployDir, 'docker-compose.yml'), dockerCompose)
|
||||
|
||||
console.log('✅ Docker 文件已生成')
|
||||
}
|
||||
|
||||
// 5. 显示部署信息
|
||||
console.log('\n📊 部署信息:')
|
||||
console.log(` 名称: ${deployPkg.name}`)
|
||||
console.log(` 版本: ${deployPkg.version}`)
|
||||
console.log(` 主文件: index.js`)
|
||||
console.log(` 依赖数: ${Object.keys(deployPkg.dependencies).length}`)
|
||||
|
||||
const indexSize = fs.statSync(path.join(deployDir, 'index.js')).size
|
||||
console.log(` 代码大小: ${(indexSize / 1024).toFixed(1)}KB`)
|
||||
|
||||
console.log('\n✨ 部署准备完成!\n')
|
||||
console.log('下一步:')
|
||||
console.log(` 1. cd ${path.relative(process.cwd(), deployDir)}`)
|
||||
console.log(` 2. npm install --omit=dev`)
|
||||
console.log(` 3. node index.js`)
|
||||
|
||||
if (isDocker) {
|
||||
console.log('\n或使用 Docker:')
|
||||
console.log(` 1. cd ${path.relative(process.cwd(), deployDir)}`)
|
||||
console.log(` 2. docker compose up`)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('\n❌ 部署失败:', error.message)
|
||||
process.exit(1)
|
||||
}
|
||||
71
apps/csms/src/db/auth-schema.ts
Normal file
71
apps/csms/src/db/auth-schema.ts
Normal 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(),
|
||||
});
|
||||
1
apps/csms/src/db/schema.ts
Normal file
1
apps/csms/src/db/schema.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from './auth-schema.ts'
|
||||
@@ -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({
|
||||
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
27
apps/csms/src/lib/auth.ts
Normal 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
24
apps/csms/src/lib/db.ts
Normal 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
|
||||
}
|
||||
}
|
||||
0
apps/csms/src/types/db.ts
Normal file
0
apps/csms/src/types/db.ts
Normal file
@@ -5,12 +5,19 @@
|
||||
"strict": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"skipLibCheck": true,
|
||||
"resolveJsonModule": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"noEmit": true,
|
||||
"types": [
|
||||
"node"
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx",
|
||||
"outDir": "./dist"
|
||||
"outDir": "./dist",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "drizzle.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user