elysia-base
This commit is contained in:
parent
af338049c7
commit
fe9949c70d
7 changed files with 320 additions and 5 deletions
31
Dockerfile
Normal file
31
Dockerfile
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# Build stage
|
||||||
|
FROM oven/bun:1 AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy package files
|
||||||
|
COPY package.json .
|
||||||
|
COPY bun.lockb .
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
RUN bun install --frozen-lockfile
|
||||||
|
|
||||||
|
# Copy source code
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
RUN bun build ./src/index.ts --compile --outfile server
|
||||||
|
|
||||||
|
# Production stage
|
||||||
|
FROM debian:bookworm-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy only the compiled binary from builder
|
||||||
|
COPY --from=builder /app/server .
|
||||||
|
|
||||||
|
# Expose the port your app runs on
|
||||||
|
EXPOSE 3000
|
||||||
|
|
||||||
|
# Run the binary
|
||||||
|
CMD ["./server"]
|
||||||
BIN
bun.lockb
Executable file
BIN
bun.lockb
Executable file
Binary file not shown.
48
docker-compose.yaml
Normal file
48
docker-compose.yaml
Normal file
|
|
@ -0,0 +1,48 @@
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "3000:3000"
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
- OTEL_EXPORTER_OTLP_ENDPOINT=http://tracing:4318
|
||||||
|
- OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
|
||||||
|
networks:
|
||||||
|
- api-network
|
||||||
|
depends_on:
|
||||||
|
- tracing
|
||||||
|
|
||||||
|
tracing:
|
||||||
|
image: jaegertracing/all-in-one:latest
|
||||||
|
environment:
|
||||||
|
- COLLECTOR_ZIPKIN_HOST_PORT=:9411
|
||||||
|
- COLLECTOR_OTLP_ENABLED=true
|
||||||
|
ports:
|
||||||
|
# UI
|
||||||
|
- "16686:16686"
|
||||||
|
# Zipkin compatible endpoint
|
||||||
|
- "9411:9411"
|
||||||
|
# OTLP gRPC
|
||||||
|
- "4317:4317"
|
||||||
|
# OTLP HTTP
|
||||||
|
- "4318:4318"
|
||||||
|
# Jaeger gRPC
|
||||||
|
- "14250:14250"
|
||||||
|
# Jaeger HTTP
|
||||||
|
- "14268:14268"
|
||||||
|
# Admin HTTP
|
||||||
|
- "14269:14269"
|
||||||
|
# Agent configs
|
||||||
|
- "5778:5778"
|
||||||
|
# Thrift compact
|
||||||
|
- "6831:6831/udp"
|
||||||
|
# Thrift binary
|
||||||
|
- "6832:6832/udp"
|
||||||
|
networks:
|
||||||
|
- api-network
|
||||||
|
|
||||||
|
networks:
|
||||||
|
api-network:
|
||||||
|
driver: bridge
|
||||||
|
|
@ -3,9 +3,13 @@
|
||||||
"version": "1.0.50",
|
"version": "1.0.50",
|
||||||
"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",
|
||||||
|
"build": "bun build --compile --minify-whitespace --minify-syntax --target bun --outfile server ./src/index.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@elysiajs/opentelemetry": "^1.2.0",
|
||||||
|
"@elysiajs/server-timing": "^1.2.0",
|
||||||
|
"@elysiajs/swagger": "^1.2.0",
|
||||||
"elysia": "latest"
|
"elysia": "latest"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
20
src/index.ts
20
src/index.ts
|
|
@ -1,7 +1,19 @@
|
||||||
import { Elysia } from "elysia";
|
import { Elysia } from "elysia";
|
||||||
|
import { swagger } from "@elysiajs/swagger";
|
||||||
|
import { opentelemetry } from "@elysiajs/opentelemetry";
|
||||||
|
import { serverTiming } from "@elysiajs/server-timing";
|
||||||
|
|
||||||
const app = new Elysia().get("/", () => "Hello Elysia").listen(3000);
|
import { note } from "./routes/note";
|
||||||
|
import { user } from "./routes/user";
|
||||||
|
|
||||||
console.log(
|
const app = new Elysia()
|
||||||
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
|
.use(opentelemetry())
|
||||||
);
|
.use(swagger())
|
||||||
|
.use(serverTiming())
|
||||||
|
.onError(({ error, code }) => {
|
||||||
|
if (code === "NOT_FOUND") return "Not Found :(";
|
||||||
|
console.error(error);
|
||||||
|
})
|
||||||
|
.use(user)
|
||||||
|
.use(note)
|
||||||
|
.listen(3000);
|
||||||
|
|
|
||||||
91
src/routes/note.ts
Normal file
91
src/routes/note.ts
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
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",
|
||||||
|
}
|
||||||
|
);
|
||||||
129
src/routes/user.ts
Normal file
129
src/routes/user.ts
Normal file
|
|
@ -0,0 +1,129 @@
|
||||||
|
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,
|
||||||
|
}));
|
||||||
Loading…
Add table
Reference in a new issue