Skip to content
Open
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
10 changes: 10 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,8 @@ jobs:
- name: Install Dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true' || steps.validate-cache.outputs.valid == 'false'
run: npm ci
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Type check
run: npm run typecheck
Expand Down Expand Up @@ -190,6 +192,8 @@ jobs:
- name: Install Dependencies
if: steps.cache-node-modules.outputs.cache-hit != 'true' || steps.validate-cache.outputs.valid == 'false'
run: npm ci
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Run affected unit tests
run: |
Expand Down Expand Up @@ -302,6 +306,7 @@ jobs:
load: true
push: false
tags: wxyc_auth_service:ci
build-args: NPM_TOKEN=${{ secrets.NPM_TOKEN }}
cache-from: type=gha,scope=auth
cache-to: type=gha,mode=max,scope=auth

Expand Down Expand Up @@ -334,6 +339,7 @@ jobs:
load: true
push: false
tags: wxyc_backend_service:ci
build-args: NPM_TOKEN=${{ secrets.NPM_TOKEN }}
cache-from: type=gha,scope=backend
cache-to: type=gha,mode=max,scope=backend

Expand Down Expand Up @@ -366,13 +372,17 @@ jobs:
- name: Install Dependencies
if: env.RUN_TESTS == 'true' && (steps.cache-node-modules.outputs.cache-hit != 'true' || steps.validate-cache.outputs.valid == 'false')
run: npm ci
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Lint .env file
if: env.RUN_TESTS == 'true'
run: npm run lint:env

- name: Set Up Test Environment
if: env.RUN_TESTS == 'true'
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
touch .env
npm run ci:env
Expand Down
2 changes: 2 additions & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@wxyc:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${NPM_TOKEN}
6 changes: 5 additions & 1 deletion Dockerfile.auth
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#Build stage
FROM node:22-alpine AS builder

ARG NPM_TOKEN

Check warning on line 4 in Dockerfile.auth

View workflow job for this annotation

GitHub Actions / Integration-Tests

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NPM_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
WORKDIR /auth-builder

COPY ./.npmrc ./
COPY ./package.json ./
COPY ./tsconfig.base.json ./
COPY ./shared ./shared
Expand All @@ -13,14 +15,16 @@
#Production stage
FROM node:22-alpine AS prod

ARG NPM_TOKEN

Check warning on line 18 in Dockerfile.auth

View workflow job for this annotation

GitHub Actions / Integration-Tests

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NPM_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
WORKDIR /auth-service

COPY ./.npmrc ./
COPY ./package* ./
COPY ./apps/auth/package* ./apps/auth/
COPY ./shared/database/package* ./shared/database/
COPY ./shared/authentication/package* ./shared/authentication/

RUN npm install --omit=dev
RUN npm install --omit=dev && rm -f .npmrc

COPY --from=builder ./auth-builder/apps/auth/dist ./apps/auth/dist
COPY --from=builder ./auth-builder/shared/database/dist ./shared/database/dist
Expand Down
6 changes: 5 additions & 1 deletion Dockerfile.backend
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#Build stage
FROM node:22-alpine AS builder

ARG NPM_TOKEN

Check warning on line 4 in Dockerfile.backend

View workflow job for this annotation

GitHub Actions / Integration-Tests

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NPM_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
WORKDIR /builder

COPY ./.npmrc ./
COPY ./package.json ./
COPY ./tsconfig.base.json ./
COPY ./shared ./shared
Expand All @@ -13,14 +15,16 @@
#Production stage
FROM node:22-alpine AS prod

ARG NPM_TOKEN

Check warning on line 18 in Dockerfile.backend

View workflow job for this annotation

GitHub Actions / Integration-Tests

Sensitive data should not be used in the ARG or ENV commands

SecretsUsedInArgOrEnv: Do not use ARG or ENV instructions for sensitive data (ARG "NPM_TOKEN") More info: https://docs.docker.com/go/dockerfile/rule/secrets-used-in-arg-or-env/
WORKDIR /application

COPY ./.npmrc ./
COPY ./package* ./
COPY ./apps/backend/package* ./apps/backend/
COPY ./shared/database/package* ./shared/database/
COPY ./shared/authentication/package* ./shared/authentication/

RUN npm install --omit=dev
RUN npm install --omit=dev && rm -f .npmrc

COPY --from=builder ./builder/apps/backend/dist ./apps/backend/dist
COPY --from=builder ./builder/shared/database/dist ./shared/database/dist
Expand Down
2 changes: 1 addition & 1 deletion apps/auth/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
"start": "node dist/app.js",
"build": "tsup --minify",
"clean": "rm -rf dist",
"docker:build": "docker build -t wxyc_auth_service:ci -f ../../Dockerfile.auth ../../",
"docker:build": "docker build --build-arg NPM_TOKEN=$NPM_TOKEN -t wxyc_auth_service:ci -f ../../Dockerfile.auth ../../",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit"
},
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
"start": "node dist/app.js",
"build": "tsup --minify",
"clean": "rm -rf dist",
"docker:build": "docker build -t wxyc_backend_service:ci -f ../../Dockerfile.backend ../../",
"docker:build": "docker build --build-arg NPM_TOKEN=$NPM_TOKEN -t wxyc_backend_service:ci -f ../../Dockerfile.backend ../../",
"dev": "tsup --watch",
"typecheck": "tsc --noEmit"
},
Expand Down
4 changes: 2 additions & 2 deletions jest.unit.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,14 @@ const config: Config = {
testMatch: ['<rootDir>/tests/unit/**/*.test.ts'],
setupFilesAfterEnv: ['<rootDir>/tests/setup/unit.setup.ts'],
transform: {
'^.+\\.tsx?$': [
'^.+\\.[jt]sx?$': [
'ts-jest',
{
tsconfig: '<rootDir>/tests/tsconfig.json',
},
],
},
transformIgnorePatterns: ['node_modules/(?!(jose|drizzle-orm)/)'],
transformIgnorePatterns: ['node_modules/(?!(jose|drizzle-orm|@wxyc/shared)/)'],
moduleNameMapper: {
// Mock workspace database package
'^@wxyc/database$': '<rootDir>/tests/mocks/database.mock.ts',
Expand Down
25 changes: 24 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions shared/authentication/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"dependencies": {
"@aws-sdk/client-ses": "^3.971.0",
"@wxyc/database": "^1.0.0",
"@wxyc/shared": "^0.2.0",
"better-auth": "^1.3.23",
"drizzle-orm": "^0.41.0",
"postgres": "^3.4.4"
Expand Down
23 changes: 18 additions & 5 deletions shared/authentication/src/auth.roles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,29 @@ export const WXYCRoles = {
stationManager,
};

export type WXYCRole = keyof typeof WXYCRoles;
import type { WXYCRole } from '@wxyc/shared/auth-client/auth';
export type { WXYCRole } from '@wxyc/shared/auth-client/auth';
export { roleToAuthorization, Authorization } from '@wxyc/shared/auth-client/auth';

// Compile-time assertion: every role in WXYCRoles is a valid shared WXYCRole.
// The reverse is intentionally not asserted -- shared includes "admin", which
// Backend-Service maps to "stationManager" via normalizeRole() rather than
// defining as a separate better-auth role.
type _AssertLocalRolesAreShared = [keyof typeof WXYCRoles] extends [WXYCRole] ? true : never;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const _localRolesValid: _AssertLocalRolesAreShared = true;

/** The set of roles that have a better-auth access control implementation. */
export type ImplementedRole = keyof typeof WXYCRoles;

/** Maps better-auth system roles to their WXYC equivalent. */
const systemRoleMap: Record<string, WXYCRole> = {
const systemRoleMap: Record<string, ImplementedRole> = {
admin: 'stationManager',
owner: 'stationManager',
};

/** Normalizes a role string to a WXYCRole, mapping better-auth system roles. */
export function normalizeRole(role: string): WXYCRole | undefined {
if (role in WXYCRoles) return role as WXYCRole;
/** Normalizes a role string to an implemented role, mapping better-auth system roles. */
export function normalizeRole(role: string): ImplementedRole | undefined {
if (role in WXYCRoles) return role as ImplementedRole;
return systemRoleMap[role];
}
2 changes: 1 addition & 1 deletion shared/authentication/tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ export default defineConfig({
outDir: 'dist',
clean: true,
sourcemap: true,
external: ['drizzle-orm', 'postgres', 'better-auth'],
external: ['drizzle-orm', 'postgres', 'better-auth', '@wxyc/shared'],
});
1 change: 1 addition & 0 deletions tests/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"moduleResolution": "Node",
"esModuleInterop": true,
"strict": false,
"allowJs": true,
"skipLibCheck": true,
"declaration": false,
"noEmit": true,
Expand Down
47 changes: 47 additions & 0 deletions tests/unit/authentication/shared-type-compatibility.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { WXYCRoles, normalizeRole, type WXYCRole } from '../../../shared/authentication/src/auth.roles';
import { Authorization, roleToAuthorization, type WXYCRole as SharedWXYCRole } from '@wxyc/shared/auth-client/auth';

describe('shared type compatibility', () => {
describe('WXYCRoles alignment', () => {
it.each(Object.keys(WXYCRoles) as WXYCRole[])('"%s" is a valid SharedWXYCRole', (role) => {
// Every role in Backend-Service's WXYCRoles must be a valid shared WXYCRole.
// This is also enforced at compile time by the type assertion in auth.roles.ts.
const sharedRole: SharedWXYCRole = role;
expect(sharedRole).toBe(role);
});
});

describe('Authorization enum', () => {
it('has expected values', () => {
expect(Authorization.NO).toBe(0);
expect(Authorization.DJ).toBe(1);
expect(Authorization.MD).toBe(2);
expect(Authorization.SM).toBe(3);
expect(Authorization.ADMIN).toBe(4);
});
});

describe('normalizeRole consistency with roleToAuthorization', () => {
it('admin normalizes to stationManager, consistent with shared ADMIN >= SM', () => {
expect(normalizeRole('admin')).toBe('stationManager');
// Shared maps "admin" to ADMIN (4), which is >= SM (3).
// Both grant full access; the normalization is a backend-specific concern.
expect(roleToAuthorization('admin')).toBe(Authorization.ADMIN);
});

it.each(['member', 'dj', 'musicDirector', 'stationManager'] as const)(
'"%s" maps to the same Authorization via both paths',
(role) => {
// Direct shared mapping
const sharedAuth = roleToAuthorization(role);
// Backend path: normalizeRole returns the role as-is, then shared maps it
const normalized = normalizeRole(role);
expect(normalized).toBe(role);
expect(normalized).toBeDefined();
if (normalized) {
expect(roleToAuthorization(normalized)).toBe(sharedAuth);
}
}
);
});
});
Loading