Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 39 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# Repository Guidelines

## Project Structure & Module Organization

- Monorepo managed with pnpm + Turbo. Backend: `backend/` (NestJS app in `src/`, migrations in `src/migrations`, tests in `test/`). Frontend: `frontend/` (React/Vite code in `src/`).
- Shared tooling at root (`turbo.json`, `pnpm-workspace.yaml`, `tsconfig.json`, husky hooks); infra assets live in `docker-compose.yml`, `k8s/`, and package `Dockerfile`s.
- Copy env templates before running services: `backend/.env.example`, `frontend/.env.example`.

## Build, Test, and Development Commands

- Install once at root: `pnpm install`.
- Develop both apps with `pnpm dev`; scope to a package via `pnpm --filter backend dev` or `pnpm --filter frontend dev`.
- Build with `pnpm build` or scoped `pnpm --filter <pkg> build`.
- Tests: `pnpm test` (all), `pnpm --filter backend test`, `pnpm --filter backend test:e2e`, `pnpm --filter backend test:cov`.
- Lint/format/typecheck: `pnpm lint`, `pnpm format`, `pnpm typecheck`. Husky + lint-staged auto-format backend TS on commit.
- Local data stack: `docker-compose up -d` (Postgres 5433, Redis 6379); run `pnpm --filter backend migration:run` after services are healthy.

## Coding Style & Naming Conventions

- TypeScript everywhere; follow ESLint configs (`backend/eslint.config.js`, `frontend/.eslintrc.cjs`) and Prettier defaults (2-space indent).
- Naming: `camelCase` for vars/functions, `PascalCase` for classes/components, `SCREAMING_SNAKE_CASE` for constants/envs. Backend follows Nest patterns (`*.module.ts`, `*.service.ts`, `*.controller.ts`); React components live one per file.
- Prefer typed DTOs/interfaces; use async/await with explicit HTTP exceptions and validation.

## Testing Guidelines

- Backend: Jest specs under `backend/test` or alongside code as `*.spec.ts`; E2E uses `backend/test/jest-e2e.json`. Cover new endpoints/services and add E2E for auth or database flows.
- Frontend: use React Testing Library; place specs next to components as `*.test.tsx`.
- Aim for passing coverage check via `pnpm --filter backend test:cov`; mock external calls in unit tests and reserve real integrations for E2E.

## Commit & Pull Request Guidelines

- Use concise, imperative commits; conventional prefixes (`feat:`, `fix:`, `ci:`) match existing history. One change-set per commit.
- Before a PR: ensure `pnpm lint`, `pnpm test`, and relevant builds succeed; call out migrations or breaking changes.
- PRs should include a short summary, linked issue, and testing notes. Add screenshots/GIFs for UI updates and flag security-sensitive changes (auth, tokens, RBAC).

## Security & Configuration Tips

- Keep secrets out of Git; rely on the env examples and local overrides. Rotate `JWT_SECRET` and database creds outside local dev.
- Confirm CORS + HTTPS settings before deploying. For data debugging, prefer `docker-compose logs -f` and `pnpm --filter backend migration:revert` instead of manual DB edits.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,9 +164,13 @@ Run both frontend and backend in development mode:
# From root directory
pnpm dev

# Or individually:
cd backend && pnpm dev # Backend on http://localhost:3001
cd frontend && pnpm dev # Frontend on http://localhost:5173
# Or individually from root:
pnpm dev:backend # Backend on http://localhost:3001
pnpm dev:frontend # Frontend on http://localhost:5173

# Or from package directories:
cd backend && pnpm dev # Backend only
cd frontend && pnpm dev # Frontend only
```

**Note**: Redis is optional. The application will fall back to in-memory caching if Redis is unavailable.
Expand Down
1 change: 1 addition & 0 deletions backend/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ DATABASE_NAME=stationDb
JWT_SECRET=test-jwt-secret-for-e2e-tests-only
PORT=3000
APP_NAME=STATION BACKEND TEST
USE_REDIS_CACHE=false
2 changes: 1 addition & 1 deletion backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json",
"test:e2e": "jest --config ./test/jest-e2e.json --runInBand",
"clean": "rm -rf dist"
},
"dependencies": {
Expand Down
14 changes: 14 additions & 0 deletions backend/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ import { AuditLogsModule } from './modules/audit-logs/audit-logs.module';
isGlobal: true,
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => {
const useRedis =
configService.get<string>('USE_REDIS_CACHE', 'true') === 'true';

if (!useRedis) {
return {
store: 'memory',
ttl: 300000,
max: 100,
// Disable interval cleanup to avoid open handles in tests
checkperiod: 0,
isCacheableValue: () => true,
};
}

try {
const store = await redisStore({
socket: {
Expand Down
4 changes: 4 additions & 0 deletions backend/src/data-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,14 @@ import { Organization } from './modules/organizations/organization.entity';
import { Role } from './modules/roles/role.entity';
import { UserOrganizationRole } from './modules/user-organization-roles/user-organization-role.entity';
import { RefreshToken } from './modules/auth/refresh-token.entity';
import { PasswordReset } from './modules/auth/password-reset.entity';
import { AuditLog } from './modules/audit-logs/audit-log.entity';
import { CreateUsersTable1716956654528 } from './migrations/1716956654528-CreateUsersTable';
import { CreateOrganizationsRolesAndJunctionTable1730841000000 } from './migrations/1730841000000-CreateOrganizationsRolesAndJunctionTable';
import { CreateAuditLogsTable1730900000000 } from './migrations/1730900000000-CreateAuditLogsTable';
import { CreateRefreshTokenTable1731715200000 } from './migrations/1731715200000-CreateRefreshTokenTable';
import { AddUserProfileFields1732000000000 } from './migrations/1732000000000-AddUserProfileFields';
import { CreatePasswordResetsTable1732050000000 } from './migrations/1732050000000-CreatePasswordResetsTable';

export const AppDataSource = new DataSource({
type: 'postgres',
Expand All @@ -26,6 +28,7 @@ export const AppDataSource = new DataSource({
Role,
UserOrganizationRole,
RefreshToken,
PasswordReset,
AuditLog,
],
migrations: [
Expand All @@ -34,6 +37,7 @@ export const AppDataSource = new DataSource({
CreateAuditLogsTable1730900000000,
CreateRefreshTokenTable1731715200000,
AddUserProfileFields1732000000000,
CreatePasswordResetsTable1732050000000,
],
synchronize: false,
});
Expand Down
31 changes: 31 additions & 0 deletions backend/src/database/seeds/database-seeder.service.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Logger } from '@nestjs/common';
import { Repository } from 'typeorm';
import { DatabaseSeederService } from './database-seeder.service';
import { Role } from '../../modules/roles/role.entity';
Expand All @@ -18,6 +19,9 @@ describe('DatabaseSeederService', () => {
let organizationsRepository: Repository<Organization>;
let usersRepository: Repository<User>;
let userOrgRolesRepository: Repository<UserOrganizationRole>;
let loggerLogSpy: jest.SpyInstance;
let loggerWarnSpy: jest.SpyInstance;
let loggerErrorSpy: jest.SpyInstance;

const mockRole = {
id: 1,
Expand Down Expand Up @@ -48,7 +52,29 @@ describe('DatabaseSeederService', () => {
roleId: 1,
};

beforeAll(() => {
loggerLogSpy = jest
.spyOn(Logger.prototype, 'log')
.mockImplementation(() => undefined);
loggerWarnSpy = jest
.spyOn(Logger.prototype, 'warn')
.mockImplementation(() => undefined);
loggerErrorSpy = jest
.spyOn(Logger.prototype, 'error')
.mockImplementation(() => undefined);
});

afterAll(() => {
loggerLogSpy.mockRestore();
loggerWarnSpy.mockRestore();
loggerErrorSpy.mockRestore();
});

beforeEach(async () => {
loggerLogSpy.mockClear();
loggerWarnSpy.mockClear();
loggerErrorSpy.mockClear();

const module: TestingModule = await Test.createTestingModule({
providers: [
DatabaseSeederService,
Expand Down Expand Up @@ -155,6 +181,11 @@ describe('DatabaseSeederService', () => {
.mockRejectedValue(new Error('Database error'));

await expect(service.seedAll()).rejects.toThrow('Database error');

expect(loggerErrorSpy).toHaveBeenCalledWith(
'❌ Database seeding failed:',
expect.any(Error),
);
});
});
});
4 changes: 0 additions & 4 deletions backend/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import * as figlet from 'figlet';
import * as dotenv from 'dotenv';
import { HttpExceptionFilter } from './common/filters/http-exception.filter';
import { TransformInterceptor } from './common/interceptors/transform.interceptor';

dotenv.config();

Expand Down Expand Up @@ -35,9 +34,6 @@ async function bootstrap() {
// Global Exception Filter for standardized error responses
app.useGlobalFilters(new HttpExceptionFilter());

// Global Response Transform Interceptor for standardized success responses
app.useGlobalInterceptors(new TransformInterceptor());

// Swagger/OpenAPI Documentation Setup
const config = new DocumentBuilder()
.setTitle('Station API')
Expand Down
83 changes: 83 additions & 0 deletions backend/src/migrations/1732050000000-CreatePasswordResetsTable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {
MigrationInterface,
QueryRunner,
Table,
TableForeignKey,
} from 'typeorm';

export class CreatePasswordResetsTable1732050000000
implements MigrationInterface
{
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.createTable(
new Table({
name: 'password_resets',
columns: [
{
name: 'id',
type: 'int',
isPrimary: true,
isGenerated: true,
generationStrategy: 'increment',
},
{
name: 'userId',
type: 'int',
isNullable: false,
},
{
name: 'token',
type: 'varchar',
length: '255',
isUnique: true,
isNullable: false,
},
{
name: 'expiresAt',
type: 'timestamp',
isNullable: false,
},
{
name: 'used',
type: 'boolean',
default: false,
isNullable: false,
},
{
name: 'createdAt',
type: 'timestamp',
default: 'CURRENT_TIMESTAMP',
isNullable: false,
},
],
}),
true,
);

await queryRunner.createForeignKey(
'password_resets',
new TableForeignKey({
columnNames: ['userId'],
referencedColumnNames: ['id'],
referencedTableName: 'user',
onDelete: 'CASCADE',
}),
);

// Create index for faster lookups
await queryRunner.query(
`CREATE INDEX "IDX_password_resets_token" ON "password_resets" ("token")`,
);
}

public async down(queryRunner: QueryRunner): Promise<void> {
const table = await queryRunner.getTable('password_resets');
const foreignKey = table?.foreignKeys.find(
(fk) => fk.columnNames.indexOf('userId') !== -1,
);
if (foreignKey) {
await queryRunner.dropForeignKey('password_resets', foreignKey);
}
await queryRunner.dropTable('password_resets');
}
}
Loading