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
|
||||
SERVICE_NAME=<dash-seperated-lowercased-unique-accross-projects-name>
|
||||
DATABASE_URL="postgresql://..."
|
||||
MINIO_ACCESS_KEY=
|
||||
MINIO_SECRET_KEY=
|
||||
MINIO_ENDPOINT_URL=
|
||||
MINIO_BUCKET_NAME=
|
||||
BETTER_AUTH_SECRET=
|
||||
DB_USER=postgres
|
||||
DB_PASSWORD=
|
||||
DB_NAME=postgres
|
||||
DB_HOST=localhost
|
||||
DB_PORT=5432
|
||||
|
||||
# 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=
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -29,7 +29,6 @@ yarn-error.log*
|
|||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.services
|
||||
.env.production.local
|
||||
|
||||
# vercel
|
||||
|
|
@ -42,5 +41,3 @@ yarn-error.log*
|
|||
**/*.log
|
||||
package-lock.json
|
||||
**/*.bun
|
||||
|
||||
./server
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@ RUN bun install --frozen-lockfile
|
|||
# Copy source code
|
||||
COPY . .
|
||||
|
||||
# RUN DB Migrations and build
|
||||
RUN bun run db:migrateb && bun run build
|
||||
# Build the application
|
||||
RUN bun build ./src/index.ts --compile --outfile server
|
||||
|
||||
# Production stage
|
||||
FROM debian:bookworm-slim
|
||||
|
|
|
|||
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: .
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "${PORT}:${PORT}"
|
||||
- "3000:3000"
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
OTEL_EXPORTER_OTLP_ENDPOINT: http://tracing:4318
|
||||
OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
|
||||
DATABASE_URL: ${DATABASE_URL}
|
||||
- NODE_ENV=production
|
||||
- OTEL_EXPORTER_OTLP_ENDPOINT=http://tracing:4318
|
||||
- OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
||||
- 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({
|
||||
out: "./drizzle",
|
||||
schema: "./src/db/schema",
|
||||
schema: "./src/db/schema.ts",
|
||||
dialect: "postgresql",
|
||||
dbCredentials: {
|
||||
url: process.env.DATABASE_URL!,
|
||||
|
|
|
|||
|
|
@ -16,13 +16,6 @@ CREATE TABLE "auth"."account" (
|
|||
"updated_at" timestamp NOT NULL
|
||||
);
|
||||
--> 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" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
|
|
@ -55,16 +48,5 @@ CREATE TABLE "auth"."verification" (
|
|||
"updated_at" timestamp
|
||||
);
|
||||
--> 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"."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 "note" ADD CONSTRAINT "note_ownerId_user_id_fk" FOREIGN KEY ("ownerId") REFERENCES "auth"."user"("id") ON DELETE no action ON UPDATE no action;
|
||||
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;
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"id": "50bd2c27-8d45-478f-a894-b2f7cd4a718b",
|
||||
"id": "1ada386b-032f-4c0c-a867-e25f2e921167",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"version": "7",
|
||||
"dialect": "postgresql",
|
||||
|
|
@ -110,43 +110,6 @@
|
|||
"checkConstraints": {},
|
||||
"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": {
|
||||
"name": "session",
|
||||
"schema": "auth",
|
||||
|
|
@ -342,77 +305,6 @@
|
|||
"policies": {},
|
||||
"checkConstraints": {},
|
||||
"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": {},
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@
|
|||
{
|
||||
"idx": 0,
|
||||
"version": "7",
|
||||
"when": 1737019435130,
|
||||
"tag": "0000_uneven_professor_monster",
|
||||
"when": 1736605568502,
|
||||
"tag": "0000_rich_reavers",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -5,13 +5,9 @@
|
|||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"dev": "bun run --watch src/index.ts",
|
||||
"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:check": "drizzle-kit check",
|
||||
"db:generate": "drizzle-kit generate",
|
||||
"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"
|
||||
},
|
||||
"dependencies": {
|
||||
|
|
@ -26,7 +22,6 @@
|
|||
"drizzle-orm": "^0.38.3",
|
||||
"drizzle-typebox": "^0.2.1",
|
||||
"elysia": "latest",
|
||||
"minio": "^8.0.3",
|
||||
"nodemailer": "^6.9.16",
|
||||
"pg": "^8.13.1",
|
||||
"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 { drizzle } from "drizzle-orm/node-postgres";
|
||||
import { getDbConfig } from "../lib/utils/env";
|
||||
|
||||
const dbConfig = getDbConfig()
|
||||
|
||||
export const db = drizzle(dbConfig.DATABASE_URL);
|
||||
export const db = drizzle(process.env.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 { opentelemetry } from "@elysiajs/opentelemetry";
|
||||
import { serverTiming } from "@elysiajs/server-timing";
|
||||
import { cors } from "@elysiajs/cors";
|
||||
import { getBaseConfig, validateEnv } from "./lib/utils/env";
|
||||
import { api } from "./api";
|
||||
import { cors } from '@elysiajs/cors'
|
||||
import { note } from "./api/note/note.route";
|
||||
import { betterAuthView } from "./lib/auth/auth-view";
|
||||
import { userMiddleware, userInfo } from "./middlewares/auth-middleware";
|
||||
|
||||
const baseConfig = getBaseConfig();
|
||||
|
||||
validateEnv();
|
||||
const app = new Elysia()
|
||||
.use(cors())
|
||||
.use(
|
||||
opentelemetry({
|
||||
serviceName: baseConfig.SERVICE_NAME,
|
||||
})
|
||||
)
|
||||
.use(opentelemetry())
|
||||
.use(swagger({
|
||||
path: "/docs",
|
||||
}))
|
||||
.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 }) => {
|
||||
if (code === "NOT_FOUND")
|
||||
return {
|
||||
message: `Not Found`,
|
||||
success: false,
|
||||
name: baseConfig.SERVICE_NAME,
|
||||
status: "active",
|
||||
docs: {
|
||||
default: "/api/docs",
|
||||
auth: "/api/auth/docs",
|
||||
},
|
||||
};
|
||||
if (code === "NOT_FOUND") return "Not Found :(";
|
||||
console.error(error);
|
||||
})
|
||||
.use(api);
|
||||
app.listen(baseConfig.PORT);
|
||||
console.log(`Server is running on: http://127.0.0.1:${baseConfig.PORT}`);
|
||||
.derive(({ request }) => userMiddleware(request))
|
||||
.all("/api/auth/*", betterAuthView)
|
||||
.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 { db } from "../../db/index";
|
||||
import { openAPI } from "better-auth/plugins"
|
||||
import { user, account, verification, session, rateLimit } from "../../db/schema/auth";
|
||||
import { sendMail } from "../mail/mail";
|
||||
import { renderToStaticMarkup } from "react-dom/server";
|
||||
import { createElement } from "react";
|
||||
import AuthEmail from "../../emails/auth";
|
||||
import { user, account, verification, session } from "../../db/schema";
|
||||
|
||||
export const auth = betterAuth({
|
||||
database: drizzleAdapter(db, {
|
||||
// We're using Drizzle as our database
|
||||
provider: "pg",
|
||||
schema: {
|
||||
user: user,
|
||||
session: session,
|
||||
account: account,
|
||||
verification: verification,
|
||||
rateLimit: rateLimit,
|
||||
verification: verification
|
||||
}
|
||||
}),
|
||||
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: {
|
||||
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: [
|
||||
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 { auth } from "../lib/auth/auth";
|
||||
import { error } from "elysia";
|
||||
|
||||
// user middleware (compute user and session and pass to routes)
|
||||
export const userMiddleware = async (request: Request) => {
|
||||
const session = await auth.api.getSession({ headers: request.headers });
|
||||
|
||||
if (!session) {
|
||||
return error("Unauthorized", 401);
|
||||
return {
|
||||
user: null,
|
||||
session: null,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue