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
15 changes: 12 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Need full history for merge-base
fetch-depth: 0 # Need full history for merge-base

- name: Get merge-base SHA
id: merge-base
Expand Down Expand Up @@ -140,7 +140,16 @@ jobs:
if: steps.cache-node-modules.outputs.cache-hit != 'true' || steps.validate-cache.outputs.valid == 'false'
run: npm ci

- name: Build (includes type checking)
- name: Type check
run: npm run typecheck

- name: Lint
run: npm run lint

- name: Check formatting
run: npm run format:check

- name: Build
run: npm run build

# Unit tests - runs affected tests only
Expand All @@ -152,7 +161,7 @@ jobs:
- name: Checkout Code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Need full history for --changedSince
fetch-depth: 0 # Need full history for --changedSince

- name: Set Up Node.js
uses: actions/setup-node@v4
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ dist
mirror-logs
.env
.DS_STORE
shared/authentication/tsconfig.tsbuildinfo
*.tsbuildinfo
5 changes: 5 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
dist
coverage
node_modules
shared/database/src/migrations
*.sql
6 changes: 6 additions & 0 deletions .prettierrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 120,
"tabWidth": 2
}
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,14 @@ The dev experience makes extensive use of Node.js project scripts. Here's a rund
- `npm run drizzle:migrate` : Applies the generated migrations to the database specified by the environment variables `DB_HOST`, `DB_NAME`, and `DB_PORT`. It also requires `DB_USERNAME` and `DB_PASSWORD`.
- `npm run drizzle:drop` : Deletes a given migration file from the migrations directory and removes it from the drizzle cache.

#### Code Quality

- `npm run typecheck` : Runs `tsc --noEmit` across all workspaces to verify type safety without emitting files.
- `npm run lint` : Runs ESLint with TypeScript type-checked rules and security analysis.
- `npm run lint:fix` : Runs ESLint with auto-fix enabled.
- `npm run format` : Formats all files with Prettier.
- `npm run format:check` : Verifies all files match Prettier formatting (used in CI).

#### Environment Variables

Here is an example environment variable file. Create a file with these contents named `.env` in the root of your locally cloned project to ensure your dev environment works properly.
Expand Down
35 changes: 11 additions & 24 deletions apps/auth/app.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
// Auth service using Express
import { config } from 'dotenv';
const dotenvResult = config();

Check warning on line 3 in apps/auth/app.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

'dotenvResult' is assigned a value but never used

import { auth } from '@wxyc/authentication';
import { toNodeHandler } from 'better-auth/node';
Expand Down Expand Up @@ -44,37 +44,28 @@
const { eq, desc, like, and } = await import('drizzle-orm');

// First, look up the user by email to get their userId
const userResult = await db
.select({ id: user.id })
.from(user)
.where(eq(user.email, identifier))
.limit(1);
const userResult = await db.select({ id: user.id }).from(user).where(eq(user.email, identifier)).limit(1);

if (userResult.length === 0) {
return res.status(404).json({ error: 'User not found with this email' });
}

const userId = userResult[0].id;
const tokenPrefix = `${type}:`;

Check warning on line 54 in apps/auth/app.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

Invalid type "string | ParsedQs | (string | ParsedQs)[]" of template literal expression

Check warning on line 54 in apps/auth/app.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

'type' may use Object's default stringification format ('[object Object]') when stringified
const result = await db
.select()
.from(verification)
.where(and(
eq(verification.value, userId),
like(verification.identifier, `${tokenPrefix}%`)
))
.where(and(eq(verification.value, userId), like(verification.identifier, `${tokenPrefix}%`)))
.orderBy(desc(verification.createdAt))
.limit(1);

if (result.length === 0) {
return res.status(404).json({ error: `No ${type} token found for this user` });

Check warning on line 63 in apps/auth/app.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

Invalid type "string | ParsedQs | (string | ParsedQs)[]" of template literal expression

Check warning on line 63 in apps/auth/app.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

'type' may use Object's default stringification format ('[object Object]') when stringified
}

// Extract the actual token from the identifier (e.g., "reset-password:abc123" -> "abc123")
const fullIdentifier = result[0].identifier;
const token = fullIdentifier.startsWith(tokenPrefix)
? fullIdentifier.slice(tokenPrefix.length)
: fullIdentifier;
const token = fullIdentifier.startsWith(tokenPrefix) ? fullIdentifier.slice(tokenPrefix.length) : fullIdentifier;

res.json({
token,
Expand All @@ -90,7 +81,7 @@
// Expire a user's session for testing session timeout
app.post('/auth/test/expire-session', async (req, res) => {
try {
const { userId } = req.body;

Check warning on line 84 in apps/auth/app.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

Unsafe assignment of an `any` value
if (!userId || typeof userId !== 'string') {
return res.status(400).json({ error: 'userId is required in request body' });
}
Expand All @@ -110,7 +101,9 @@
}
});

console.log('[TEST ENDPOINTS] Test helper endpoints enabled (/auth/test/verification-token, /auth/test/expire-session)');
console.log(
'[TEST ENDPOINTS] Test helper endpoints enabled (/auth/test/verification-token, /auth/test/expire-session)'
);
}

// Mount the Better Auth handler for all auth routes
Expand All @@ -125,7 +118,7 @@
const response = await fetch(`${authServiceUrl}/auth/ok`);

// Forward the status and body from the /auth/ok response
const data = await response.json(); // Assuming /auth/ok returns JSON

Check warning on line 121 in apps/auth/app.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

Unsafe assignment of an `any` value
res.status(response.status).json(data);
} catch (error) {
console.error('Error proxying /healthcheck to /auth/ok:', error);
Expand Down Expand Up @@ -207,7 +200,7 @@
},
});

organizationId = newOrganization.id;

Check warning on line 203 in apps/auth/app.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

Unsafe assignment of an `any` value
}

if (!organizationId) {
Expand All @@ -218,7 +211,7 @@
model: 'member',
where: [
{ field: 'userId', value: newUser.id },
{ field: 'organizationId', value: organizationId },

Check warning on line 214 in apps/auth/app.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

Unsafe assignment of an `any` value
],
});

Expand All @@ -230,7 +223,7 @@
model: 'member',
data: {
userId: newUser.id,
organizationId: organizationId,

Check warning on line 226 in apps/auth/app.ts

View workflow job for this annotation

GitHub Actions / lint-and-typecheck

Unsafe assignment of an `any` value
role: 'stationManager',
createdAt: new Date(),
},
Expand All @@ -240,10 +233,7 @@
// This ensures the user has admin permissions for Better Auth Admin plugin
const { db, user } = await import('@wxyc/database');
const { eq } = await import('drizzle-orm');
await db
.update(user)
.set({ role: 'admin' })
.where(eq(user.id, newUser.id));
await db.update(user).set({ role: 'admin' }).where(eq(user.id, newUser.id));

console.log('Default user created successfully with admin role.');
} catch (error) {
Expand All @@ -257,7 +247,7 @@
try {
const { db, user, member, organization } = await import('@wxyc/database');
const { eq, sql } = await import('drizzle-orm');

const defaultOrgSlug = process.env.DEFAULT_ORG_SLUG;
if (!defaultOrgSlug) {
console.log('[ADMIN PERMISSIONS] DEFAULT_ORG_SLUG not set, skipping admin role fix');
Expand Down Expand Up @@ -285,10 +275,7 @@
console.log(`[ADMIN PERMISSIONS] Found ${usersNeedingFix.length} users needing admin role fix: `);
for (const u of usersNeedingFix) {
console.log(`[ADMIN PERMISSIONS] - ${u.userEmail} (${u.memberRole}) - current role: ${u.userRole || 'null'}`);
await db
.update(user)
.set({ role: 'admin' })
.where(eq(user.id, u.userId));
await db.update(user).set({ role: 'admin' }).where(eq(user.id, u.userId));
console.log(`[ADMIN PERMISSIONS] - Fixed: ${u.userEmail} now has admin role`);
}
} else {
Expand All @@ -300,11 +287,11 @@
};

// Initialize default user and sync admin roles before starting the server
(async () => {
void (async () => {
await createDefaultUser();
await syncAdminRoles();

app.listen(parseInt(port), async () => {
app.listen(parseInt(port), () => {
console.log(`listening on port: ${port}! (auth service)`);
});
})();
Expand Down
3 changes: 2 additions & 1 deletion apps/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"build": "tsup --minify",
"clean": "rm -rf dist",
"docker:build": "docker build -t wxyc_auth_service:ci -f ../../Dockerfile.auth ../../",
"dev": "tsup --watch"
"dev": "tsup --watch",
"typecheck": "tsc --noEmit"
},
"author": "Jackson Meade",
"license": "MIT",
Expand Down
20 changes: 10 additions & 10 deletions apps/auth/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"extends": ["../../tsconfig.base.json"],
"references": [{ "path": "../../shared/authentication" }],
"include": ["."],
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@/*": ["./*"]
}
"extends": ["../../tsconfig.base.json"],
"references": [{ "path": "../../shared/authentication" }],
"include": ["."],
"compilerOptions": {
"module": "esnext",
"moduleResolution": "bundler",
"paths": {
"@/*": ["./*"]
}
}
}
}
27 changes: 10 additions & 17 deletions apps/auth/tsup.config.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,16 @@
import { defineConfig } from "tsup";
import { defineConfig } from 'tsup';

export default defineConfig((options) => ({
entry: ["app.ts"],
outDir: "dist",
format: ["esm"],
platform: "node",
target: "node20",
entry: ['app.ts'],
outDir: 'dist',
format: ['esm'],
platform: 'node',
target: 'node20',
clean: true,
sourcemap: true,
external: [
"@wxyc/database",
"better-auth",
"drizzle-orm",
"express",
"cors",
"postgres"
],
external: ['@wxyc/database', 'better-auth', 'drizzle-orm', 'express', 'cors', 'postgres'],
env: {
NODE_ENV: process.env.NODE_ENV || "development",
NODE_ENV: process.env.NODE_ENV || 'development',
},
onSuccess: options.watch ? "node ./dist/app.js" : undefined,
}));
onSuccess: options.watch ? 'node ./dist/app.js' : undefined,
}));
4 changes: 2 additions & 2 deletions apps/backend/controllers/djs.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { RequestHandler } from 'express';
import * as DJService from '../services/djs.service';
import { NewBinEntry } from "@wxyc/database";
import { NewBinEntry } from '@wxyc/database';

export type binBody = {
dj_id: string;
Expand All @@ -15,7 +15,7 @@ export const addToBin: RequestHandler<object, unknown, binBody> = async (req, re
res.status(400).send('Bad Request, Missing DJ or album identifier: album_id');
} else {
const bin_entry: NewBinEntry = {
dj_id: req.body.dj_id as string,
dj_id: req.body.dj_id,
album_id: req.body.album_id,
track_title: req.body.track_title === undefined ? null : req.body.track_title,
};
Expand Down
35 changes: 10 additions & 25 deletions apps/backend/controllers/events.conroller.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,5 @@
import { RequestHandler, Response } from "express";
import {
serverEventsMgr,
TestEvents,
Topics,
type EventData,
} from "../utils/serverEvents";
import { RequestHandler, Response } from 'express';
import { serverEventsMgr, TestEvents, Topics, type EventData } from '../utils/serverEvents';

// Role constants for event authorization
const ROLE_DJ = 'dj';
Expand Down Expand Up @@ -38,11 +33,7 @@ type regReqBody = {
topics?: string[];
};

export const registerEventClient: RequestHandler<
object,
unknown,
regReqBody
> = (req, res) => {
export const registerEventClient: RequestHandler<object, unknown, regReqBody> = (req, res) => {
const client = serverEventsMgr.registerClient(res);

const topics = filterAuthorizedTopics(res, req.body.topics || []);
Expand All @@ -55,27 +46,21 @@ type subReqBody = {
topics: string[];
};

export const subscribeToTopic: RequestHandler<object, unknown, subReqBody> = (
req,
res,
next
) => {
export const subscribeToTopic: RequestHandler<object, unknown, subReqBody> = (req, res, next) => {
const { client_id, topics } = req.body;

if (!client_id || !topics) {
return res.status(400).json({
message: "Bad Request: client_id or topics missing from request body",
message: 'Bad Request: client_id or topics missing from request body',
});
}

try {
const subbedTopics = serverEventsMgr.subscribe(topics, client_id);

res
.status(200)
.json({ message: "successfully subscribed", topics: subbedTopics });
res.status(200).json({ message: 'successfully subscribed', topics: subbedTopics });
} catch (e) {
console.error("Failed to subscribe to event: ", e);
console.error('Failed to subscribe to event: ', e);

return next(e);
}
Expand All @@ -85,20 +70,20 @@ export const testTrigger: RequestHandler = (req, res, next) => {
const data: EventData = {
type: TestEvents.test,
payload: {
message: "This is a test message sent over sse",
message: 'This is a test message sent over sse',
},
timestamp: new Date(),
};

try {
serverEventsMgr.broadcast(Topics.test, data);
} catch (e) {
console.error("Failed to broadcast event: ", e);
console.error('Failed to broadcast event: ', e);

return next(e);
}

res.status(200).json({
message: "event triggered",
message: 'event triggered',
});
};
3 changes: 1 addition & 2 deletions apps/backend/controllers/flowsheet.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Request, RequestHandler } from 'express';
import { Mutex } from 'async-mutex';
import { NewFSEntry, FSEntry, Show, ShowDJ, library } from "@wxyc/database";
import { NewFSEntry, FSEntry, Show, ShowDJ, library } from '@wxyc/database';
import * as flowsheet_service from '../services/flowsheet.service.js';
import { fetchAndCacheMetadata } from '../services/metadata/index.js';

Expand Down Expand Up @@ -28,7 +28,6 @@ export interface IFSEntryMetadata {
}

export interface IFSEntry extends FSEntry {
entry_type: string;
rotation_play_freq: string | null;
metadata: IFSEntryMetadata;
}
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/controllers/library.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {
NewGenre,
NewRotationRelease,
RotationRelease,
} from "@wxyc/database";
} from '@wxyc/database';
import * as libraryService from '../services/library.service.js';

type NewAlbumRequest = {
Expand Down
4 changes: 2 additions & 2 deletions apps/backend/controllers/schedule.controller.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Request, RequestHandler } from 'express';
import * as ScheduleService from '../services/schedule.service.js';
import { NewShift } from "@wxyc/database";
import { NewShift } from '@wxyc/database';

export const getSchedule: RequestHandler<object, unknown, object, object> = async (req, res, next) => {
try {
Expand All @@ -16,7 +16,7 @@ export const getSchedule: RequestHandler<object, unknown, object, object> = asyn
export const addToSchedule: RequestHandler = async (req: Request<object, object, NewShift>, res, next) => {
const { body } = req;
try {
let response = await ScheduleService.addToSchedule(body);
const response = await ScheduleService.addToSchedule(body);
res.status(200).json(response);
} catch (e) {
console.error('Error adding to schedule');
Expand Down
7 changes: 4 additions & 3 deletions apps/backend/middleware/anonymousAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ export const requireAnonymousAuth: RequestHandler = async (
return;
}

// Check if user is banned (better-auth admin plugin)
if (session.user.banned) {
// Check if user is banned (better-auth admin plugin adds these fields)
const userWithBan = session.user as typeof session.user & { banned?: boolean; banReason?: string | null };
if (userWithBan.banned) {
res.status(403).json({
message: 'Access denied',
reason: session.user.banReason || 'Account suspended',
reason: userWithBan.banReason || 'Account suspended',
});
return;
}
Expand Down
Loading
Loading