diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..4cc714a --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +DATABASE_URL= \ No newline at end of file diff --git a/.gitignore b/.gitignore index 87e5610..996fc0e 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ yarn-debug.log* yarn-error.log* # local env files +.env .env.local .env.development.local .env.test.local diff --git a/auth-schema.ts b/auth-schema.ts new file mode 100644 index 0000000..4214f12 --- /dev/null +++ b/auth-schema.ts @@ -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"), +}); diff --git a/bun.lockb b/bun.lockb index bc19a66..975d250 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/docker-compose.yaml b/docker-compose.yaml index 048a584..c7753ab 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -43,6 +43,22 @@ services: networks: - api-network + db: + image: postgres:latest + environment: + POSTGRES_USER: myusername + POSTGRES_PASSWORD: mypassword + POSTGRES_DB: mydatabase + ports: + - "5432:5432" + volumes: + - postgres_data:/var/lib/postgresql/data + networks: + - api-network + networks: api-network: driver: bridge + +volumes: + postgres_data: diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 0000000..bb9afba --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,11 @@ +import "dotenv/config"; +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + out: "./drizzle", + schema: "./src/db/schema.ts", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/drizzle/0000_elite_ben_parker.sql b/drizzle/0000_elite_ben_parker.sql new file mode 100644 index 0000000..64f5981 --- /dev/null +++ b/drizzle/0000_elite_ben_parker.sql @@ -0,0 +1,50 @@ +CREATE TABLE "account" ( + "id" text PRIMARY KEY NOT NULL, + "account_id" text NOT NULL, + "provider_id" text NOT NULL, + "user_id" text NOT NULL, + "access_token" text, + "refresh_token" text, + "id_token" text, + "access_token_expires_at" timestamp, + "refresh_token_expires_at" timestamp, + "scope" text, + "password" text, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL +); +--> statement-breakpoint +CREATE TABLE "session" ( + "id" text PRIMARY KEY NOT NULL, + "expires_at" timestamp NOT NULL, + "token" text NOT NULL, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + "ip_address" text, + "user_agent" text, + "user_id" text NOT NULL, + CONSTRAINT "session_token_unique" UNIQUE("token") +); +--> statement-breakpoint +CREATE TABLE "user" ( + "id" text PRIMARY KEY NOT NULL, + "name" text NOT NULL, + "email" text NOT NULL, + "email_verified" boolean NOT NULL, + "image" text, + "created_at" timestamp NOT NULL, + "updated_at" timestamp NOT NULL, + CONSTRAINT "user_email_unique" UNIQUE("email") +); +--> statement-breakpoint +CREATE TABLE "verification" ( + "id" text PRIMARY KEY NOT NULL, + "identifier" text NOT NULL, + "value" text NOT NULL, + "expires_at" timestamp NOT NULL, + "created_at" timestamp, + "updated_at" timestamp +); +--> statement-breakpoint +ALTER TABLE "account" ADD CONSTRAINT "account_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "session" ADD CONSTRAINT "session_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json new file mode 100644 index 0000000..df9dd9b --- /dev/null +++ b/drizzle/meta/0000_snapshot.json @@ -0,0 +1,319 @@ +{ + "id": "f4717319-16e8-43a1-8b25-12a12964c98f", + "prevId": "00000000-0000-0000-0000-000000000000", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": [ + "token" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json new file mode 100644 index 0000000..4e586cb --- /dev/null +++ b/drizzle/meta/_journal.json @@ -0,0 +1,13 @@ +{ + "version": "7", + "dialect": "postgresql", + "entries": [ + { + "idx": 0, + "version": "7", + "when": 1736255248333, + "tag": "0000_elite_ben_parker", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/package.json b/package.json index 1dc1017..39ae0d1 100644 --- a/package.json +++ b/package.json @@ -4,16 +4,37 @@ "scripts": { "test": "echo \"Error: no test specified\" && exit 1", "dev": "bun run --watch src/index.ts", + "email": "email dev --dir src/emails", "build": "bun build --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts" }, "dependencies": { "@elysiajs/opentelemetry": "^1.2.0", "@elysiajs/server-timing": "^1.2.0", "@elysiajs/swagger": "^1.2.0", - "elysia": "latest" + "@paralleldrive/cuid2": "^2.2.2", + "@react-email/components": "^0.0.31", + "better-auth": "^1.1.10", + "dotenv": "^16.4.7", + "drizzle-orm": "^0.38.3", + "drizzle-typebox": "^0.2.1", + "elysia": "latest", + "nodemailer": "^6.9.16", + "pg": "^8.13.1", + "react": "^19.0.0", + "react-dom": "^19.0.0" + }, + "override": { + "@sinclair/typebox": "0.34.13" }, "devDependencies": { - "bun-types": "latest" + "@types/nodemailer": "^6.4.17", + "@types/pg": "^8.11.10", + "@types/react": "^19.0.3", + "@types/react-dom": "^19.0.2", + "bun-types": "latest", + "drizzle-kit": "^0.30.1", + "react-email": "^3.0.4", + "tsx": "^4.19.2" }, "module": "src/index.js" } \ No newline at end of file diff --git a/src/api/note/note.controller.ts b/src/api/note/note.controller.ts new file mode 100644 index 0000000..9086a64 --- /dev/null +++ b/src/api/note/note.controller.ts @@ -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) { + return (this.data[index] = { ...this.data[index], ...note }); + } +} diff --git a/src/api/note/note.model.ts b/src/api/note/note.model.ts new file mode 100644 index 0000000..41889b3 --- /dev/null +++ b/src/api/note/note.model.ts @@ -0,0 +1,7 @@ +import { t } from "elysia"; + +export const memoSchema = t.Object({ + data: t.String(), +}); + +export type Memo = typeof memoSchema.static; diff --git a/src/api/note/note.route.ts b/src/api/note/note.route.ts new file mode 100644 index 0000000..c2f624b --- /dev/null +++ b/src/api/note/note.route.ts @@ -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", + } + ); diff --git a/src/api/otp/otp.route.ts b/src/api/otp/otp.route.ts new file mode 100644 index 0000000..2c35f32 --- /dev/null +++ b/src/api/otp/otp.route.ts @@ -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" }), + } +); diff --git a/src/api/user/user.model.ts b/src/api/user/user.model.ts new file mode 100644 index 0000000..dbb100d --- /dev/null +++ b/src/api/user/user.model.ts @@ -0,0 +1,23 @@ +import { pgTable, varchar, timestamp } from "drizzle-orm/pg-core"; + +import { createId } from "@paralleldrive/cuid2"; +import { createInsertSchema } from "drizzle-typebox"; +import { t } from "elysia"; + +export const user = pgTable("user", { + id: varchar("id") + .$defaultFn(() => createId()) + .primaryKey(), + username: varchar("username").notNull().unique(), + password: varchar("password").notNull(), + email: varchar("email").notNull().unique(), + salt: varchar("salt", { length: 64 }).notNull(), + createdAt: timestamp("created_at").defaultNow().notNull(), +}); + +const _createUser = createInsertSchema(user, { + email: t.String({ format: "email" }), +}); + +// ✅ This works, by referencing the type from `drizzle-typebox` +export const createUserType = t.Omit(_createUser, ["id", "salt", "createdAt"]); diff --git a/src/db/index.ts b/src/db/index.ts new file mode 100644 index 0000000..eeef779 --- /dev/null +++ b/src/db/index.ts @@ -0,0 +1,4 @@ +import "dotenv/config"; +import { drizzle } from "drizzle-orm/node-postgres"; + +const db = drizzle(process.env.DATABASE_URL!); diff --git a/src/db/model.ts b/src/db/model.ts new file mode 100644 index 0000000..d50a294 --- /dev/null +++ b/src/db/model.ts @@ -0,0 +1,7 @@ +import { createUserType } from "../api/user/user.model"; + +export const db = { + insert: { + user: createUserType, + }, +}; diff --git a/src/db/schema.ts b/src/db/schema.ts new file mode 100644 index 0000000..4214f12 --- /dev/null +++ b/src/db/schema.ts @@ -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"), +}); diff --git a/src/emails/otp.tsx b/src/emails/otp.tsx new file mode 100644 index 0000000..1c7e2eb --- /dev/null +++ b/src/emails/otp.tsx @@ -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 ( + +
+
+ + Verify your Email Address + + + Use the following code to verify your email address + + {otp} + + This code is valid for 10 minutes + + + Thank you joining us + +
+
+
+ ) +} + +OTPEmail.PreviewProps = { + otp: 123456 +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 5ad5897..11fd30f 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,8 +3,9 @@ import { swagger } from "@elysiajs/swagger"; import { opentelemetry } from "@elysiajs/opentelemetry"; import { serverTiming } from "@elysiajs/server-timing"; -import { note } from "./routes/note"; -import { user } from "./routes/user"; +import { note } from "./api/note/note.route"; +import { betterAuthView } from "./lib/auth/auth-view"; +import { userMiddleware, userInfo } from "./middlewares/auth-middleware"; const app = new Elysia() .use(opentelemetry()) @@ -14,6 +15,9 @@ const app = new Elysia() if (code === "NOT_FOUND") return "Not Found :("; console.error(error); }) - .use(user) .use(note) - .listen(3000); + .derive(({ request }) => userMiddleware(request)) + .all("/api/auth/*", betterAuthView) + .get("/user", ({ user, session }) => userInfo(user, session)); + +app.listen(3000); diff --git a/src/lib/auth/auth-view.ts b/src/lib/auth/auth-view.ts new file mode 100644 index 0000000..5bca005 --- /dev/null +++ b/src/lib/auth/auth-view.ts @@ -0,0 +1,12 @@ +import { Context, Elysia } from "elysia"; +import { auth } from "./auth"; + +export const betterAuthView = (context: Context) => { + const BETTER_AUTH_ACCEPT_METHODS = ["POST", "GET"]; + // validate request method + if (BETTER_AUTH_ACCEPT_METHODS.includes(context.request.method)) { + return auth.handler(context.request); + } else { + context.error(405); + } +}; diff --git a/src/lib/auth/auth.ts b/src/lib/auth/auth.ts new file mode 100644 index 0000000..dc116b6 --- /dev/null +++ b/src/lib/auth/auth.ts @@ -0,0 +1,26 @@ +import { betterAuth } from "better-auth"; +import { drizzleAdapter } from "better-auth/adapters/drizzle"; +import { db } from "../../db/model"; +export const auth = betterAuth({ + database: drizzleAdapter(db, { + // We're using Drizzle as our database + provider: "pg", + }), + emailAndPassword: { + enabled: true, // If you want to use email and password auth + }, + socialProviders: { + /* + * We're using Google and Github as our social provider, + * make sure you have set your environment variables + */ + // github: { + // clientId: process.env.GITHUB_CLIENT_ID!, + // clientSecret: process.env.GITHUB_CLIENT_SECRET!, + // }, + // google: { + // clientId: process.env.GOOGLE_CLIENT_ID!, + // clientSecret: process.env.GOOGLE_CLIENT_SECRET!, + // }, + }, +}); diff --git a/src/middlewares/auth-middleware.ts b/src/middlewares/auth-middleware.ts new file mode 100644 index 0000000..1cadaaa --- /dev/null +++ b/src/middlewares/auth-middleware.ts @@ -0,0 +1,29 @@ +import { Session, User } from "better-auth/types"; +import { auth } from "../lib/auth/auth"; + +// 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 { + user: null, + session: null, + }; + } + + return { + user: session.user, + session: session.session, + }; +}; + +// user info view +// type User can be export from `typeof auth.$Infer.Session.user` +// type Session can be export from `typeof auth.$Infer.Session.session` +export const userInfo = (user: User | null, session: Session | null) => { + return { + user: user, + session: session, + }; +}; diff --git a/src/routes/note.ts b/src/routes/note.ts deleted file mode 100644 index 3ba2538..0000000 --- a/src/routes/note.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Elysia, t } from "elysia"; -import { getUserId, userService } from "./user"; - -const memo = t.Object({ - data: t.String(), - author: t.String(), -}); - -type Memo = typeof memo.static; - -class Note { - constructor( - public data: Memo[] = [ - { - data: "Moonhalo", - author: "saltyaom", - }, - ] - ) {} - - add(note: Memo) { - this.data.push(note); - - return this.data; - } - - remove(index: number) { - return this.data.splice(index, 1); - } - - update(index: number, note: Partial) { - return (this.data[index] = { ...this.data[index], ...note }); - } -} - -export const note = new Elysia({ prefix: "/note" }) - .use(userService) - .decorate("note", new Note()) - .model({ - memo: t.Omit(memo, ["author"]), - }) - .onTransform(function log({ body, params, path, request: { method } }) { - console.log(`${method} ${path}`, { - body, - params, - }); - }) - .get("/", ({ note }) => note.data) - .use(getUserId) - .put( - "/", - ({ note, body: { data }, username }) => - note.add({ data, author: username }), - { - 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, username }) => { - if (index in note.data) - return note.update(index, { data, author: username }); - - return error(422); - }, - { - isSignIn: true, - body: "memo", - } - ); diff --git a/src/routes/user.ts b/src/routes/user.ts deleted file mode 100644 index b486ac4..0000000 --- a/src/routes/user.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { Elysia, t } from "elysia"; - -export const userService = new Elysia({ name: "user/service" }) - .state({ - user: {} as Record, - session: {} as Record, - }) - .model({ - signIn: t.Object({ - username: t.String({ minLength: 1 }), - password: t.String({ minLength: 8 }), - }), - session: t.Cookie( - { - token: t.Number(), - }, - { - secrets: "seia", - } - ), - optionalSession: t.Optional(t.Ref("session")), - }) - .macro({ - isSignIn(enabled: boolean) { - if (!enabled) return; - - return { - beforeHandle({ error, cookie: { token }, store: { session } }) { - if (!token.value) - return error(401, { - success: false, - message: "Unauthorized", - }); - - const username = session[token.value as unknown as number]; - - if (!username) - return error(401, { - success: false, - message: "Unauthorized", - }); - }, - }; - }, - }); - -export const getUserId = new Elysia() - .use(userService) - .guard({ - isSignIn: true, - cookie: "session", - }) - .resolve(({ store: { session }, cookie: { token } }) => ({ - username: session[token.value], - })) - .as("plugin"); - -export const user = new Elysia({ prefix: "/user" }) - .use(userService) - .put( - "/sign-up", - async ({ body: { username, password }, store, error }) => { - if (store.user[username]) - return error(400, { - success: false, - message: "User already exists", - }); - - store.user[username] = await Bun.password.hash(password); - - return { - success: true, - message: "User created", - }; - }, - { - body: "signIn", - } - ) - .post( - "/sign-in", - async ({ - store: { user, session }, - error, - body: { username, password }, - cookie: { token }, - }) => { - if ( - !user[username] || - !(await Bun.password.verify(password, user[username])) - ) - return error(400, { - success: false, - message: "Invalid username or password", - }); - - const key = crypto.getRandomValues(new Uint32Array(1))[0]; - session[key] = username; - token.value = key; - - return { - success: true, - message: `Signed in as ${username}`, - }; - }, - { - body: "signIn", - cookie: "optionalSession", - } - ) - .get( - "/sign-out", - ({ cookie: { token } }) => { - token.remove(); - - return { - success: true, - message: "Signed out", - }; - }, - { - cookie: "optionalSession", - } - ) - .use(getUserId) - .get("/profile", ({ username }) => ({ - success: true, - username, - })); diff --git a/tsconfig.json b/tsconfig.json index 1ca2350..671e128 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,6 @@ { "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ - /* Projects */ // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */ // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */ @@ -9,11 +8,10 @@ // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ - /* Language and Environment */ - "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + "target": "ES2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ - // "jsx": "preserve", /* Specify what JSX code is generated. */ + "jsx": "react", /* Specify what JSX code is generated. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */ // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */ @@ -23,26 +21,25 @@ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ - /* Modules */ - "module": "ES2022", /* Specify what module code is generated. */ + "module": "ES2022", /* Specify what module code is generated. */ // "rootDir": "./", /* Specify the root folder within your source files. */ - "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ + "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */ // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ - "types": ["bun-types"], /* Specify type package names to be included without being referenced in a source file. */ + "types": [ + "bun-types" + ], /* Specify type package names to be included without being referenced in a source file. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "resolveJsonModule": true, /* Enable importing .json files. */ // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */ - /* JavaScript Support */ // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */ - /* Emit */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */ @@ -67,16 +64,14 @@ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ - /* Interop Constraints */ // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ - "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ - "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ - + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ /* Type Checking */ - "strict": true, /* Enable all strict type-checking options. */ + "strict": true, /* Enable all strict type-checking options. */ // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */ // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */ // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */ @@ -95,9 +90,8 @@ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ - /* Completeness */ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ - "skipLibCheck": true /* Skip type checking all .d.ts files. */ + "skipLibCheck": true /* Skip type checking all .d.ts files. */ } -} +} \ No newline at end of file