Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: potentially more fixes! #52

Merged
merged 49 commits into from
Apr 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
8e4e3ee
fix: incorrect coersion for query fields
SebassNoob Apr 19, 2024
02a3179
chore: remove useless console log in test
SebassNoob Apr 22, 2024
4a3333b
feat: gracefully clean up sigint
SebassNoob Apr 22, 2024
f8c8d79
feat: update lockfiles
SebassNoob Apr 22, 2024
db35a7f
fix: block until minio is recreated
SebassNoob Apr 22, 2024
d5a8d29
fix: remove properties out of expected tests
SebassNoob Apr 22, 2024
1627dff
refactor: types in constants and comment some code
SebassNoob Apr 22, 2024
4004f3e
chore: rename e2e tests to api
SebassNoob Apr 22, 2024
abbe26c
feat: set up eslint
SebassNoob Apr 22, 2024
a6c558b
fix: remove lint from ci
SebassNoob Apr 22, 2024
4f71e42
fix: all eslint errors
SebassNoob Apr 22, 2024
61cc8c9
fix: combine tests, add warning lint
SebassNoob Apr 22, 2024
6aeedbb
chore: prettier
SebassNoob Apr 22, 2024
11e9fa7
feat: add autofix ci
SebassNoob Apr 22, 2024
5ef8a77
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 22, 2024
e300f77
chore: bump checkout version
SebassNoob Apr 22, 2024
68aca35
fix: attempt fix of eslint ci?
SebassNoob Apr 22, 2024
68a4eee
fix: again?
SebassNoob Apr 22, 2024
4e3b49a
fix: again again?
SebassNoob Apr 22, 2024
c02fab5
fix: final fix?
SebassNoob Apr 22, 2024
19246e6
feat: add servicehoursexportmodel
SebassNoob Apr 22, 2024
93fed86
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 22, 2024
352a989
fix: revert replaced error message
SebassNoob Apr 22, 2024
65db8b9
refactor: validation of middleware function name in API routes
SebassNoob Apr 23, 2024
d086e19
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 23, 2024
b3a1bba
feat: improve verify attendance
SebassNoob Apr 23, 2024
6285484
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 23, 2024
985d20e
chore: remove legacy userwithprofilepicture
SebassNoob Apr 23, 2024
4aaca43
fix: mapping for update profile picture
SebassNoob Apr 23, 2024
56e11b3
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 23, 2024
b82ebc8
fix: delete profile picture from localstorage only on successful api …
SebassNoob Apr 23, 2024
a663dba
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 23, 2024
a5f640e
fix: timeout during postgres client install
SebassNoob Apr 24, 2024
ddb4475
feat: add interface for all exports models to follow
SebassNoob Apr 24, 2024
dd544a7
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 24, 2024
31f5dcb
fix: remove console log
SebassNoob Apr 27, 2024
bbb6e89
feat: update tests
SebassNoob Apr 27, 2024
27d09e2
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 27, 2024
6a5fef8
fix: attempt to get sonar to ignore error
SebassNoob Apr 27, 2024
0a99fa0
refactor: parseErrorMessage -> parseServerError
SebassNoob Apr 27, 2024
9b3b791
fix: rename file
SebassNoob Apr 27, 2024
847120b
feat: add docs to remap asset url
SebassNoob Apr 27, 2024
49bf642
feat: more descriptive client errors
SebassNoob Apr 27, 2024
972ee46
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 27, 2024
70848cf
fix: reformat err handling
SebassNoob Apr 27, 2024
536d687
fix: remove console log
SebassNoob Apr 28, 2024
d86df51
[autofix.ci] apply automated fixes
autofix-ci[bot] Apr 28, 2024
48387ac
fix: fail silently without causing crash on app update
SebassNoob Apr 28, 2024
8e9226e
fix: weird failed condition for profile page
SebassNoob Apr 28, 2024
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
34 changes: 34 additions & 0 deletions .github/workflows/autofix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: autofix.ci # needed to securely identify the workflow

on:
pull_request:
push:
branches:
- '**'
permissions:
contents: read

jobs:
autofix:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup bun
uses: oven-sh/setup-bun@v1
with:
bun-version: 1.1.3

- name: Install dependencies (backend)
run: cd interapp-backend && bun install

- name: Format code (backend)
run: cd interapp-backend && bun run prettier

- name: Install dependencies (frontend)
run: cd interapp-frontend && bun install

- name: Format code (frontend)
run: cd interapp-frontend && bun run prettier

- uses: autofix-ci/action@d3e591514b99d0fca6779455ff8338516663f7cc
12 changes: 9 additions & 3 deletions .github/workflows/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@ on:

jobs:
test-backend:
name: Test backend
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Build test environment
run: docker compose -f docker-compose.test.yml build --no-cache
Expand All @@ -27,18 +28,23 @@ jobs:

- name: Install dependencies
run: cd interapp-backend && bun install
- name: Test with bun

- name: Lint code
run: cd interapp-backend && bun run lint

- name: Run unit and api tests
run: cd interapp-backend && bun run test

- name: Tear down test environment
run: docker compose -f docker-compose.test.yml down

build-application:
name: Build application
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3
uses: actions/checkout@v4

- name: Setup prod environment
run: docker compose -f docker-compose.prod.yml up -d --build
Expand Down
7 changes: 7 additions & 0 deletions interapp-backend/api/models/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { HTTPErrors } from '@utils/errors';
import { SignJWT, jwtVerify, JWTPayload, JWTVerifyResult } from 'jose';

import redisClient from '@utils/init_redis';
import minioClient from '@utils/init_minio';

export interface UserJWT {
user_id: number;
Expand All @@ -12,6 +13,8 @@ export interface UserJWT {

type JWTtype = 'access' | 'refresh';

const MINIO_BUCKETNAME = process.env.MINIO_BUCKETNAME as string;

export class AuthModel {
private static readonly accessSecret = new TextEncoder().encode(
process.env.JWT_ACCESS_SECRET as string,
Expand Down Expand Up @@ -79,6 +82,7 @@ export class AuthModel {
'user.email',
'user.verified',
'user.service_hours',
'user.profile_picture',
])
.from(User, 'user')
.leftJoinAndSelect('user.user_permissions', 'user_permissions')
Expand Down Expand Up @@ -109,6 +113,9 @@ export class AuthModel {
email: user.email,
verified: user.verified,
service_hours: user.service_hours,
profile_picture: user.profile_picture
? await minioClient.presignedGetObject(MINIO_BUCKETNAME, user.profile_picture)
: null,
permissions: user.user_permissions.map((perm) => perm.permission_id),
};

Expand Down
Original file line number Diff line number Diff line change
@@ -1,48 +1,20 @@
import appDataSource from '@utils/init_datasource';
import { ServiceSession, AttendanceStatus } from '@db/entities';
import xlsx, { WorkSheet } from 'node-xlsx';
import {
AttendanceExportsResult,
AttendanceExportsXLSX,
AttendanceQueryExportsConditions,
ExportsModelImpl,
staticImplements,
} from './types';
import { BaseExportsModel } from './exports_base';
import { ServiceSession, type AttendanceStatus } from '@db/entities';
import { HTTPErrors } from '@utils/errors';
import { WorkSheet } from 'node-xlsx';
import appDataSource from '@utils/init_datasource';

type ExportsResult = {
service_session_id: number;
start_time: string;
end_time: string;
service: {
name: string;
service_id: number;
};
service_session_users: {
service_session_id: number;
username: string;
ad_hoc: boolean;
attended: AttendanceStatus;
is_ic: boolean;
}[];
};

type ExportsXLSX = [['username', ...string[]], ...[string, ...(AttendanceStatus | null)[]][]];

type QueryExportsConditions = {
id: number;
} & (
| {
start_date: string; // ISO strings, we have already validated this
end_date: string;
}
| {
start_date?: never;
end_date?: never;
}
);

export class ExportsModel {
private static getSheetOptions = (ret: ExportsXLSX) => ({
'!cols': [{ wch: 24 }, ...Array(ret.length).fill({ wch: 16 })],
});
private static constructXLSX = (...data: Parameters<typeof xlsx.build>[0]) => xlsx.build(data);

public static async queryExports({ id, start_date, end_date }: QueryExportsConditions) {
let res: ExportsResult[];
@staticImplements<ExportsModelImpl>()
export class AttendanceExportsModel extends BaseExportsModel {
public static async queryExports({ id, start_date, end_date }: AttendanceQueryExportsConditions) {
let res: AttendanceExportsResult[];
if (start_date === undefined || end_date === undefined) {
res = await appDataSource.manager
.createQueryBuilder()
Expand Down Expand Up @@ -82,16 +54,16 @@ export class ExportsModel {
return res;
}

public static async formatXLSX(conds: QueryExportsConditions) {
public static async formatXLSX(conds: AttendanceQueryExportsConditions) {
const ret = await this.queryExports(conds);

if (ret.length === 0) throw HTTPErrors.RESOURCE_NOT_FOUND;

// create headers
// start_time is in ascending order
const headers: ExportsXLSX[0] = (['username'] as ExportsXLSX[0]).concat(
const headers = (['username'] as AttendanceExportsXLSX[0]).concat(
ret.map(({ start_time }) => start_time),
) as ExportsXLSX[0];
) as AttendanceExportsXLSX[0];

// output needs to be in the form:
// [username, [attendance status]]
Expand All @@ -118,15 +90,13 @@ export class ExportsModel {
});
});

const body: ExportsXLSX[1][] = Object.entries(usernameMap).map(([username, attendance]) => [
username,
...attendance,
]);
const body: AttendanceExportsXLSX[1][] = Object.entries(usernameMap).map(
([username, attendance]) => [username, ...attendance],
);

const out: ExportsXLSX = [headers, ...body];
const out: AttendanceExportsXLSX = [headers, ...body];

const sheetOptions = this.getSheetOptions(out);
console.log(sheetOptions);

return { name: ret[0].service.name, data: out, options: sheetOptions };
}
Expand Down
8 changes: 8 additions & 0 deletions interapp-backend/api/models/exports/exports_base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import xlsx from 'node-xlsx';

export class BaseExportsModel {
protected static getSheetOptions = <T extends unknown[]>(ret: T) => ({
'!cols': [{ wch: 24 }, ...Array(ret.length).fill({ wch: 16 })],
});
protected static constructXLSX = (...data: Parameters<typeof xlsx.build>[0]) => xlsx.build(data);
}
15 changes: 15 additions & 0 deletions interapp-backend/api/models/exports/exports_service_hours.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
AttendanceExportsResult,
AttendanceExportsXLSX,
AttendanceQueryExportsConditions,
ExportsModelImpl,
staticImplements,
} from './types';
import { BaseExportsModel } from './exports_base';
import { ServiceSession, type AttendanceStatus } from '@db/entities';
import { HTTPErrors } from '@utils/errors';
import { WorkSheet } from 'node-xlsx';
import appDataSource from '@utils/init_datasource';

// @staticImplements<ExportsModelImpl>()
export class ServiceHoursExportsModel extends BaseExportsModel {}
2 changes: 2 additions & 0 deletions interapp-backend/api/models/exports/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { AttendanceExportsModel } from './exports_attendance';
export { ServiceHoursExportsModel } from './exports_service_hours';
50 changes: 50 additions & 0 deletions interapp-backend/api/models/exports/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { AttendanceStatus } from '@db/entities';

export interface ExportsModelImpl {
queryExports(conds: unknown): Promise<unknown[]>;
formatXLSX(conds: unknown): Promise<unknown>;
packXLSX(ids: number[], start_date?: string, end_date?: string): Promise<Buffer>;
}

// class decorator that asserts that a class implements an interface statically
// https://stackoverflow.com/a/43674389
export function staticImplements<T>() {
return <U extends T>(constructor: U) => {
constructor; // NOSONAR
};
}

export type AttendanceExportsResult = {
service_session_id: number;
start_time: string;
end_time: string;
service: {
name: string;
service_id: number;
};
service_session_users: {
service_session_id: number;
username: string;
ad_hoc: boolean;
attended: AttendanceStatus;
is_ic: boolean;
}[];
};

export type AttendanceExportsXLSX = [
['username', ...string[]],
...[string, ...(AttendanceStatus | null)[]][],
];

export type AttendanceQueryExportsConditions = {
id: number;
} & (
| {
start_date: string; // ISO strings, we have already validated this
end_date: string;
}
| {
start_date?: never;
end_date?: never;
}
);
47 changes: 40 additions & 7 deletions interapp-backend/api/models/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,21 +347,54 @@ export class ServiceModel {
}));
}
public static async verifyAttendance(hash: string, username: string) {
const service_session_id = await redisClient.hGet('service_session', hash);
if (!service_session_id) {
const id = await redisClient.hGet('service_session', hash);
if (!id) {
throw HTTPErrors.INVALID_HASH;
}
const service_session_user = await this.getServiceSessionUser(
parseInt(service_session_id),
username,
);
const service_session_id = parseInt(id);

const service_session_user = await this.getServiceSessionUser(service_session_id, username);

if (service_session_user.attended === AttendanceStatus.Attended) {
throw HTTPErrors.ALREADY_ATTENDED;
}
service_session_user.attended = AttendanceStatus.Attended;
await this.updateServiceSessionUser(service_session_user);
return service_session_user;

// get some metadata and return it to the user

type _Return = {
start_time: string;
end_time: string;
service_hours: number;
name: string;
ad_hoc: boolean;
};
const res = await appDataSource.manager
.createQueryBuilder()
.select([
'service_session.start_time',
'service_session.end_time',
'service_session.service_hours',
'service.name',
])
.from(ServiceSession, 'service_session')
.leftJoin('service_session.service', 'service')
.where('service_session_id = :id', { id: service_session_id })
.getOne();

// literally impossible for this to be null
if (!res) {
throw HTTPErrors.RESOURCE_NOT_FOUND;
}

return {
start_time: res.start_time,
end_time: res.end_time,
service_hours: res.service_hours,
name: res.service.name,
ad_hoc: service_session_user.ad_hoc,
} as _Return;
}
public static async getAdHocServiceSessions() {
const res = await appDataSource.manager
Expand Down
Loading