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
83 changes: 83 additions & 0 deletions .github/workflows/verify.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Lints, builds and tests the backend and frontend on every push, but
# only for the side(s) that actually changed.

name: Verify

permissions:
contents: read

on:
push:

jobs:
changes:
runs-on: ubuntu-latest
outputs:
backend: ${{ steps.filter.outputs.backend }}
frontend: ${{ steps.filter.outputs.frontend }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- uses: dorny/paths-filter@v4
id: filter
with:
filters: |
backend:
- 'backend/**'
frontend:
- 'frontend/**'

backend:
needs: changes
if: needs.changes.outputs.backend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
package_json_file: backend/package.json
- name: Use Node.js
uses: actions/setup-node@v7
with:
node-version: '24.x'
cache: 'pnpm'
cache-dependency-path: backend/pnpm-lock.yaml
- name: Install dependencies
run: cd backend/ && pnpm install
- name: Lint
run: cd backend/ && pnpm lint
- name: Build
run: cd backend/ && pnpm run build
- name: Test
run: cd backend/ && pnpm test

frontend:
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
needs: changes
if: needs.changes.outputs.frontend == 'true'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup pnpm
uses: pnpm/action-setup@v6
with:
package_json_file: frontend/package.json
- name: Use Node.js
uses: actions/setup-node@v7
with:
node-version: '24.x'
cache: 'pnpm'
cache-dependency-path: |
backend/pnpm-lock.yaml
frontend/pnpm-lock.yaml
- name: Build backend (frontend imports its compiled types via the @backend alias)
run: cd backend/ && pnpm install && pnpm run build
- name: Install dependencies
run: cd frontend/ && pnpm install
- name: Lint
run: cd frontend/ && pnpm lint
- name: Build
run: cd frontend/ && pnpm run build
- name: Test
run: cd frontend/ && pnpm test
Comment thread
github-advanced-security[bot] marked this conversation as resolved.
Fixed
63 changes: 63 additions & 0 deletions backend/access.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { CollectionAccessAction, UserContext } from 'amberbase';
import { SetlistEntity } from './models.js';
import { UserRole } from './definitions.js';

/**
* Access rights for the setlists collection.
* Public setlists can be created by editors and performers.
* Private setlists can be created by everyone.
*/
export const setlistsAccessRights = (user: UserContext, doc: SetlistEntity | null, action: CollectionAccessAction): boolean => {
if (action === 'create') {
if (user.roles.includes(UserRole.Editor) || user.roles.includes(UserRole.Performer)) {
return true;
}
if (user.roles.includes(UserRole.Reader)) {
return doc?.createdBy === user.userId && !doc?.isPublic;
}
return false;
}
// Setlists can be subscribed to by all roles, but only public setlists can be read per default.
// This is done via access tags below.
if (action === 'subscribe') {
return user.roles.includes(UserRole.Editor) || user.roles.includes(UserRole.Performer) || user.roles.includes(UserRole.Reader);
}
// Setlists can be deleted by editors or the corresponding creator.
if (action === 'delete') {
return user.roles.includes(UserRole.Editor) || doc?.createdBy === user.userId;
}
// Setlists can be updated by the corresponding creator.
// Public setlists can be updated by editors and performers.
if (action === 'update') {
if (doc?.createdBy === user.userId) {
return true;
}
if (doc?.isPublic && (user.roles.includes(UserRole.Editor) || user.roles.includes(UserRole.Performer))) {
return true;
}
return false;
}

return false;
};

/**
* The owner of a private setlist can share it with other users
*/
export const setlistsAccessTagsFromDocument = (doc: SetlistEntity): string[] => {
const tags = [`o-${doc.createdBy}`];
if (doc.isPublic) {
tags.push('public');
} else {
doc.sharedWith.forEach((userId) => {
tags.push(`s-${userId}`);
});
}
return tags;
};

export const setlistsAccessTagsFromUser = (user: UserContext): string[] => [
'public',
`o-${user.userId}`,
`s-${user.userId}`,
];
2 changes: 1 addition & 1 deletion backend/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export const can = (action: string, roles: UserRole[], context?: CanContext): bo
[UserRole.Performer]: 2,
[UserRole.Reader]: 1,
};
const highestRole = roles.toSorted((a, b) => level[a] - level[b])[0];
const highestRole = roles.toSorted((a, b) => level[b] - level[a])[0];

// Admins are allowed in general
if (highestRole === UserRole.Admin) {
Expand Down
59 changes: 5 additions & 54 deletions backend/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { amber, CollectionAccessAction, UserContext } from 'amberbase';
import { amber } from 'amberbase';
import cookieParser from 'cookie-parser';
import express from 'express';
import rateLimit from 'express-rate-limit';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { setlistsAccessRights, setlistsAccessTagsFromDocument, setlistsAccessTagsFromUser } from './access.js';
import { SetlistEntity, SongEntity } from './models.js';
import { UserRole } from './definitions.js';

Expand All @@ -27,59 +28,9 @@ const appInit = amber()
}
})
.withCollection<SetlistEntity>('setlists', {
accessRights: (user: UserContext, doc: SetlistEntity | null, action: CollectionAccessAction) => {
// Public setlists can be created by editors and performers.
// Private setlists can be created by everyone.
if (action === 'create') {
if (user.roles.includes(UserRole.Editor) || user.roles.includes(UserRole.Performer)) {
return true;
}
if (user.roles.includes(UserRole.Reader)) {
return doc?.createdBy === user.userId && !doc?.isPublic;
}
return false;
}
// Setlists can be subscribed to by all roles, but only public setlists can be read per default.
// This is done via access tags below.
if (action === 'subscribe') {
return user.roles.includes(UserRole.Editor) || user.roles.includes(UserRole.Performer) || user.roles.includes(UserRole.Reader);
}
// Setlists can be deleted by editors or the corresponding creator.
if (action === 'delete') {
return user.roles.includes(UserRole.Editor) || doc?.createdBy === user.userId;
}
// Setlists can be updated by the corresponding creator.
// Public setlists can be updated by editors and performers.
if (action === 'update') {
if (doc?.createdBy === user.userId) {
return true;
}
if (doc?.isPublic && (user.roles.includes(UserRole.Editor) || user.roles.includes(UserRole.Performer))) {
return true;
}
return false;
}

return false;
},

// The owner of a private setlist can share it with other users
accessTagsFromDocument: (doc: SetlistEntity) => {
const tags = [`o-${doc.createdBy}`];
if (doc.isPublic) {
tags.push('public');
} else {
doc.sharedWith.forEach((userId) => {
tags.push(`s-${userId}`);
});
}
return tags;
},
accessTagsFromUser: (user: UserContext) => [
'public',
`o-${user.userId}`,
`s-${user.userId}`,
],
accessRights: setlistsAccessRights,
accessTagsFromDocument: setlistsAccessTagsFromDocument,
accessTagsFromUser: setlistsAccessTagsFromUser,
})
.withUi({
availableRoles: [UserRole.Editor, UserRole.Performer, UserRole.Reader],
Expand Down
6 changes: 4 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"main": "index.js",
"packageManager": "pnpm@11.9.0",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"test": "vitest run",
"test:watch": "vitest",
"start": "node dist/index.js",
"build": "tsc",
"lint": "oxlint",
Expand All @@ -25,6 +26,7 @@
"@types/ws": "^8.18.1",
"oxlint": "^1.76.0",
"oxlint-tsgolint": "^7.0.2001",
"typescript": "^6.0.3"
"typescript": "^6.0.3",
"vitest": "^4.1.10"
}
}
Loading
Loading