Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
e5c7567
refactor: remove DTOs
tomast1337 Jan 11, 2026
e86f11f
feat: add validation package with DTOs
tomast1337 Jan 11, 2026
91c89da
feat: integrate @nbw/validation package into project structure
tomast1337 Jan 11, 2026
144a9eb
Merge branch 'develop' of github.com:OpenNBS/NoteBlockWorld into feat…
tomast1337 Apr 13, 2026
d58c628
feat(validation): migrate to Zod in @nbw/validation; drop class-valid…
tomast1337 Apr 13, 2026
9225d87
feat(validation): enhance JSON string handling in UploadSongDto schema
tomast1337 Apr 14, 2026
71a73ca
docs(validation): update README to reflect package purpose and usage …
tomast1337 Apr 14, 2026
b691a99
chore(validation): remove jest configuration file
tomast1337 Apr 14, 2026
478ebb4
refactor(validation): drop types.ts barrels; add uploadMeta and use D…
tomast1337 Apr 14, 2026
99bc5e6
feat: user profiles
tomast1337 Apr 18, 2026
fb03997
feat(ui): add Button, Input, Label, Textarea components and update pa…
tomast1337 Apr 18, 2026
05e5a2a
refactor(validation): update PageQuery DTO to use enum for order field
tomast1337 Apr 21, 2026
e34b7ff
refactor(song): enhance song preview handling and introduce SongPrevi…
tomast1337 Apr 21, 2026
57ad783
refactor(imports): update imports from @nbw/database to @nbw/validation
tomast1337 Apr 21, 2026
e9db4ce
refactor(song-search): enhance song search functionality and introduc…
tomast1337 Apr 21, 2026
df0e319
docs: Clarifying stripInvalidZodMarkersFromParameters function
tomast1337 Apr 21, 2026
b41352d
refactor(validation): replace config-shim imports with direct imports…
tomast1337 Apr 21, 2026
844c491
refactor(user): update user retrieval to support pagination and filte…
tomast1337 Apr 21, 2026
d042506
refactor(user): simplify user index query handling
tomast1337 Apr 21, 2026
7c3ec5a
refactor(song): update song entity and DTO to enforce maximum length …
tomast1337 Apr 21, 2026
acbe421
Revert "feat(ui): add Button, Input, Label, Textarea components and u…
tomast1337 Apr 21, 2026
cbcb79f
Revert "feat: user profiles"
tomast1337 Apr 21, 2026
83f6d47
refactor(imports): standardize import paths by removing file extensions
tomast1337 Apr 21, 2026
b2d4c62
split assignment and return to separate files
tomast1337 Apr 21, 2026
358df50
refactor(validation): remove mongoose dependency and update import paths
tomast1337 Apr 21, 2026
1e7eea7
refactor(imports): update import path for jsonStringField in UploadSo…
tomast1337 Apr 21, 2026
d72d3ad
feat(admin): bootstrap Bun-native fullstack admin app foundation
tomast1337 Apr 21, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions apps/admin/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
NODE_ENV=development
ADMIN_APP_HOST=0.0.0.0
ADMIN_APP_PORT=4010

MONGO_URL=mongodb://localhost:27017/noteblockworld

S3_ENDPOINT=http://localhost:9000
S3_BUCKET_SONGS=noteblockworld-songs
S3_BUCKET_THUMBS=noteblockworld-thumbs
S3_KEY=minioadmin
S3_SECRET=minioadmin
S3_REGION=us-east-1
14 changes: 14 additions & 0 deletions apps/admin/components.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"tsx": true,
"tailwind": {
"css": "src/web/styles.css",
"baseColor": "zinc",
"cssVariables": false
},
"aliases": {
"components": "@admin-web/components",
"utils": "@admin-web/lib"
}
}
38 changes: 38 additions & 0 deletions apps/admin/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"name": "@nbw/admin",
"version": "0.0.1",
"private": true,
"type": "module",
"scripts": {
"build": "NODE_ENV=production bun run scripts/build.ts",
"dev": "bun run build && bun --watch run src/index.ts",
"start": "bun run dist/server/index.js",
"lint": "eslint \"src/**/*.{ts,tsx}\" --fix",
"typecheck": "tsc -p tsconfig.json --noEmit"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.946.0",
"@aws-sdk/s3-request-presigner": "^3.946.0",
"@nbw/config": "workspace:*",
"@nbw/database": "workspace:*",
"@nbw/validation": "workspace:*",
"@radix-ui/react-slot": "^1.2.4",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.556.0",
"mongoose": "^9.0.1",
"react": "^19.2.1",
"react-dom": "^19.2.1",
"tailwind-merge": "^3.4.0",
"zod": "^4.1.13"
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.18",
"@types/bun": "^1.3.4",
"@types/react": "^19.2.7",
"@types/react-dom": "^19.2.3",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"typescript": "^5.9.3"
}
}
7 changes: 7 additions & 0 deletions apps/admin/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const config = {
plugins: {
'@tailwindcss/postcss': {},
},
};

export default config;
84 changes: 84 additions & 0 deletions apps/admin/scripts/build.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import { $ } from 'bun';

import tailwindcss from '@tailwindcss/postcss';
import postcss from 'postcss';

const DIST_DIR = './dist';
const PUBLIC_DIR = `${DIST_DIR}/public`;
const SERVER_DIR = `${DIST_DIR}/server`;

async function buildStyles() {
const sourceCss = await Bun.file('./src/web/styles.css').text();
const output = await postcss([tailwindcss()]).process(sourceCss, {
from: './src/web/styles.css',
to: `${PUBLIC_DIR}/styles.css`,
});

await Bun.write(`${PUBLIC_DIR}/styles.css`, output.css);
}

async function buildSpa() {
const result = await Bun.build({
entrypoints: ['./src/web/main.tsx'],
outdir: PUBLIC_DIR,
target: 'browser',
format: 'esm',
splitting: false,
minify: false,
sourcemap: 'linked',
});

if (!result.success) {
throw new Error(
`SPA build failed: ${result.logs.map((log) => log.message).join('\n')}`,
);
}

await Bun.write(
`${PUBLIC_DIR}/index.html`,
await Bun.file('./src/web/index.html').text(),
);
}

async function buildServer() {
const result = await Bun.build({
entrypoints: ['./src/index.ts'],
outdir: SERVER_DIR,
target: 'bun',
format: 'esm',
splitting: false,
minify: false,
sourcemap: 'linked',
external: ['@nbw/database', '@nbw/config', '@nbw/validation'],
});

if (!result.success) {
throw new Error(
`Server build failed: ${result.logs
.map((log) => log.message)
.join('\n')}`,
);
}
}

async function buildAll() {
await $`rm -rf ${DIST_DIR}`;
await $`mkdir -p ${PUBLIC_DIR} ${SERVER_DIR}`;

await Promise.all([buildStyles(), buildSpa(), buildServer()]);
}

if (process.argv.includes('--watch')) {
throw new Error(
'Watch mode is handled by `bun --watch run src/index.ts` in `bun run dev`.',
);
}

buildAll()
.then(() => {
process.stdout.write('Built admin app successfully.\n');
})
.catch((error) => {
process.stderr.write(`${String(error)}\n`);
process.exit(1);
});
91 changes: 91 additions & 0 deletions apps/admin/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import path from 'path';

import { connectDatabase } from '@admin/db/connect';
import { createDatabaseModels } from '@admin/db/models';
import { parseEnvironment } from '@admin/env';
import { routeApiRequest } from '@admin/http/router';
import { type ServiceContext } from '@admin/services/context';
import { createS3StorageClient } from '@admin/storage/s3-client';

const env = parseEnvironment(process.env);

await connectDatabase(env.MONGO_URL);

const models = createDatabaseModels();
const storage = createS3StorageClient(env);
await storage.verifyBuckets();

const context: ServiceContext = {
env,
models,
storage,
startedAt: Date.now(),
};

async function resolvePublicRoot() {
const candidates = [
path.resolve(import.meta.dir, '../public'),
path.resolve(import.meta.dir, '../dist/public'),
];

for (const candidate of candidates) {
if (await Bun.file(path.join(candidate, 'index.html')).exists()) {
return candidate;
}
}

return candidates[0];
}

const publicRoot = await resolvePublicRoot();

function safeResolvePublicPath(urlPath: string) {
const normalized = path.normalize(urlPath).replace(/^(\.\.(\/|\\|$))+/, '');
const fullPath = path.resolve(publicRoot, `.${normalized}`);

if (!fullPath.startsWith(publicRoot)) {
return null;
}

return fullPath;
}

async function serveStatic(request: Request) {
const url = new URL(request.url);
const pathname = url.pathname === '/' ? '/index.html' : url.pathname;
const filePath = safeResolvePublicPath(pathname);

if (filePath) {
const file = Bun.file(filePath);
if (await file.exists()) {
return new Response(file);
}
}

const spaFallback = Bun.file(path.join(publicRoot, 'index.html'));
if (await spaFallback.exists()) {
return new Response(spaFallback);
}

return new Response('index.html was not found. Run `bun run build` first.', {
status: 500,
});
}

Bun.serve({
hostname: env.ADMIN_APP_HOST,
port: env.ADMIN_APP_PORT,
async fetch(request) {
const url = new URL(request.url);

if (url.pathname.startsWith('/api/')) {
return routeApiRequest(request, context);
}

return serveStatic(request);
},
});

process.stdout.write(
`@nbw/admin listening on http://${env.ADMIN_APP_HOST}:${env.ADMIN_APP_PORT}\n`,
);
5 changes: 5 additions & 0 deletions apps/admin/src/server/db/connect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import mongoose from 'mongoose';

export async function connectDatabase(mongoUrl: string) {
await mongoose.connect(mongoUrl);
}
15 changes: 15 additions & 0 deletions apps/admin/src/server/db/models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import mongoose from 'mongoose';

import { Song, SongSchema, User, UserSchema } from '@nbw/database';

export function createDatabaseModels() {
const songs =
mongoose.models[Song.name] ?? mongoose.model(Song.name, SongSchema);

const users =
mongoose.models[User.name] ?? mongoose.model(User.name, UserSchema);

return { songs, users };
}

export type DatabaseModels = ReturnType<typeof createDatabaseModels>;
29 changes: 29 additions & 0 deletions apps/admin/src/server/env.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { z } from 'zod';

const envSchema = z.object({
NODE_ENV: z.enum(['development', 'production']).default('development'),
ADMIN_APP_HOST: z.string().default('0.0.0.0'),
ADMIN_APP_PORT: z.coerce.number().int().positive().default(4010),
MONGO_URL: z.string().min(1),
S3_ENDPOINT: z.string().min(1),
S3_BUCKET_SONGS: z.string().min(1),
S3_BUCKET_THUMBS: z.string().min(1),
S3_KEY: z.string().min(1),
S3_SECRET: z.string().min(1),
S3_REGION: z.string().min(1),
});

export type AdminEnvironment = z.output<typeof envSchema>;

export function parseEnvironment(source: Record<string, string | undefined>) {
const result = envSchema.safeParse(source);

if (!result.success) {
const messages = result.error.issues
.map((issue) => `${issue.path.join('.')}: ${issue.message}`)
.join('\n');
throw new Error(`Admin environment validation failed:\n${messages}`);
}

return result.data;
}
23 changes: 23 additions & 0 deletions apps/admin/src/server/http/json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export function json(
payload: unknown,
init: Omit<ResponseInit, 'headers'> & {
headers?: HeadersInit;
} = {},
) {
return new Response(JSON.stringify(payload), {
...init,
headers: {
'content-type': 'application/json; charset=utf-8',
...init.headers,
},
});
}

export function notFound() {
return json(
{
error: 'Not Found',
},
{ status: 404 },
);
}
34 changes: 34 additions & 0 deletions apps/admin/src/server/http/router.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { notFound } from '@admin/http/json';
import { handleHealthRequest } from '@admin/routes/health.route';
import {
handleDashboardStatsRequest,
handleSongsRequest,
} from '@admin/routes/songs.route';
import { handleUsersRequest } from '@admin/routes/users.route';
import type { ServiceContext } from '@admin/services/context';

type RouteHandler = (
request: Request,
context: ServiceContext,
) => Promise<Response>;

const routeTable = new Map<string, RouteHandler>([
['/api/health', async (_request, context) => handleHealthRequest(context)],
['/api/users', handleUsersRequest],
['/api/songs', handleSongsRequest],
['/api/dashboard/stats', handleDashboardStatsRequest],
]);

export async function routeApiRequest(
request: Request,
context: ServiceContext,
): Promise<Response> {
const url = new URL(request.url);
const handler = routeTable.get(url.pathname);

if (!handler) {
return notFound();
}

return handler(request, context);
}
10 changes: 10 additions & 0 deletions apps/admin/src/server/routes/health.route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { json } from '@admin/http/json';
import type { ServiceContext } from '@admin/services/context';

export async function handleHealthRequest(context: ServiceContext) {
return json({
ok: true,
uptimeMs: Date.now() - context.startedAt,
env: context.env.NODE_ENV,
});
}
Loading
Loading