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",
|
||||
"scripts": {
|
||||
"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": {
|
||||
"@elysiajs/opentelemetry": "^1.2.0",
|
||||
"@elysiajs/server-timing": "^1.2.0",
|
||||
"@elysiajs/swagger": "^1.2.0",
|
||||
"elysia": "latest"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
|
|||
20
src/index.ts
20
src/index.ts
|
|
@ -1,7 +1,19 @@
|
|||
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(
|
||||
`🦊 Elysia is running at ${app.server?.hostname}:${app.server?.port}`
|
||||
);
|
||||
const app = new Elysia()
|
||||
.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