Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
79ac817
refactor: remove DTOs
tomast1337 Jan 11, 2026
1673d0a
feat: add validation package with DTOs
tomast1337 Jan 11, 2026
fcb2c9f
feat: integrate @nbw/validation package into project structure
tomast1337 Jan 11, 2026
054e778
feat(validation): migrate to Zod in @nbw/validation; drop class-valid…
tomast1337 Apr 13, 2026
c92d9cf
feat(validation): enhance JSON string handling in UploadSongDto schema
tomast1337 Apr 14, 2026
91dca25
docs(validation): update README to reflect package purpose and usage …
tomast1337 Apr 14, 2026
6f372eb
chore(validation): remove jest configuration file
tomast1337 Apr 14, 2026
e76083b
refactor(validation): drop types.ts barrels; add uploadMeta and use D…
tomast1337 Apr 14, 2026
b007eba
feat: user profiles
tomast1337 Apr 18, 2026
5081308
feat(ui): add Button, Input, Label, Textarea components and update pa…
tomast1337 Apr 18, 2026
2dbbb60
refactor(validation): update PageQuery DTO to use enum for order field
tomast1337 Apr 21, 2026
41b0e78
refactor(song): enhance song preview handling and introduce SongPrevi…
tomast1337 Apr 21, 2026
d2039b8
refactor(imports): update imports from @nbw/database to @nbw/validation
tomast1337 Apr 21, 2026
066ea50
refactor(song-search): enhance song search functionality and introduc…
tomast1337 Apr 21, 2026
2cb4ec0
docs: Clarifying stripInvalidZodMarkersFromParameters function
tomast1337 Apr 21, 2026
e6308dc
refactor(validation): replace config-shim imports with direct imports…
tomast1337 Apr 21, 2026
1d57cdf
refactor(user): update user retrieval to support pagination and filte…
tomast1337 Apr 21, 2026
70747a6
refactor(user): simplify user index query handling
tomast1337 Apr 21, 2026
a6a61ce
refactor(song): update song entity and DTO to enforce maximum length …
tomast1337 Apr 21, 2026
6947062
Revert "feat(ui): add Button, Input, Label, Textarea components and u…
tomast1337 Apr 21, 2026
e2c843e
Revert "feat: user profiles"
tomast1337 Apr 21, 2026
2eb5358
refactor(imports): standardize import paths by removing file extensions
tomast1337 Apr 21, 2026
ac67d3b
split assignment and return to separate files
tomast1337 Apr 21, 2026
170e118
refactor(validation): remove mongoose dependency and update import paths
tomast1337 Apr 21, 2026
8857558
refactor(imports): update import path for jsonStringField in UploadSo…
tomast1337 Apr 21, 2026
0bc3436
feat(backend): deterministic seed API and safer user updates for dev …
tomast1337 Apr 23, 2026
0ee293f
feat: add Cypress e2e app and tighten local Docker + seed workflow
tomast1337 Apr 23, 2026
fc6f441
feat(auth): add e2e session endpoint for Cypress integration
tomast1337 Apr 23, 2026
750e464
feat(e2e): enhance Cypress configuration and add baseline snapshot fu…
tomast1337 Apr 23, 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
33 changes: 24 additions & 9 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,22 +21,37 @@ You'll need the following installed on your machine:
- [Docker](https://www.docker.com/)
- [Docker Compose](https://docs.docker.com/compose/)

We provide a `docker-compose-dev.yml` file that sets up:
We provide [`docker-compose.yml`](docker-compose.yml) at the repository root. It defines:

- A MongoDB instance
- A local mail server (`maildev`)
- An S3-compatible storage (`minio`)
- A MinIO client
- **MongoDB**
- **MailDev** (SMTP + web UI for local email)
- **MinIO** (S3-compatible storage)
- A **MinIO client** job (profile `minio-init`) that creates buckets and CORS—**not** started by a plain `docker compose up`

To start the services, run the following in the root directory:
Start dependencies from the repo root in one of these ways:

**Recommended** (waits for MongoDB and MinIO to be healthy, then runs bucket setup):

```bash
bun run docker:up
```

**Manual** (then create buckets once; uploads will fail with `NoSuchBucket` until you do):

```bash
docker compose up -d
bun run docker:minio-init
```

To tear down volumes and start clean (still runs MinIO init after `up --wait`):

```bash
docker-compose -f docker-compose.yml up -d
bun run docker:reset:fresh
```

> Remove the `-d` flag if you'd like to see container logs in your terminal.
> Drop `-d` on `docker compose up -d` if you prefer logs attached to your terminal.

You can find authentication details in the [`docker-compose.yml`](docker-compose.yml) file.
Ports and default credentials (Mongo, MinIO, MailDev) are in [`docker-compose.yml`](docker-compose.yml). Match the MinIO bucket names and keys in your backend `.env` (see the example block under **Environment Variables** below).

---

Expand Down
13 changes: 13 additions & 0 deletions apps/backend/.env.development.example
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
NODE_ENV=

# Optional: non-empty value enables `POST /v1/auth/e2e/session` (dev only) for Cypress —
# same value as Cypress env `E2E_AUTH_SECRET` / `CYPRESS_E2E_AUTH_SECRET`.
E2E_AUTH_SECRET=

GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=

Expand All @@ -23,6 +27,15 @@ APP_DOMAIN=

RECAPTCHA_KEY=

# MinIO from repo docker-compose: after `docker compose up -d` (or `bun run docker:up`),
# create buckets once with `bun run docker:minio-init` from the monorepo root (see root package.json).
# Example local values (match docker-compose mc bucket names):
# 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
S3_ENDPOINT=
S3_BUCKET_SONGS=
S3_BUCKET_THUMBS=
Expand Down
3 changes: 2 additions & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@encode42/nbs.js": "^5.0.2",
"@nbw/config": "workspace:*",
"@nbw/database": "workspace:*",
"@nbw/validation": "workspace:*",
"@nbw/song": "workspace:*",
"@nbw/sounds": "workspace:*",
"@nbw/thumbnail": "workspace:*",
Expand All @@ -44,10 +45,10 @@
"axios": "^1.13.2",
"bcryptjs": "^3.0.3",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.3",
"esm": "^3.2.25",
"express": "^5.2.1",
"mongoose": "^9.0.1",
"nestjs-zod": "^5.0.1",
"multer": "2.1.1",
"nanoid": "^5.1.6",
"passport": "^0.7.0",
Expand Down
6 changes: 3 additions & 3 deletions apps/backend/scripts/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,6 @@ const build = async () => {
await Bun.$`rm -rf dist`;

const optionalRequirePackages = [
'class-transformer',
'class-transformer/storage',
'class-validator',
'@nestjs/microservices',
'@nestjs/websockets',
'@fastify/static',
Expand All @@ -76,8 +73,11 @@ const build = async () => {
}),
'@nbw/config',
'@nbw/database',
'@nbw/validation',
'@nbw/song',
'@nbw/sounds',
// @nestjs/swagger → @nestjs/mapped-types requires class-transformer metadata storage; bundler mis-resolves subpaths
'class-transformer',
],
splitting: true,
});
Expand Down
11 changes: 8 additions & 3 deletions apps/backend/src/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Logger, Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { APP_GUARD } from '@nestjs/core';
import { APP_GUARD, APP_PIPE } from '@nestjs/core';
import { MongooseModule, MongooseModuleFactoryOptions } from '@nestjs/mongoose';
import { ThrottlerGuard, ThrottlerModule } from '@nestjs/throttler';
import { ZodValidationPipe } from 'nestjs-zod';
import { MailerModule } from '@nestjs-modules/mailer';
import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handlebars.adapter';

import { AuthModule } from './auth/auth.module';
import { validate } from './config/EnvironmentVariables';
import { validateEnv } from '@nbw/validation';
import { EmailLoginModule } from './email-login/email-login.module';
import { FileModule } from './file/file.module';
import { ParseTokenPipe } from './lib/parseToken';
Expand All @@ -21,7 +22,7 @@ import { UserModule } from './user/user.module';
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env.test', '.env.development', '.env.production'],
validate,
validate: validateEnv,
}),
//DatabaseModule,
MongooseModule.forRootAsync({
Expand Down Expand Up @@ -82,6 +83,10 @@ import { UserModule } from './user/user.module';
controllers: [],
providers: [
ParseTokenPipe,
{
provide: APP_PIPE,
useClass: ZodValidationPipe,
},
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
Expand Down
132 changes: 132 additions & 0 deletions apps/backend/src/auth/auth.controller.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { BadRequestException, NotFoundException } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import type { Request, Response } from 'express';

import { UserService } from '@server/user/user.service';

import { AuthController } from './auth.controller';
import { AuthService } from './auth.service';
import { MagicLinkEmailStrategy } from './strategies/magicLinkEmail.strategy';
Expand All @@ -11,6 +15,16 @@ const mockAuthService = {
discordLogin: jest.fn(),
verifyToken: jest.fn(),
loginWithEmail: jest.fn(),
issueSessionTokensForUser: jest.fn(),
};

const mockUserService = {
findByEmail: jest.fn(),
findByID: jest.fn(),
};

const mockConfigService = {
get: jest.fn(),
};

const mockMagicLinkEmailStrategy = {
Expand All @@ -36,6 +50,8 @@ describe('AuthController', () => {
provide: MagicLinkEmailStrategy,
useValue: mockMagicLinkEmailStrategy,
},
{ provide: UserService, useValue: mockUserService },
{ provide: ConfigService, useValue: mockConfigService },
],
}).compile();

Expand Down Expand Up @@ -192,4 +208,120 @@ describe('AuthController', () => {
expect(authService.verifyToken).toHaveBeenCalledWith(req, res);
});
});

describe('e2eSession', () => {
it('returns 404 when not in development', async () => {
mockConfigService.get.mockImplementation((key: string) => {
if (key === 'NODE_ENV') return 'production';
if (key === 'E2E_AUTH_SECRET') return 'secret';
return undefined;
});

await expect(
controller.e2eSession('secret', { email: 'a@b.c' }),
).rejects.toBeInstanceOf(NotFoundException);
});

it('returns 404 when E2E_AUTH_SECRET is empty', async () => {
mockConfigService.get.mockImplementation((key: string) => {
if (key === 'NODE_ENV') return 'development';
if (key === 'E2E_AUTH_SECRET') return '';
return undefined;
});

await expect(
controller.e2eSession('secret', { email: 'a@b.c' }),
).rejects.toBeInstanceOf(NotFoundException);
});

it('returns 404 when header secret is wrong', async () => {
mockConfigService.get.mockImplementation((key: string) => {
if (key === 'NODE_ENV') return 'development';
if (key === 'E2E_AUTH_SECRET') return 'good';
return undefined;
});

await expect(
controller.e2eSession('bad', { email: 'a@b.c' }),
).rejects.toBeInstanceOf(NotFoundException);
});

it('returns 400 when both email and userId are provided', async () => {
mockConfigService.get.mockImplementation((key: string) => {
if (key === 'NODE_ENV') return 'development';
if (key === 'E2E_AUTH_SECRET') return 's';
return undefined;
});

await expect(
controller.e2eSession('s', { email: 'a@b.c', userId: 'id' }),
).rejects.toBeInstanceOf(BadRequestException);
});

it('returns 400 when neither email nor userId', async () => {
mockConfigService.get.mockImplementation((key: string) => {
if (key === 'NODE_ENV') return 'development';
if (key === 'E2E_AUTH_SECRET') return 's';
return undefined;
});

await expect(controller.e2eSession('s', {})).rejects.toBeInstanceOf(
BadRequestException,
);
});

it('returns tokens for existing user by email', async () => {
mockConfigService.get.mockImplementation((key: string) => {
if (key === 'NODE_ENV') return 'development';
if (key === 'E2E_AUTH_SECRET') return 's';
return undefined;
});
const user = { _id: 'u1', email: 'e@e.com', username: 'u' };
mockUserService.findByEmail.mockResolvedValueOnce(user);
mockAuthService.issueSessionTokensForUser.mockResolvedValueOnce({
access_token: 'a',
refresh_token: 'r',
});

const out = await controller.e2eSession('s', { email: 'e@e.com' });

expect(out).toEqual({ access_token: 'a', refresh_token: 'r' });
expect(mockUserService.findByEmail).toHaveBeenCalledWith('e@e.com');
expect(mockAuthService.issueSessionTokensForUser).toHaveBeenCalledWith(
user,
);
});

it('returns tokens for existing user by userId', async () => {
mockConfigService.get.mockImplementation((key: string) => {
if (key === 'NODE_ENV') return 'development';
if (key === 'E2E_AUTH_SECRET') return 's';
return undefined;
});
const user = { _id: 'u1', email: 'e@e.com', username: 'u' };
mockUserService.findByID.mockResolvedValueOnce(user);
mockAuthService.issueSessionTokensForUser.mockResolvedValueOnce({
access_token: 'a',
refresh_token: 'r',
});

const out = await controller.e2eSession('s', { userId: 'abc' });

expect(out).toEqual({ access_token: 'a', refresh_token: 'r' });
expect(mockUserService.findByID).toHaveBeenCalledWith('abc');
});

it('returns 404 when user is not found', async () => {
mockConfigService.get.mockImplementation((key: string) => {
if (key === 'NODE_ENV') return 'development';
if (key === 'E2E_AUTH_SECRET') return 's';
return undefined;
});
mockUserService.findByEmail.mockResolvedValueOnce(null);

await expect(
controller.e2eSession('s', { email: 'missing@x.com' }),
).rejects.toBeInstanceOf(NotFoundException);
});
});
});
Loading
Loading