Compare commits
No commits in common. "21308fa5bd242c1c77c13ebfced27869e95ac15b" and "8cb31194d76c07f7192d19147120b8490ee1b6e4" have entirely different histories.
21308fa5bd
...
8cb31194d7
35 changed files with 329 additions and 1133 deletions
17
.env.example
17
.env.example
|
|
@ -1,11 +1,8 @@
|
||||||
PORT=3000
|
DB_USER=postgres
|
||||||
SERVICE_NAME=<dash-seperated-lowercased-unique-accross-projects-name>
|
DB_PASSWORD=
|
||||||
DATABASE_URL="postgresql://..."
|
DB_NAME=postgres
|
||||||
MINIO_ACCESS_KEY=
|
DB_HOST=localhost
|
||||||
MINIO_SECRET_KEY=
|
DB_PORT=5432
|
||||||
MINIO_ENDPOINT_URL=
|
|
||||||
MINIO_BUCKET_NAME=
|
|
||||||
BETTER_AUTH_SECRET=
|
|
||||||
|
|
||||||
# DO NOT CHANGE
|
|
||||||
BETTER_AUTH_URL=http://127.0.0.1:${PORT}
|
DATABASE_URL='postgres://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}'
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
DB_USER=
|
|
||||||
DB_PASSWORD=
|
|
||||||
DB_PORT=
|
|
||||||
|
|
||||||
MINIO_ROOT_USER=
|
|
||||||
MINIO_ROOT_PASSWORD=
|
|
||||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -29,7 +29,6 @@ yarn-error.log*
|
||||||
.env.local
|
.env.local
|
||||||
.env.development.local
|
.env.development.local
|
||||||
.env.test.local
|
.env.test.local
|
||||||
.env.services
|
|
||||||
.env.production.local
|
.env.production.local
|
||||||
|
|
||||||
# vercel
|
# vercel
|
||||||
|
|
@ -41,6 +40,4 @@ yarn-error.log*
|
||||||
**/*.tgz
|
**/*.tgz
|
||||||
**/*.log
|
**/*.log
|
||||||
package-lock.json
|
package-lock.json
|
||||||
**/*.bun
|
**/*.bun
|
||||||
|
|
||||||
./server
|
|
||||||
|
|
@ -13,8 +13,8 @@ RUN bun install --frozen-lockfile
|
||||||
# Copy source code
|
# Copy source code
|
||||||
COPY . .
|
COPY . .
|
||||||
|
|
||||||
# RUN DB Migrations and build
|
# Build the application
|
||||||
RUN bun run db:migrateb && bun run build
|
RUN bun build ./src/index.ts --compile --outfile server
|
||||||
|
|
||||||
# Production stage
|
# Production stage
|
||||||
FROM debian:bookworm-slim
|
FROM debian:bookworm-slim
|
||||||
|
|
@ -28,4 +28,4 @@ COPY --from=builder /app/server .
|
||||||
EXPOSE 3000
|
EXPOSE 3000
|
||||||
|
|
||||||
# Run the binary
|
# Run the binary
|
||||||
CMD ["./server"]
|
CMD ["./server"]
|
||||||
51
auth-schema.ts
Normal file
51
auth-schema.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
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").notNull(),
|
||||||
|
image: text("image"),
|
||||||
|
createdAt: timestamp("created_at").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const session = pgTable("session", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
expiresAt: timestamp("expires_at").notNull(),
|
||||||
|
token: text("token").notNull().unique(),
|
||||||
|
createdAt: timestamp("created_at").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull(),
|
||||||
|
ipAddress: text("ip_address"),
|
||||||
|
userAgent: text("user_agent"),
|
||||||
|
userId: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
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),
|
||||||
|
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").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").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"),
|
||||||
|
updatedAt: timestamp("updated_at"),
|
||||||
|
});
|
||||||
BIN
bun.lockb
BIN
bun.lockb
Binary file not shown.
|
|
@ -1,44 +0,0 @@
|
||||||
services:
|
|
||||||
tracing:
|
|
||||||
image: jaegertracing/all-in-one:latest
|
|
||||||
environment:
|
|
||||||
COLLECTOR_ZIPKIN_HOST_PORT: 9411
|
|
||||||
COLLECTOR_OTLP_ENABLED: true
|
|
||||||
ports:
|
|
||||||
- "16686:16686"
|
|
||||||
|
|
||||||
db:
|
|
||||||
image: postgres:latest
|
|
||||||
environment:
|
|
||||||
POSTGRES_USER: ${DB_USER}
|
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
|
||||||
POSTGRES_DB: ${DB_NAME}
|
|
||||||
ports:
|
|
||||||
- "${DB_PORT}:5432"
|
|
||||||
volumes:
|
|
||||||
- postgres_data:/var/lib/postgresql/data
|
|
||||||
|
|
||||||
minio:
|
|
||||||
image: minio/minio:latest
|
|
||||||
ports:
|
|
||||||
- "9000:9000"
|
|
||||||
- "9001:9001"
|
|
||||||
environment:
|
|
||||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
|
||||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
|
||||||
command: server /data --console-address ":9001"
|
|
||||||
volumes:
|
|
||||||
- minio_data:/data
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "mc", "ready", "local"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 20s
|
|
||||||
retries: 3
|
|
||||||
|
|
||||||
networks:
|
|
||||||
api-network:
|
|
||||||
driver: bridge
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
postgres_data:
|
|
||||||
minio_data:
|
|
||||||
|
|
@ -4,10 +4,47 @@ services:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
ports:
|
ports:
|
||||||
- "${PORT}:${PORT}"
|
- "3000:3000"
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
environment:
|
environment:
|
||||||
NODE_ENV: production
|
- NODE_ENV=production
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT: http://tracing:4318
|
- OTEL_EXPORTER_OTLP_ENDPOINT=http://tracing:4318
|
||||||
OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
|
- OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
||||||
DATABASE_URL: ${DATABASE_URL}
|
- DATABASE_URL=postgresql://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}
|
||||||
|
networks:
|
||||||
|
- api-network
|
||||||
|
depends_on:
|
||||||
|
- tracing
|
||||||
|
|
||||||
|
tracing:
|
||||||
|
image: jaegertracing/all-in-one:latest
|
||||||
|
environment:
|
||||||
|
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
|
||||||
|
- COLLECTOR_OTLP_ENABLED=true
|
||||||
|
ports:
|
||||||
|
- "16686:16686"
|
||||||
|
networks:
|
||||||
|
- api-network
|
||||||
|
|
||||||
|
db:
|
||||||
|
image: postgres:latest
|
||||||
|
env_file:
|
||||||
|
- .env
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: postgres
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
POSTGRES_DB: postgres
|
||||||
|
ports:
|
||||||
|
- "5432:5432"
|
||||||
|
volumes:
|
||||||
|
- postgres_data:/var/lib/postgresql/data
|
||||||
|
networks:
|
||||||
|
- api-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
api-network:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
out: "./drizzle",
|
out: "./drizzle",
|
||||||
schema: "./src/db/schema",
|
schema: "./src/db/schema.ts",
|
||||||
dialect: "postgresql",
|
dialect: "postgresql",
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
url: process.env.DATABASE_URL!,
|
url: process.env.DATABASE_URL!,
|
||||||
|
|
|
||||||
|
|
@ -16,13 +16,6 @@ CREATE TABLE "auth"."account" (
|
||||||
"updated_at" timestamp NOT NULL
|
"updated_at" timestamp NOT NULL
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE "auth"."rate_limit" (
|
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
|
||||||
"key" text,
|
|
||||||
"count" integer,
|
|
||||||
"last_request" integer
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
CREATE TABLE "auth"."session" (
|
CREATE TABLE "auth"."session" (
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
"expires_at" timestamp NOT NULL,
|
"expires_at" timestamp NOT NULL,
|
||||||
|
|
@ -55,16 +48,5 @@ CREATE TABLE "auth"."verification" (
|
||||||
"updated_at" timestamp
|
"updated_at" timestamp
|
||||||
);
|
);
|
||||||
--> statement-breakpoint
|
--> statement-breakpoint
|
||||||
CREATE TABLE "note" (
|
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
|
||||||
"title" text,
|
|
||||||
"content" text,
|
|
||||||
"createdAt" timestamp DEFAULT now() NOT NULL,
|
|
||||||
"updatedAt" timestamp,
|
|
||||||
"deletedAt" timestamp,
|
|
||||||
"ownerId" text NOT NULL
|
|
||||||
);
|
|
||||||
--> statement-breakpoint
|
|
||||||
ALTER TABLE "auth"."account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
ALTER TABLE "auth"."account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
||||||
ALTER TABLE "auth"."session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
ALTER TABLE "auth"."session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "auth"."user"("id") ON DELETE no action ON UPDATE no action;
|
||||||
ALTER TABLE "note" ADD CONSTRAINT "note_ownerId_user_id_fk" FOREIGN KEY ("ownerId") REFERENCES "auth"."user"("id") ON DELETE no action ON UPDATE no action;
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
{
|
{
|
||||||
"id": "50bd2c27-8d45-478f-a894-b2f7cd4a718b",
|
"id": "1ada386b-032f-4c0c-a867-e25f2e921167",
|
||||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"dialect": "postgresql",
|
"dialect": "postgresql",
|
||||||
|
|
@ -110,43 +110,6 @@
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
},
|
||||||
"auth.rate_limit": {
|
|
||||||
"name": "rate_limit",
|
|
||||||
"schema": "auth",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"key": {
|
|
||||||
"name": "key",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"count": {
|
|
||||||
"name": "count",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"last_request": {
|
|
||||||
"name": "last_request",
|
|
||||||
"type": "integer",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
},
|
|
||||||
"auth.session": {
|
"auth.session": {
|
||||||
"name": "session",
|
"name": "session",
|
||||||
"schema": "auth",
|
"schema": "auth",
|
||||||
|
|
@ -342,77 +305,6 @@
|
||||||
"policies": {},
|
"policies": {},
|
||||||
"checkConstraints": {},
|
"checkConstraints": {},
|
||||||
"isRLSEnabled": false
|
"isRLSEnabled": false
|
||||||
},
|
|
||||||
"public.note": {
|
|
||||||
"name": "note",
|
|
||||||
"schema": "",
|
|
||||||
"columns": {
|
|
||||||
"id": {
|
|
||||||
"name": "id",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": true,
|
|
||||||
"notNull": true
|
|
||||||
},
|
|
||||||
"title": {
|
|
||||||
"name": "title",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"content": {
|
|
||||||
"name": "content",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"createdAt": {
|
|
||||||
"name": "createdAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true,
|
|
||||||
"default": "now()"
|
|
||||||
},
|
|
||||||
"updatedAt": {
|
|
||||||
"name": "updatedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"deletedAt": {
|
|
||||||
"name": "deletedAt",
|
|
||||||
"type": "timestamp",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": false
|
|
||||||
},
|
|
||||||
"ownerId": {
|
|
||||||
"name": "ownerId",
|
|
||||||
"type": "text",
|
|
||||||
"primaryKey": false,
|
|
||||||
"notNull": true
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"indexes": {},
|
|
||||||
"foreignKeys": {
|
|
||||||
"note_ownerId_user_id_fk": {
|
|
||||||
"name": "note_ownerId_user_id_fk",
|
|
||||||
"tableFrom": "note",
|
|
||||||
"tableTo": "user",
|
|
||||||
"schemaTo": "auth",
|
|
||||||
"columnsFrom": [
|
|
||||||
"ownerId"
|
|
||||||
],
|
|
||||||
"columnsTo": [
|
|
||||||
"id"
|
|
||||||
],
|
|
||||||
"onDelete": "no action",
|
|
||||||
"onUpdate": "no action"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"compositePrimaryKeys": {},
|
|
||||||
"uniqueConstraints": {},
|
|
||||||
"policies": {},
|
|
||||||
"checkConstraints": {},
|
|
||||||
"isRLSEnabled": false
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enums": {},
|
"enums": {},
|
||||||
|
|
|
||||||
|
|
@ -5,8 +5,8 @@
|
||||||
{
|
{
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"version": "7",
|
"version": "7",
|
||||||
"when": 1737019435130,
|
"when": 1736605568502,
|
||||||
"tag": "0000_uneven_professor_monster",
|
"tag": "0000_rich_reavers",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -5,13 +5,9 @@
|
||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"dev": "bun run --watch src/index.ts",
|
"dev": "bun run --watch src/index.ts",
|
||||||
"email": "email dev --dir src/emails",
|
"email": "email dev --dir src/emails",
|
||||||
"auth:generate": "bun x @better-auth/cli generate --config src/lib/auth/auth.ts --output src/db/schema/auth.ts && drizzle-kit migrate",
|
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"db:check": "drizzle-kit check",
|
|
||||||
"db:generate": "drizzle-kit generate",
|
"db:generate": "drizzle-kit generate",
|
||||||
"db:migrate": "drizzle-kit migrate",
|
"db:migrate": "drizzle-kit migrate",
|
||||||
"db:pull": "drizzle-kit pull",
|
|
||||||
"db:push": "drizzle-kit push",
|
|
||||||
"build": "bun build --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts"
|
"build": "bun build --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|
@ -26,7 +22,6 @@
|
||||||
"drizzle-orm": "^0.38.3",
|
"drizzle-orm": "^0.38.3",
|
||||||
"drizzle-typebox": "^0.2.1",
|
"drizzle-typebox": "^0.2.1",
|
||||||
"elysia": "latest",
|
"elysia": "latest",
|
||||||
"minio": "^8.0.3",
|
|
||||||
"nodemailer": "^6.9.16",
|
"nodemailer": "^6.9.16",
|
||||||
"pg": "^8.13.1",
|
"pg": "^8.13.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
import { Elysia, t } from "elysia";
|
|
||||||
|
|
||||||
import { betterAuthView } from "../lib/auth/auth-view";
|
|
||||||
import { getAuthConfig, getBaseConfig } from "../lib/utils/env";
|
|
||||||
import { router } from "./routes";
|
|
||||||
|
|
||||||
const baseConfig = getBaseConfig();
|
|
||||||
const authConfig = getAuthConfig();
|
|
||||||
|
|
||||||
export const api = new Elysia({
|
|
||||||
name: baseConfig.SERVICE_NAME,
|
|
||||||
prefix: "/api",
|
|
||||||
detail: {
|
|
||||||
summary: `Get status`,
|
|
||||||
description: `Get status for ${baseConfig.SERVICE_NAME}`,
|
|
||||||
externalDocs: {
|
|
||||||
description: "Auth API",
|
|
||||||
url: `${authConfig.BETTER_AUTH_URL}/docs`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.all("/auth/*", betterAuthView)
|
|
||||||
.use(router)
|
|
||||||
.get(
|
|
||||||
"",
|
|
||||||
() => {
|
|
||||||
return {
|
|
||||||
message: `Server is running`,
|
|
||||||
success: true,
|
|
||||||
name: baseConfig.SERVICE_NAME,
|
|
||||||
status: "active",
|
|
||||||
docs: {
|
|
||||||
default: "/api/docs",
|
|
||||||
auth: "/api/auth/docs",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
{
|
|
||||||
response: {
|
|
||||||
200: t.Object(
|
|
||||||
{
|
|
||||||
message: t.String({ default: `Server is running` }),
|
|
||||||
success: t.Boolean({ default: true }),
|
|
||||||
name: t.String({ default: baseConfig.SERVICE_NAME }),
|
|
||||||
status: t.String({ default: `active` }),
|
|
||||||
docs: t.Object({
|
|
||||||
default: t.String({ default: "/api/docs" }),
|
|
||||||
auth: t.String({ default: "/api/auth/docs" }),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Success",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
404: t.Object(
|
|
||||||
{
|
|
||||||
message: t.String({ default: `Not found` }),
|
|
||||||
success: t.Boolean({ default: false }),
|
|
||||||
name: t.String({ default: baseConfig.SERVICE_NAME }),
|
|
||||||
status: t.String({ default: `active` }),
|
|
||||||
docs: t.Object({
|
|
||||||
default: t.String({ default: "/api/docs" }),
|
|
||||||
auth: t.String({ default: "/api/auth/docs" }),
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Not found",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
25
src/api/note/note.controller.ts
Normal file
25
src/api/note/note.controller.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import { Memo } from "./note.model";
|
||||||
|
|
||||||
|
export class Note {
|
||||||
|
constructor(
|
||||||
|
public data: Memo[] = [
|
||||||
|
{
|
||||||
|
data: "Moonhalo",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
) {}
|
||||||
|
|
||||||
|
add(note: Memo) {
|
||||||
|
this.data.push(note);
|
||||||
|
|
||||||
|
return this.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(index: number) {
|
||||||
|
return this.data.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(index: number, note: Partial<Memo>) {
|
||||||
|
return (this.data[index] = { ...this.data[index], ...note });
|
||||||
|
}
|
||||||
|
}
|
||||||
7
src/api/note/note.model.ts
Normal file
7
src/api/note/note.model.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { t } from "elysia";
|
||||||
|
|
||||||
|
export const memoSchema = t.Object({
|
||||||
|
data: t.String(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Memo = typeof memoSchema.static;
|
||||||
44
src/api/note/note.route.ts
Normal file
44
src/api/note/note.route.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
import { Elysia, t } from "elysia";
|
||||||
|
import { Note } from "./note.controller";
|
||||||
|
import { memoSchema } from "./note.model";
|
||||||
|
|
||||||
|
export const note = new Elysia({ prefix: "/note" })
|
||||||
|
.decorate("note", new Note())
|
||||||
|
.model({
|
||||||
|
memo: t.Omit(memoSchema, ["author"]),
|
||||||
|
})
|
||||||
|
.get("/", ({ note }) => note.data)
|
||||||
|
.put("/", ({ note, body: { data } }) => note.add({ data }), {
|
||||||
|
body: "memo",
|
||||||
|
})
|
||||||
|
.get(
|
||||||
|
"/:index",
|
||||||
|
({ note, params: { index }, error }) => {
|
||||||
|
return note.data[index] ?? error(404, "Not Found :(");
|
||||||
|
},
|
||||||
|
{
|
||||||
|
params: t.Object({
|
||||||
|
index: t.Number(),
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.guard({
|
||||||
|
params: t.Object({
|
||||||
|
index: t.Number(),
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.delete("/:index", ({ note, params: { index }, error }) => {
|
||||||
|
if (index in note.data) return note.remove(index);
|
||||||
|
|
||||||
|
return error(422);
|
||||||
|
})
|
||||||
|
.patch(
|
||||||
|
"/:index",
|
||||||
|
({ note, params: { index }, body: { data }, error }) => {
|
||||||
|
if (index in note.data) return note.update(index, { data });
|
||||||
|
return error(422);
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: "memo",
|
||||||
|
}
|
||||||
|
);
|
||||||
36
src/api/otp/otp.route.ts
Normal file
36
src/api/otp/otp.route.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
import { Elysia, t } from "elysia";
|
||||||
|
import { renderToStaticMarkup } from "react-dom/server";
|
||||||
|
import nodemailer from "nodemailer";
|
||||||
|
import OTPEmail from "../../emails/otp";
|
||||||
|
import { createElement } from "react";
|
||||||
|
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: "smtp.gehenna.sh",
|
||||||
|
port: 465,
|
||||||
|
auth: {
|
||||||
|
user: "makoto",
|
||||||
|
pass: "12345678",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const otp = new Elysia({ prefix: "/otp" }).get(
|
||||||
|
"/",
|
||||||
|
async ({ body }) => {
|
||||||
|
// Random between 100,000 and 999,999
|
||||||
|
const otp = ~~(Math.random() * (900_000 - 1)) + 100_000;
|
||||||
|
|
||||||
|
const html = renderToStaticMarkup(createElement(OTPEmail, { otp }));
|
||||||
|
|
||||||
|
await transporter.sendMail({
|
||||||
|
from: "ibuki@gehenna.sh",
|
||||||
|
to: body,
|
||||||
|
subject: "Verify your email address",
|
||||||
|
html,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.String({ format: "email" }),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
|
|
||||||
import { Elysia, t } from "elysia";
|
|
||||||
import { noteRouter } from "./note/note.route";
|
|
||||||
|
|
||||||
|
|
||||||
export const router = new Elysia().use(noteRouter)
|
|
||||||
|
|
@ -1,141 +0,0 @@
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
|
||||||
import { db } from "../../../db";
|
|
||||||
import { note } from "../../../db/schema/note";
|
|
||||||
import { CreateNoteType } from "./note.model";
|
|
||||||
|
|
||||||
export class NoteController {
|
|
||||||
async createNote(new_note: CreateNoteType, ownerId: string) {
|
|
||||||
const new_note_data = { ...new_note, ownerId: ownerId };
|
|
||||||
const result = await db
|
|
||||||
.insert(note)
|
|
||||||
.values(new_note_data)
|
|
||||||
.returning({
|
|
||||||
id: note.id,
|
|
||||||
title: note.title,
|
|
||||||
content: note.content,
|
|
||||||
createdAt: note.createdAt,
|
|
||||||
updatedAt: note.updatedAt,
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: result,
|
|
||||||
message: "Note created successfully",
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getOwnerNotes(ownerId: string, limit:number=10, offset:number=0) {
|
|
||||||
const result = await db
|
|
||||||
.select({
|
|
||||||
id: note.id,
|
|
||||||
title: note.title,
|
|
||||||
content: note.content,
|
|
||||||
createdAt: note.createdAt,
|
|
||||||
updatedAt: note.updatedAt,
|
|
||||||
})
|
|
||||||
.from(note)
|
|
||||||
.where(and(eq(note.ownerId, ownerId), isNull(note.deletedAt)))
|
|
||||||
.limit(limit).offset(offset)
|
|
||||||
.execute();
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: result,
|
|
||||||
message: "",
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async getNoteById(noteId: string, ownerId: string) {
|
|
||||||
const result = await db
|
|
||||||
.select({
|
|
||||||
id: note.id,
|
|
||||||
title: note.title,
|
|
||||||
content: note.content,
|
|
||||||
createdAt: note.createdAt,
|
|
||||||
updatedAt: note.updatedAt,
|
|
||||||
})
|
|
||||||
.from(note)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(note.id, noteId),
|
|
||||||
eq(note.ownerId, ownerId),
|
|
||||||
isNull(note.deletedAt)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: result,
|
|
||||||
message: "",
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateNoteById(
|
|
||||||
noteId: string,
|
|
||||||
updated_note: CreateNoteType,
|
|
||||||
ownerId: string
|
|
||||||
) {
|
|
||||||
const new_note_data = { ...updated_note, updatedAt: new Date() };
|
|
||||||
const result = await db
|
|
||||||
.update(note)
|
|
||||||
.set(new_note_data)
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(note.id, noteId),
|
|
||||||
eq(note.ownerId, ownerId),
|
|
||||||
isNull(note.deletedAt)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.returning({
|
|
||||||
id: note.id,
|
|
||||||
title: note.title,
|
|
||||||
content: note.content,
|
|
||||||
createdAt: note.createdAt,
|
|
||||||
updatedAt: note.updatedAt,
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: result,
|
|
||||||
message: "Note updated successfully",
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteNoteById(noteId: string, ownerId: string) {
|
|
||||||
await db
|
|
||||||
.update(note)
|
|
||||||
.set({ deletedAt: new Date() })
|
|
||||||
.where(
|
|
||||||
and(
|
|
||||||
eq(note.id, noteId),
|
|
||||||
eq(note.ownerId, ownerId),
|
|
||||||
isNull(note.deletedAt)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.execute();
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: null,
|
|
||||||
message: "Note deleted successfully",
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async deleteAllNotes(ownerId: string) {
|
|
||||||
await db
|
|
||||||
.update(note)
|
|
||||||
.set({ deletedAt: new Date() })
|
|
||||||
.where(and(eq(note.ownerId, ownerId), isNull(note.deletedAt)))
|
|
||||||
.execute();
|
|
||||||
return {
|
|
||||||
success: true,
|
|
||||||
data: null,
|
|
||||||
message: "Notes deleted successfully",
|
|
||||||
error: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
import { createSelectSchema } from "drizzle-typebox";
|
|
||||||
import { t } from "elysia";
|
|
||||||
import { note } from "../../../db/schema/note";
|
|
||||||
import { InferInsertModel } from "drizzle-orm";
|
|
||||||
import { commonResponses } from "../../../lib/utils/common";
|
|
||||||
|
|
||||||
export const _NoteSchema = createSelectSchema(note);
|
|
||||||
|
|
||||||
export const NoteSchema = t.Omit(_NoteSchema, ["deletedAt", "ownerId"]);
|
|
||||||
|
|
||||||
export type CreateNoteType = Pick<
|
|
||||||
InferInsertModel<typeof note>,
|
|
||||||
"title" | "content"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export const createNoteSchema = t.Pick(NoteSchema, [
|
|
||||||
"title",
|
|
||||||
"content",
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const getNoteResponses = {
|
|
||||||
200: t.Object({
|
|
||||||
success:t.Boolean({default:true}),
|
|
||||||
data: t.Array(NoteSchema),
|
|
||||||
error: t.Null(),
|
|
||||||
message: t.String()
|
|
||||||
}, {
|
|
||||||
description:"Success"
|
|
||||||
}) ,
|
|
||||||
...commonResponses
|
|
||||||
}
|
|
||||||
|
|
||||||
export const deleteNoteResponses = {
|
|
||||||
200: t.Object({
|
|
||||||
success:t.Boolean({default:true}),
|
|
||||||
data: t.Null(),
|
|
||||||
error: t.Null(),
|
|
||||||
message: t.String({default:"Note deletion succesful"})
|
|
||||||
}, {
|
|
||||||
description:"Success"
|
|
||||||
}),
|
|
||||||
...commonResponses
|
|
||||||
}
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
import { Elysia, t } from "elysia";
|
|
||||||
import { createNoteSchema, deleteNoteResponses, getNoteResponses, NoteSchema } from "./note.model";
|
|
||||||
import { NoteController } from "./note.controller";
|
|
||||||
import { userMiddleware } from "../../../middlewares/auth-middleware";
|
|
||||||
|
|
||||||
export const noteRouter = new Elysia({
|
|
||||||
prefix: "/note",
|
|
||||||
name: "CRUD Operations for Notes",
|
|
||||||
"analytic":true,
|
|
||||||
tags: ["Note"],
|
|
||||||
detail: {
|
|
||||||
description: "Notes CRUD operations",
|
|
||||||
},
|
|
||||||
})
|
|
||||||
.decorate("note", new NoteController())
|
|
||||||
.model({
|
|
||||||
note: NoteSchema,
|
|
||||||
})
|
|
||||||
.derive(({ request }) => userMiddleware(request))
|
|
||||||
.onError(({ error, code, }) => {
|
|
||||||
console.error(error);
|
|
||||||
return {
|
|
||||||
message: "",
|
|
||||||
success: false,
|
|
||||||
data: null,
|
|
||||||
error: code.toString()
|
|
||||||
};
|
|
||||||
})
|
|
||||||
.get(
|
|
||||||
"",
|
|
||||||
async ({ note, user, query}) => {
|
|
||||||
return await note.getOwnerNotes(user.id, query.limit, query.offset);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
query:t.Object({
|
|
||||||
limit: t.Optional(t.Number()),
|
|
||||||
offset: t.Optional(t.Number())
|
|
||||||
}),
|
|
||||||
response:getNoteResponses,
|
|
||||||
detail:{
|
|
||||||
"description":"Get all notes of the user",
|
|
||||||
"summary":"Get all notes"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.get(
|
|
||||||
":id",
|
|
||||||
async ({ note, user, params:{id} }) => {
|
|
||||||
return await note.getNoteById(id, user.id);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params:t.Object({
|
|
||||||
id: t.String(),
|
|
||||||
}),
|
|
||||||
response: getNoteResponses,
|
|
||||||
detail:{
|
|
||||||
"description":"Get a note by Id",
|
|
||||||
"summary":"Get a note"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.post(
|
|
||||||
"",
|
|
||||||
async ({ body, note, user }) => {
|
|
||||||
return await note.createNote(body, user.id);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
body: createNoteSchema,
|
|
||||||
response: getNoteResponses,
|
|
||||||
detail:{
|
|
||||||
"description":"Create a new note",
|
|
||||||
"summary":"Create a note"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).patch(
|
|
||||||
":id",
|
|
||||||
async ({ body, note, user, params:{id} }) => {
|
|
||||||
return await note.updateNoteById(id, body, user.id);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
body: createNoteSchema,
|
|
||||||
params:t.Object({
|
|
||||||
id: t.String(),
|
|
||||||
}),
|
|
||||||
response: getNoteResponses,
|
|
||||||
detail:{
|
|
||||||
"description":"Update a note by Id",
|
|
||||||
"summary":"Update a note"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
).delete(
|
|
||||||
":id",
|
|
||||||
async ({ note, user, params:{id} }) => {
|
|
||||||
return await note.deleteNoteById(id, user.id);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
params:t.Object({
|
|
||||||
id: t.String(),
|
|
||||||
}),
|
|
||||||
response: deleteNoteResponses,
|
|
||||||
detail:{
|
|
||||||
"description":"Delete a note by Id",
|
|
||||||
"summary":"Delete a note"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.delete(
|
|
||||||
"",
|
|
||||||
async ({ note, user }) => {
|
|
||||||
return await note.deleteAllNotes(user.id);
|
|
||||||
},
|
|
||||||
{
|
|
||||||
response: deleteNoteResponses,
|
|
||||||
detail:{
|
|
||||||
"description":"Delete all notes of an user",
|
|
||||||
"summary":"Delete all notes"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
@ -1,7 +1,4 @@
|
||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import { drizzle } from "drizzle-orm/node-postgres";
|
import { drizzle } from "drizzle-orm/node-postgres";
|
||||||
import { getDbConfig } from "../lib/utils/env";
|
|
||||||
|
|
||||||
const dbConfig = getDbConfig()
|
export const db = drizzle(process.env.DATABASE_URL!);
|
||||||
|
|
||||||
export const db = drizzle(dbConfig.DATABASE_URL);
|
|
||||||
|
|
|
||||||
53
src/db/schema.ts
Normal file
53
src/db/schema.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import { pgTable, pgSchema, text, timestamp, boolean } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
|
export const auth_schema = pgSchema("auth")
|
||||||
|
|
||||||
|
export const user = auth_schema.table("user", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
name: text("name").notNull(),
|
||||||
|
email: text("email").notNull().unique(),
|
||||||
|
emailVerified: boolean("email_verified").notNull(),
|
||||||
|
image: text("image"),
|
||||||
|
createdAt: timestamp("created_at").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const session = auth_schema.table("session", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
expiresAt: timestamp("expires_at").notNull(),
|
||||||
|
token: text("token").notNull().unique(),
|
||||||
|
createdAt: timestamp("created_at").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull(),
|
||||||
|
ipAddress: text("ip_address"),
|
||||||
|
userAgent: text("user_agent"),
|
||||||
|
userId: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const account = auth_schema.table("account", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
accountId: text("account_id").notNull(),
|
||||||
|
providerId: text("provider_id").notNull(),
|
||||||
|
userId: text("user_id")
|
||||||
|
.notNull()
|
||||||
|
.references(() => user.id),
|
||||||
|
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").notNull(),
|
||||||
|
updatedAt: timestamp("updated_at").notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const verification = auth_schema.table("verification", {
|
||||||
|
id: text("id").primaryKey(),
|
||||||
|
identifier: text("identifier").notNull(),
|
||||||
|
value: text("value").notNull(),
|
||||||
|
expiresAt: timestamp("expires_at").notNull(),
|
||||||
|
createdAt: timestamp("created_at"),
|
||||||
|
updatedAt: timestamp("updated_at"),
|
||||||
|
});
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
import { text, integer, timestamp, boolean, pgSchema } from "drizzle-orm/pg-core";
|
|
||||||
|
|
||||||
export const authSchema = pgSchema('auth');
|
|
||||||
|
|
||||||
export const user = authSchema.table("user", {
|
|
||||||
id: text("id").primaryKey(),
|
|
||||||
name: text('name').notNull(),
|
|
||||||
email: text('email').notNull().unique(),
|
|
||||||
emailVerified: boolean('email_verified').notNull(),
|
|
||||||
image: text('image'),
|
|
||||||
createdAt: timestamp('created_at').notNull(),
|
|
||||||
updatedAt: timestamp('updated_at').notNull()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const session = authSchema.table("session", {
|
|
||||||
id: text("id").primaryKey(),
|
|
||||||
expiresAt: timestamp('expires_at').notNull(),
|
|
||||||
token: text('token').notNull().unique(),
|
|
||||||
createdAt: timestamp('created_at').notNull(),
|
|
||||||
updatedAt: timestamp('updated_at').notNull(),
|
|
||||||
ipAddress: text('ip_address'),
|
|
||||||
userAgent: text('user_agent'),
|
|
||||||
userId: text('user_id').notNull().references(() => user.id)
|
|
||||||
});
|
|
||||||
|
|
||||||
export const account = authSchema.table("account", {
|
|
||||||
id: text("id").primaryKey(),
|
|
||||||
accountId: text('account_id').notNull(),
|
|
||||||
providerId: text('provider_id').notNull(),
|
|
||||||
userId: text('user_id').notNull().references(() => user.id),
|
|
||||||
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').notNull(),
|
|
||||||
updatedAt: timestamp('updated_at').notNull()
|
|
||||||
});
|
|
||||||
|
|
||||||
export const verification = authSchema.table("verification", {
|
|
||||||
id: text("id").primaryKey(),
|
|
||||||
identifier: text('identifier').notNull(),
|
|
||||||
value: text('value').notNull(),
|
|
||||||
expiresAt: timestamp('expires_at').notNull(),
|
|
||||||
createdAt: timestamp('created_at'),
|
|
||||||
updatedAt: timestamp('updated_at')
|
|
||||||
});
|
|
||||||
|
|
||||||
export const rateLimit = authSchema.table("rate_limit", {
|
|
||||||
id: text("id").primaryKey(),
|
|
||||||
key: text('key'),
|
|
||||||
count: integer('count'),
|
|
||||||
lastRequest: integer('last_request')
|
|
||||||
});
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
|
|
||||||
import { createId } from '@paralleldrive/cuid2'
|
|
||||||
import { user } from "./auth";
|
|
||||||
|
|
||||||
export const note = pgTable("note", {
|
|
||||||
id: text("id").primaryKey().$defaultFn(()=> `note_${createId()}`),
|
|
||||||
title: text("title"),
|
|
||||||
content: text("content"),
|
|
||||||
createdAt: timestamp().notNull().defaultNow(),
|
|
||||||
updatedAt: timestamp(),
|
|
||||||
deletedAt: timestamp(),
|
|
||||||
ownerId: text().notNull().references(() => user.id)
|
|
||||||
})
|
|
||||||
|
|
@ -1,28 +0,0 @@
|
||||||
import * as React from 'react'
|
|
||||||
import { Tailwind, Section, Text } from '@react-email/components'
|
|
||||||
|
|
||||||
export default function AuthEmail({ message, link }: { message: string, link: string }) {
|
|
||||||
return (
|
|
||||||
<Tailwind>
|
|
||||||
<Section className="flex justify-center items-center w-full min-h-screen font-sans">
|
|
||||||
<Section className="flex flex-col items-center w-76 rounded-2xl px-6 py-1 bg-gray-50">
|
|
||||||
<Text className="text-2xl font-medium text-violet-500">
|
|
||||||
{message}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-gray-500 my-0">
|
|
||||||
Use the following Link to {message}
|
|
||||||
</Text>
|
|
||||||
<a href={link} className="text-blue-400 font-bold pt-2">Link</a>
|
|
||||||
<Text className="text-gray-600 text-xs">
|
|
||||||
Thanks
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</Section>
|
|
||||||
</Tailwind>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
AuthEmail.PreviewProps = {
|
|
||||||
link: "https://example.com",
|
|
||||||
message: "Verify your email address"
|
|
||||||
}
|
|
||||||
30
src/emails/otp.tsx
Normal file
30
src/emails/otp.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import * as React from 'react'
|
||||||
|
import { Tailwind, Section, Text } from '@react-email/components'
|
||||||
|
|
||||||
|
export default function OTPEmail({ otp }: { otp: number }) {
|
||||||
|
return (
|
||||||
|
<Tailwind>
|
||||||
|
<Section className="flex justify-center items-center w-full min-h-screen font-sans">
|
||||||
|
<Section className="flex flex-col items-center w-76 rounded-2xl px-6 py-1 bg-gray-50">
|
||||||
|
<Text className="text-xs font-medium text-violet-500">
|
||||||
|
Verify your Email Address
|
||||||
|
</Text>
|
||||||
|
<Text className="text-gray-500 my-0">
|
||||||
|
Use the following code to verify your email address
|
||||||
|
</Text>
|
||||||
|
<Text className="text-5xl font-bold pt-2">{otp}</Text>
|
||||||
|
<Text className="text-gray-400 font-light text-xs pb-4">
|
||||||
|
This code is valid for 10 minutes
|
||||||
|
</Text>
|
||||||
|
<Text className="text-gray-600 text-xs">
|
||||||
|
Thank you joining us
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</Section>
|
||||||
|
</Tailwind>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OTPEmail.PreviewProps = {
|
||||||
|
otp: 123456
|
||||||
|
}
|
||||||
54
src/index.ts
54
src/index.ts
|
|
@ -2,47 +2,27 @@ import { Elysia } from "elysia";
|
||||||
import { swagger } from "@elysiajs/swagger";
|
import { swagger } from "@elysiajs/swagger";
|
||||||
import { opentelemetry } from "@elysiajs/opentelemetry";
|
import { opentelemetry } from "@elysiajs/opentelemetry";
|
||||||
import { serverTiming } from "@elysiajs/server-timing";
|
import { serverTiming } from "@elysiajs/server-timing";
|
||||||
import { cors } from "@elysiajs/cors";
|
import { cors } from '@elysiajs/cors'
|
||||||
import { getBaseConfig, validateEnv } from "./lib/utils/env";
|
import { note } from "./api/note/note.route";
|
||||||
import { api } from "./api";
|
import { betterAuthView } from "./lib/auth/auth-view";
|
||||||
|
import { userMiddleware, userInfo } from "./middlewares/auth-middleware";
|
||||||
|
|
||||||
const baseConfig = getBaseConfig();
|
|
||||||
|
|
||||||
validateEnv();
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
.use(cors())
|
.use(cors())
|
||||||
.use(
|
.use(opentelemetry())
|
||||||
opentelemetry({
|
.use(swagger({
|
||||||
serviceName: baseConfig.SERVICE_NAME,
|
path: "/docs",
|
||||||
})
|
}))
|
||||||
)
|
|
||||||
.use(serverTiming())
|
.use(serverTiming())
|
||||||
.use(
|
|
||||||
swagger({
|
|
||||||
path: "/api/docs",
|
|
||||||
documentation: {
|
|
||||||
info: {
|
|
||||||
title: baseConfig.SERVICE_NAME,
|
|
||||||
version: "0.0.1",
|
|
||||||
description: `API docs for ${baseConfig.SERVICE_NAME}`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.onError(({ error, code }) => {
|
.onError(({ error, code }) => {
|
||||||
if (code === "NOT_FOUND")
|
if (code === "NOT_FOUND") return "Not Found :(";
|
||||||
return {
|
|
||||||
message: `Not Found`,
|
|
||||||
success: false,
|
|
||||||
name: baseConfig.SERVICE_NAME,
|
|
||||||
status: "active",
|
|
||||||
docs: {
|
|
||||||
default: "/api/docs",
|
|
||||||
auth: "/api/auth/docs",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
console.error(error);
|
console.error(error);
|
||||||
})
|
})
|
||||||
.use(api);
|
.derive(({ request }) => userMiddleware(request))
|
||||||
app.listen(baseConfig.PORT);
|
.all("/api/auth/*", betterAuthView)
|
||||||
console.log(`Server is running on: http://127.0.0.1:${baseConfig.PORT}`);
|
.use(note)
|
||||||
|
.get("/user", ({ user, session }) => userInfo(user, session));
|
||||||
|
|
||||||
|
app.listen(3000);
|
||||||
|
|
||||||
|
console.log("Server is running on: http://localhost:3000")
|
||||||
|
|
|
||||||
|
|
@ -2,66 +2,21 @@ import { betterAuth } from "better-auth";
|
||||||
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
import { drizzleAdapter } from "better-auth/adapters/drizzle";
|
||||||
import { db } from "../../db/index";
|
import { db } from "../../db/index";
|
||||||
import { openAPI } from "better-auth/plugins"
|
import { openAPI } from "better-auth/plugins"
|
||||||
import { user, account, verification, session, rateLimit } from "../../db/schema/auth";
|
import { user, account, verification, session } from "../../db/schema";
|
||||||
import { sendMail } from "../mail/mail";
|
|
||||||
import { renderToStaticMarkup } from "react-dom/server";
|
|
||||||
import { createElement } from "react";
|
|
||||||
import AuthEmail from "../../emails/auth";
|
|
||||||
|
|
||||||
export const auth = betterAuth({
|
export const auth = betterAuth({
|
||||||
database: drizzleAdapter(db, {
|
database: drizzleAdapter(db, {
|
||||||
|
// We're using Drizzle as our database
|
||||||
provider: "pg",
|
provider: "pg",
|
||||||
schema: {
|
schema: {
|
||||||
user: user,
|
user: user,
|
||||||
session: session,
|
session: session,
|
||||||
account: account,
|
account: account,
|
||||||
verification: verification,
|
verification: verification
|
||||||
rateLimit: rateLimit,
|
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
rateLimit: {
|
|
||||||
window: 60,
|
|
||||||
max: 100,
|
|
||||||
storage: "database",
|
|
||||||
modelName: "rateLimit", //optional by default "rateLimit" is used
|
|
||||||
customRules: {
|
|
||||||
"/sign-in/email": {
|
|
||||||
window: 3600 * 12,
|
|
||||||
max: 10,
|
|
||||||
},
|
|
||||||
"/two-factor/*": async (request) => {
|
|
||||||
// custom function to return rate limit window and max
|
|
||||||
return {
|
|
||||||
window: 3600 * 12,
|
|
||||||
max: 10,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true, // If you want to use email and password auth
|
enabled: true, // If you want to use email and password auth
|
||||||
requireEmailVerification: false,
|
|
||||||
sendResetPassword: async ({ user, url }, request) => {
|
|
||||||
const subject = "Reset your password";
|
|
||||||
const html = renderToStaticMarkup(createElement(AuthEmail, { message: subject, link: url }));
|
|
||||||
await sendMail({
|
|
||||||
to: user.email,
|
|
||||||
subject: subject,
|
|
||||||
html: html,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
emailVerification: {
|
|
||||||
sendVerificationEmail: async ({ user, url, token }, request) => {
|
|
||||||
const subject = "Verify your email address";
|
|
||||||
const html = renderToStaticMarkup(createElement(AuthEmail, { message: subject, link: url }));
|
|
||||||
await sendMail({
|
|
||||||
to: user.email,
|
|
||||||
subject: subject,
|
|
||||||
html: html,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
},
|
},
|
||||||
plugins: [
|
plugins: [
|
||||||
openAPI({
|
openAPI({
|
||||||
|
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
import nodemailer from 'nodemailer'
|
|
||||||
|
|
||||||
export async function sendMail({ to, subject, text, html }: { to: string, subject: string, text?: string, html?: string }) {
|
|
||||||
const transporter = nodemailer.createTransport({
|
|
||||||
host: process.env.SMTP_HOST!,
|
|
||||||
port: +process.env.SMTP_PORT!,
|
|
||||||
auth: {
|
|
||||||
user: process.env.SMTP_USER!,
|
|
||||||
pass: process.env.SMTP_PASSWORD!,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
await transporter.sendMail({
|
|
||||||
from: process.env.SMTP_FROM!,
|
|
||||||
to,
|
|
||||||
subject,
|
|
||||||
text: text,
|
|
||||||
html: html,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
@ -1,143 +0,0 @@
|
||||||
import { Client } from 'minio';
|
|
||||||
import { Buffer } from 'buffer';
|
|
||||||
import { getMinioConfig } from '../utils/env';
|
|
||||||
|
|
||||||
// MinIO client configuration
|
|
||||||
|
|
||||||
const minioConfig = getMinioConfig()
|
|
||||||
const minioClient = new Client({
|
|
||||||
endPoint: minioConfig.MINIO_ENDPOINT_URL,
|
|
||||||
useSSL: false,
|
|
||||||
accessKey: minioConfig.MINIO_ACCESS_KEY,
|
|
||||||
secretKey: minioConfig.MINIO_SECRET_KEY
|
|
||||||
});
|
|
||||||
|
|
||||||
const BUCKET_NAME = minioConfig.MINIO_BUCKET_NAME;
|
|
||||||
const SIGNED_URL_EXPIRY = 24 * 60 * 60; // 24 hours in seconds
|
|
||||||
|
|
||||||
// Ensure bucket exists
|
|
||||||
const ensureBucket = async (): Promise<void> => {
|
|
||||||
const bucketExists = await minioClient.bucketExists(BUCKET_NAME);
|
|
||||||
if (!bucketExists) {
|
|
||||||
await minioClient.makeBucket(BUCKET_NAME);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert File/Blob to Buffer
|
|
||||||
* @param file - File or Blob object
|
|
||||||
* @returns Promise<Buffer>
|
|
||||||
*/
|
|
||||||
const fileToBuffer = async (file: File | Blob): Promise<Buffer> => {
|
|
||||||
const arrayBuffer = await file.arrayBuffer();
|
|
||||||
return Buffer.from(arrayBuffer);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Upload a file to MinIO and return its signed URL
|
|
||||||
* Supports both Buffer and File/Blob (from multipart form-data)
|
|
||||||
* @param filename - The name to store the file as
|
|
||||||
* @param file - The file to upload (Buffer, File, or Blob)
|
|
||||||
* @param contentType - Optional MIME type of the file
|
|
||||||
* @returns Promise with the signed URL of the uploaded file
|
|
||||||
*/
|
|
||||||
export const uploadFileAndGetUrl = async (
|
|
||||||
filename: string,
|
|
||||||
file: Buffer | File | Blob,
|
|
||||||
contentType?: string
|
|
||||||
): Promise<string> => {
|
|
||||||
try {
|
|
||||||
await ensureBucket();
|
|
||||||
|
|
||||||
// Convert File/Blob to Buffer if needed
|
|
||||||
const fileBuffer = Buffer.isBuffer(file) ? file : await fileToBuffer(file);
|
|
||||||
|
|
||||||
// If file is from form-data and no contentType is provided, use its type
|
|
||||||
const metadata: Record<string, string> = {};
|
|
||||||
if (!contentType && 'type' in file) {
|
|
||||||
contentType = file.type;
|
|
||||||
}
|
|
||||||
if (contentType) {
|
|
||||||
metadata['Content-Type'] = contentType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Upload the file
|
|
||||||
await minioClient.putObject(
|
|
||||||
BUCKET_NAME,
|
|
||||||
filename,
|
|
||||||
fileBuffer,
|
|
||||||
fileBuffer.length,
|
|
||||||
metadata
|
|
||||||
);
|
|
||||||
|
|
||||||
// Generate and return signed URL
|
|
||||||
const url = await minioClient.presignedGetObject(
|
|
||||||
BUCKET_NAME,
|
|
||||||
filename,
|
|
||||||
SIGNED_URL_EXPIRY
|
|
||||||
);
|
|
||||||
|
|
||||||
return url;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error uploading file:', error);
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw new Error(`Failed to upload file: ${error.message}`);
|
|
||||||
}
|
|
||||||
throw new Error(`Failed to upload file: ${error}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a signed URL for an existing file
|
|
||||||
* @param filename - The name of the file to get URL for
|
|
||||||
* @returns Promise with the signed URL
|
|
||||||
*/
|
|
||||||
export const getSignedUrl = async (filename: string): Promise<string> => {
|
|
||||||
try {
|
|
||||||
// Check if file exists first
|
|
||||||
await minioClient.statObject(BUCKET_NAME, filename);
|
|
||||||
|
|
||||||
const url = await minioClient.presignedGetObject(
|
|
||||||
BUCKET_NAME,
|
|
||||||
filename,
|
|
||||||
SIGNED_URL_EXPIRY
|
|
||||||
);
|
|
||||||
|
|
||||||
return url;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating signed URL:', error);
|
|
||||||
if (error instanceof Error) throw new Error(`Failed to generate signed URL: ${error.message}`);
|
|
||||||
throw new Error(`Failed to generate signed URL: ${error}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete a file from MinIO
|
|
||||||
* @param filename - The name of the file to delete
|
|
||||||
* @returns Promise<void>
|
|
||||||
*/
|
|
||||||
export const deleteFile = async (filename: string): Promise<void> => {
|
|
||||||
try {
|
|
||||||
await minioClient.removeObject(BUCKET_NAME, filename);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting file:', error);
|
|
||||||
if (error instanceof Error) throw new Error(`Failed to delete file: ${error.message}`);
|
|
||||||
throw new Error(`Failed to delete file: ${error}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Usage examples:
|
|
||||||
|
|
||||||
// Upload a file and get its URL
|
|
||||||
// const fileBuffer = Buffer.from('Hello World');
|
|
||||||
// const url = await uploadFileAndGetUrl('hello.txt', fileBuffer);
|
|
||||||
// console.log('Uploaded file URL:', url);
|
|
||||||
|
|
||||||
// Get signed URL for existing file
|
|
||||||
// const url = await getSignedUrl('hello.txt');
|
|
||||||
// console.log('Signed URL:', url);
|
|
||||||
|
|
||||||
// Delete a file
|
|
||||||
// await deleteFile('hello.txt');
|
|
||||||
// console.log('File deleted successfully');
|
|
||||||
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
import { t } from "elysia";
|
|
||||||
|
|
||||||
export const commonResponses = {
|
|
||||||
400: t.Object(
|
|
||||||
{
|
|
||||||
data: t.Null(),
|
|
||||||
success: t.Boolean({ default: false }),
|
|
||||||
message: t.String({ default: "" }),
|
|
||||||
error: t.Number({ default: 400 }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description:
|
|
||||||
"Bad Request. Usually due to missing parameters, or invalid parameters.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
401: t.Object(
|
|
||||||
{
|
|
||||||
data: t.Null(),
|
|
||||||
success: t.Boolean({ default: false }),
|
|
||||||
message: t.String({ default: "" }),
|
|
||||||
error: t.Number({ default: 401 }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Unauthorized. Due to missing or invalid authentication.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
403: t.Object(
|
|
||||||
{
|
|
||||||
data: t.Null(),
|
|
||||||
success: t.Boolean({ default: false }),
|
|
||||||
message: t.String({ default: "" }),
|
|
||||||
error: t.Number({ default: 403 }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description:
|
|
||||||
"Forbidden. You do not have permission to access this resource or to perform this action.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
404: t.Object(
|
|
||||||
{
|
|
||||||
data: t.Null(),
|
|
||||||
success: t.Boolean({ default: false }),
|
|
||||||
message: t.String({ default: "" }),
|
|
||||||
error: t.Number({ default: 404 }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description: "Not Found. The requested resource was not found.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
429: t.Object(
|
|
||||||
{
|
|
||||||
data: t.Null(),
|
|
||||||
success: t.Boolean({ default: false }),
|
|
||||||
message: t.String({ default: "" }),
|
|
||||||
error: t.Number({ default: 429 }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description:
|
|
||||||
"Too Many Requests. You have exceeded the rate limit. Try again later.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
500: t.Object(
|
|
||||||
{
|
|
||||||
data: t.Null(),
|
|
||||||
success: t.Boolean({ default: false }),
|
|
||||||
message: t.String({ default: "" }),
|
|
||||||
error: t.Number({ default: 500 }),
|
|
||||||
},
|
|
||||||
{
|
|
||||||
description:
|
|
||||||
"Internal Server Error. This is a problem with the server that you cannot fix.",
|
|
||||||
}
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
// Define the environment schema
|
|
||||||
const envSchema = z.object({
|
|
||||||
// Base
|
|
||||||
PORT: z.string().max(5),
|
|
||||||
SERVICE_NAME: z.string(),
|
|
||||||
|
|
||||||
// Database
|
|
||||||
DATABASE_URL: z.string().url(),
|
|
||||||
|
|
||||||
// MinIO
|
|
||||||
MINIO_ACCESS_KEY: z.string(),
|
|
||||||
MINIO_SECRET_KEY: z.string().min(8),
|
|
||||||
MINIO_ENDPOINT_URL: z.string().url(),
|
|
||||||
MINIO_BUCKET_NAME: z.string(),
|
|
||||||
|
|
||||||
// Auth
|
|
||||||
BETTER_AUTH_SECRET: z.string().min(32),
|
|
||||||
BETTER_AUTH_URL: z.string().url(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create a type from the schema
|
|
||||||
type EnvConfig = z.infer<typeof envSchema>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validates environment variables and returns a typed config object
|
|
||||||
* Prints warnings for missing or invalid variables
|
|
||||||
*/
|
|
||||||
export const validateEnv = (): EnvConfig => {
|
|
||||||
const warnings: string[] = [];
|
|
||||||
|
|
||||||
// Parse environment with warning collection
|
|
||||||
const config = envSchema.safeParse(process.env);
|
|
||||||
|
|
||||||
if (!config.success) {
|
|
||||||
console.warn('\n🚨 Environment Variable Warnings:');
|
|
||||||
|
|
||||||
// Collect and categorize warnings
|
|
||||||
config.error.errors.forEach((error) => {
|
|
||||||
const path = error.path.join('.');
|
|
||||||
const message = error.message;
|
|
||||||
|
|
||||||
let warningMessage = `❌ ${path}: ${message}`;
|
|
||||||
|
|
||||||
// Add specific functionality warnings
|
|
||||||
if (path.startsWith('DB_') || path === 'DATABASE_URL') {
|
|
||||||
warningMessage += '\n ⚠️ Database functionality may not work properly';
|
|
||||||
}
|
|
||||||
if (path.startsWith('MINIO_')) {
|
|
||||||
warningMessage += '\n ⚠️ File storage functionality may not work properly';
|
|
||||||
}
|
|
||||||
if (path.startsWith('BETTER_AUTH_')) {
|
|
||||||
warningMessage += '\n ⚠️ Authentication functionality may not work properly';
|
|
||||||
}
|
|
||||||
warnings.push(warningMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Print all warnings
|
|
||||||
warnings.forEach((warning) => console.warn(warning));
|
|
||||||
console.warn('\n');
|
|
||||||
|
|
||||||
throw new Error('Environment validation failed. Check warnings above.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return config.data;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get validated environment config
|
|
||||||
* Throws error if validation fails
|
|
||||||
*/
|
|
||||||
export const getConfig = (): EnvConfig => {
|
|
||||||
return validateEnv();
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const getBaseConfig = (): Pick<EnvConfig, 'PORT' | 'SERVICE_NAME'> => {
|
|
||||||
const config = getConfig();
|
|
||||||
return {
|
|
||||||
PORT: config.PORT,
|
|
||||||
SERVICE_NAME: config.SERVICE_NAME,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Optional: Export individual config getters with type safety
|
|
||||||
export const getDbConfig = (): Pick<EnvConfig, 'DATABASE_URL'> => {
|
|
||||||
const config = getConfig();
|
|
||||||
return {
|
|
||||||
DATABASE_URL: config.DATABASE_URL,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getMinioConfig = (): Pick<EnvConfig, 'MINIO_ACCESS_KEY' | 'MINIO_SECRET_KEY' | 'MINIO_ENDPOINT_URL' | 'MINIO_BUCKET_NAME'> => {
|
|
||||||
const config = getConfig();
|
|
||||||
return {
|
|
||||||
MINIO_ACCESS_KEY: config.MINIO_ACCESS_KEY,
|
|
||||||
MINIO_SECRET_KEY: config.MINIO_SECRET_KEY,
|
|
||||||
MINIO_ENDPOINT_URL: config.MINIO_ENDPOINT_URL,
|
|
||||||
MINIO_BUCKET_NAME: config.MINIO_BUCKET_NAME,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getAuthConfig = (): Pick<EnvConfig, 'BETTER_AUTH_SECRET' | 'BETTER_AUTH_URL'> => {
|
|
||||||
const config = getConfig();
|
|
||||||
return {
|
|
||||||
BETTER_AUTH_SECRET: config.BETTER_AUTH_SECRET,
|
|
||||||
BETTER_AUTH_URL: config.BETTER_AUTH_URL,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
// Usage example:
|
|
||||||
try {
|
|
||||||
const config = getConfig();
|
|
||||||
// Your application code here
|
|
||||||
} catch (error) {
|
|
||||||
// Handle validation errors
|
|
||||||
process.exit(1);
|
|
||||||
}
|
|
||||||
|
|
@ -1,13 +1,15 @@
|
||||||
import { Session, User } from "better-auth/types";
|
import { Session, User } from "better-auth/types";
|
||||||
import { auth } from "../lib/auth/auth";
|
import { auth } from "../lib/auth/auth";
|
||||||
import { error } from "elysia";
|
|
||||||
|
|
||||||
// user middleware (compute user and session and pass to routes)
|
// user middleware (compute user and session and pass to routes)
|
||||||
export const userMiddleware = async (request: Request) => {
|
export const userMiddleware = async (request: Request) => {
|
||||||
const session = await auth.api.getSession({ headers: request.headers });
|
const session = await auth.api.getSession({ headers: request.headers });
|
||||||
|
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return error("Unauthorized", 401);
|
return {
|
||||||
|
user: null,
|
||||||
|
session: null,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue