init with db

This commit is contained in:
Sanjib Kumar Sen 2025-01-07 20:52:27 +06:00
parent fe9949c70d
commit 7aee25cb33
26 changed files with 799 additions and 244 deletions

1
.env.example Normal file
View file

@ -0,0 +1 @@
DATABASE_URL=

1
.gitignore vendored
View file

@ -25,6 +25,7 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
# local env files # local env files
.env
.env.local .env.local
.env.development.local .env.development.local
.env.test.local .env.test.local

51
auth-schema.ts Normal file
View 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

Binary file not shown.

View file

@ -43,6 +43,22 @@ services:
networks: networks:
- api-network - 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: networks:
api-network: api-network:
driver: bridge driver: bridge
volumes:
postgres_data:

11
drizzle.config.ts Normal file
View file

@ -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!,
},
});

View file

@ -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;

View file

@ -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": {}
}
}

View file

@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1736255248333,
"tag": "0000_elite_ben_parker",
"breakpoints": true
}
]
}

View file

@ -4,16 +4,37 @@
"scripts": { "scripts": {
"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",
"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": {
"@elysiajs/opentelemetry": "^1.2.0", "@elysiajs/opentelemetry": "^1.2.0",
"@elysiajs/server-timing": "^1.2.0", "@elysiajs/server-timing": "^1.2.0",
"@elysiajs/swagger": "^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": { "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" "module": "src/index.js"
} }

View 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 });
}
}

View file

@ -0,0 +1,7 @@
import { t } from "elysia";
export const memoSchema = t.Object({
data: t.String(),
});
export type Memo = typeof memoSchema.static;

View 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
View 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" }),
}
);

View file

@ -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"]);

4
src/db/index.ts Normal file
View file

@ -0,0 +1,4 @@
import "dotenv/config";
import { drizzle } from "drizzle-orm/node-postgres";
const db = drizzle(process.env.DATABASE_URL!);

7
src/db/model.ts Normal file
View file

@ -0,0 +1,7 @@
import { createUserType } from "../api/user/user.model";
export const db = {
insert: {
user: createUserType,
},
};

51
src/db/schema.ts Normal file
View 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"),
});

30
src/emails/otp.tsx Normal file
View 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
}

View file

@ -3,8 +3,9 @@ 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 { note } from "./routes/note"; import { note } from "./api/note/note.route";
import { user } from "./routes/user"; import { betterAuthView } from "./lib/auth/auth-view";
import { userMiddleware, userInfo } from "./middlewares/auth-middleware";
const app = new Elysia() const app = new Elysia()
.use(opentelemetry()) .use(opentelemetry())
@ -14,6 +15,9 @@ const app = new Elysia()
if (code === "NOT_FOUND") return "Not Found :("; if (code === "NOT_FOUND") return "Not Found :(";
console.error(error); console.error(error);
}) })
.use(user)
.use(note) .use(note)
.listen(3000); .derive(({ request }) => userMiddleware(request))
.all("/api/auth/*", betterAuthView)
.get("/user", ({ user, session }) => userInfo(user, session));
app.listen(3000);

12
src/lib/auth/auth-view.ts Normal file
View file

@ -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);
}
};

26
src/lib/auth/auth.ts Normal file
View file

@ -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!,
// },
},
});

View file

@ -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,
};
};

View file

@ -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<Memo>) {
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",
}
);

View file

@ -1,129 +0,0 @@
import { Elysia, t } from "elysia";
export const userService = new Elysia({ name: "user/service" })
.state({
user: {} as Record<string, string>,
session: {} as Record<number, string>,
})
.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,
}));

View file

@ -1,7 +1,6 @@
{ {
"compilerOptions": { "compilerOptions": {
/* Visit https://aka.ms/tsconfig to read more about this file */ /* Visit https://aka.ms/tsconfig to read more about this file */
/* Projects */ /* Projects */
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of 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. */ // "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. */ // "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. */ // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */ // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */ /* 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. */ // "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. */ // "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */ // "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'. */ // "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. */ // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */ // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */ // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
/* Modules */ /* 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. */ // "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. */ // "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. */ // "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. */ // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */ // "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. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */ // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
// "resolveJsonModule": true, /* Enable importing .json files. */ // "resolveJsonModule": true, /* Enable importing .json files. */
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */ // "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */ /* JavaScript Support */
// "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */ // "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. */ // "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'. */ // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
/* Emit */ /* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */ // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */ // "declarationMap": true, /* Create sourcemaps for d.ts files. */
@ -67,16 +64,14 @@
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */ // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */ // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
// "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */ // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
/* Interop Constraints */ /* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ // "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. */ // "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. */ // "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 */ /* 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. */ // "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'. */ // "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. */ // "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. */ // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */ // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */ // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
/* Completeness */ /* Completeness */
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ // "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. */
} }
} }