Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e5ec8ad
chore: prettier
SebassNoob Dec 17, 2023
8902d33
feat: sql dump script
SebassNoob Dec 17, 2023
bfe2aad
fix: optimise select users by service
SebassNoob Dec 17, 2023
35792ed
fix: prevent change email from submitting invalid inputs
SebassNoob Dec 17, 2023
e4af493
fix: round email button corners
SebassNoob Dec 18, 2023
41510e0
feat: add endpoint for getusersbyservice
SebassNoob Dec 17, 2023
bc9c782
fix: memory leak with bun in watchpack
SebassNoob Dec 18, 2023
c2cc116
feat: add bulk user service change to endpoints
SebassNoob Dec 18, 2023
148a3a0
feat: add service page
SebassNoob Dec 18, 2023
395b39f
fix: fix code smells
SebassNoob Dec 18, 2023
2fac2c0
feat: update navbar headers
SebassNoob Dec 18, 2023
3027187
feat: add create services gui
SebassNoob Dec 18, 2023
2d67444
fix: clean up
SebassNoob Dec 18, 2023
2361a9b
fix: hopefully fix build process
SebassNoob Dec 18, 2023
3165bb0
feat: add delete service action
SebassNoob Dec 20, 2023
c46c2e9
fix: move to node to prevent segfaults
SebassNoob Dec 20, 2023
fca45c9
feat: finalise services page
SebassNoob Dec 20, 2023
27bb03c
chore: clean up code smells
SebassNoob Dec 20, 2023
1ba0871
chore: revert breaking changes, optimise further
SebassNoob Dec 20, 2023
acdfa90
chore: bump version
SebassNoob Dec 20, 2023
30e967f
feat: add minio
SebassNoob Dec 21, 2023
1ff254f
feat: add fullstack support for minio
SebassNoob Dec 21, 2023
f2f996d
fix: auto test failure
SebassNoob Dec 21, 2023
941af9d
fix: fix incorrect placeholder image pathing
SebassNoob Dec 21, 2023
71887f5
fix: simplify initialisation
SebassNoob Dec 21, 2023
a8a6936
fix: revert accidental removal of build test
SebassNoob Dec 21, 2023
921ab5e
chore: update readme
SebassNoob Dec 21, 2023
e7d98d5
chore: standardise import aliases
SebassNoob Dec 22, 2023
3c2e3ec
fix: incorrect aliases
SebassNoob Dec 22, 2023
9e88e81
fix: update auth style breakpoints
SebassNoob Dec 22, 2023
b9334a7
fix: show error on invalid user permission combination
SebassNoob Dec 31, 2023
3ad08f9
chore: bump bun version
SebassNoob Jan 1, 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
7 changes: 5 additions & 2 deletions .github/workflows/pipeline.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@v3

- name: Setup test environment
run: docker compose -f docker-compose.test.yml up -d --build
- name: Build test environment
run: docker compose -f docker-compose.test.yml build --no-cache

- name: Run test environment
run: docker compose -f docker-compose.test.yml up -d

- name: Setup bun
uses: oven-sh/setup-bun@v1
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -174,4 +174,5 @@ dist
# Finder (MacOS) folder config
.DS_Store

pgdata/
pgdata/
minio_data/
17 changes: 14 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,22 @@ Ensure that you have the docker daemon running, along with ``bun``, ``make`` and

Clone with ``git clone https://github.com/raffles-interact/interapp.git``

### Running
### First time setup

1. Run ``touch ./interapp-backend/.env.local`` and paste the contents of the file that the maintainers gave you.

2. Navigate to ``./interapp-backend/`` and ``./interapp-frontend/`` and run ``bun i`` in both directories.

Ensure you are in the root of the project, and that ``.env.local`` exists. Run ``make build`` and ``make watch`` (for HMR).
3. Run ``make build`` and ``make run``.

4. (Optional) To give yourself all the permissions on the website, ssh into the ``interapp-postgres`` container and run an SQL query that gives your account permissions from 0 - 6. This should look like:
```sql
INSERT INTO user_permission (username, permission_id, "userUsername") VALUES (‘<username>’, 1, ‘<username>’), (‘<username>’, 2, ‘<username>’), (‘<username>’, 3, ‘<username>’), (‘<username>’, 4, ‘<username>’), (‘<username>’, 5, ‘<username>’), (‘<username>’, 6, ‘<username>’);
```

### Running

If your IDE is giving you import errors, run ``bun i`` in the terminal.
Run ``make build`` and ``make run`` for the development server. If needed, add ``version=(test|prod)`` for test and production servers respectively.

Go to ``localhost:3000`` for frontend and ``localhost:3000/api`` for api routes

Expand Down
30 changes: 29 additions & 1 deletion docker-compose.dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,33 @@ services:
redis:
condition: service_healthy

minio:
container_name: interapp-minio
image: minio/minio
restart: on-failure
command: minio server /data
env_file:
- ./interapp-backend/.env.development
- ./interapp-backend/.env.local
ports:
- 9000:9000
- 9001:9001
volumes:
- ./interapp-backend/minio_data:/data
networks:
- interapp-network
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 2s
timeout: 2s
retries: 5
start_period: 60s
depends_on:
postgres:
condition: service_healthy



backend:
container_name: interapp-backend
restart: on-failure
Expand All @@ -88,6 +115,8 @@ services:
condition: service_healthy
redis:
condition: service_healthy
minio:
condition: service_healthy

frontend:
container_name: interapp-frontend
Expand All @@ -104,7 +133,6 @@ services:
- interapp-network
depends_on:
- backend
- postgres
deploy:
resources:
reservations:
Expand Down
24 changes: 24 additions & 0 deletions docker-compose.prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,30 @@ services:
redis:
condition: service_healthy

minio:
container_name: interapp-minio
image: minio/minio
restart: on-failure
command: minio server /data
env_file:
- ./interapp-backend/.env.production
ports:
- 9000:9000
- 9001:9001
volumes:
- ./interapp-backend/minio_data:/data
networks:
- interapp-network
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 2s
timeout: 2s
retries: 5
start_period: 60s
depends_on:
postgres:
condition: service_healthy


backend:
container_name: interapp-backend
Expand Down
24 changes: 24 additions & 0 deletions docker-compose.test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,28 @@ services:
retries: 7
start_period: 60s

minio:
container_name: interapp-minio
image: minio/minio
restart: on-failure
command: minio server /data
env_file:
- ./interapp-backend/tests/config/.env.docker
ports:
- 9000:9000
- 9001:9001
networks:
- interapp-network
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 2s
timeout: 2s
retries: 5
start_period: 60s
depends_on:
postgres:
condition: service_healthy

backend:
container_name: interapp-backend-test
restart: on-failure
Expand All @@ -71,6 +93,8 @@ services:
condition: service_healthy
redis:
condition: service_healthy
minio:
condition: service_healthy


networks:
Expand Down
12 changes: 11 additions & 1 deletion interapp-backend/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,14 @@ API_PORT=8000
REDIS_URL=redis://interapp-redis:6379
FRONTEND_URL=http://localhost:3000

SCHOOL_EMAIL_REGEX="^[A-Za-z0-9]+@student\.ri\.edu\.sg|[A-Za-z0-9]+@rafflesgirlssch.edu.sg$"
SCHOOL_EMAIL_REGEX="^[A-Za-z0-9]+@student\.ri\.edu\.sg|[A-Za-z0-9]+@rafflesgirlssch.edu.sg$"

MINIO_ROOT_USER=minio-root
MINIO_ROOT_PASSWORD=minio-password
MINIO_ADDRESS=':9000'
MINIO_CONSOLE_ADDRESS=':9001'
MINIO_ACCESSKEY=minio-root
MINIO_SECRETKEY=minio-password

MINIO_BUCKETNAME=interapp-minio
MINIO_ENDPOINT=interapp-minio
12 changes: 11 additions & 1 deletion interapp-backend/.env.production
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,14 @@ API_PORT=8000
REDIS_URL=redis://interapp-redis:6379
FRONTEND_URL=http://localhost:3000 # TODO: Change this to the actual frontend URL

SCHOOL_EMAIL_REGEX="^[A-Za-z0-9]+@student\.ri\.edu\.sg|[A-Za-z0-9]+@rafflesgirlssch.edu.sg$"
SCHOOL_EMAIL_REGEX="^[A-Za-z0-9]+@student\.ri\.edu\.sg|[A-Za-z0-9]+@rafflesgirlssch.edu.sg$"

MINIO_ROOT_USER=minio-root
MINIO_ROOT_PASSWORD=minio-password
MINIO_ADDRESS=':9000'
MINIO_CONSOLE_ADDRESS=':9001'
MINIO_ACCESSKEY=minio-root
MINIO_SECRETKEY=minio-password

MINIO_BUCKETNAME=interapp-minio
MINIO_ENDPOINT=interapp-minio
3 changes: 3 additions & 0 deletions interapp-backend/.prettierignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
node_modules/**/*
pgdata/**/*
minio_data/**/*
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
text-align: center;
text-decoration: none;
display: inline-block;
border-radius: 7px;
}
.button a {
color: white;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
text-align: center;
text-decoration: none;
display: inline-block;
border-radius: 7px;
}
.button a {
color: white;
Expand Down
52 changes: 51 additions & 1 deletion interapp-backend/api/models/service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { HTTPError, HTTPErrorCode } from '@utils/errors';
import appDataSource from '@utils/init_datasource';
import minioClient from '@utils/init_minio';
import dataUrlToBuffer from '@utils/dataUrlToBuffer';
import { Service, ServiceSession, ServiceSessionUser } from '@db/entities';
import { UserModel } from './user';

Expand All @@ -13,7 +15,25 @@ export class ServiceModel {
newService.contact_email = service.contact_email;
newService.contact_number = service.contact_number;
newService.website = service.website;
newService.promotional_image = service.promotional_image;

if (!service.promotional_image) newService.promotional_image = null;
else {
const convertedFile = dataUrlToBuffer(service.promotional_image);
if (!convertedFile) {
throw new HTTPError(
'Invalid promotional image',
'Promotional image is not a valid data URL',
HTTPErrorCode.BAD_REQUEST_ERROR,
);
}
await minioClient.putObject(
process.env.MINIO_BUCKETNAME as string,
'service/' + service.name,
convertedFile.buffer,
{ 'Content-Type': convertedFile.mimetype },
);
newService.promotional_image = 'service/' + service.name;
}

newService.day_of_week = service.day_of_week;
newService.start_time = service.start_time;
Expand Down Expand Up @@ -48,10 +68,33 @@ export class ServiceModel {
HTTPErrorCode.NOT_FOUND_ERROR,
);
}
if (service.promotional_image)
service.promotional_image = await minioClient.presignedGetObject(
process.env.MINIO_BUCKETNAME as string,
service.promotional_image as string,
);
return service;
}
public static async updateService(service: Service) {
const service_ic = await UserModel.getUser(service.service_ic_username);
if (!service.promotional_image) service.promotional_image = null;
else {
const convertedFile = dataUrlToBuffer(service.promotional_image);
if (!convertedFile) {
throw new HTTPError(
'Invalid promotional image',
'Promotional image is not a valid data URL',
HTTPErrorCode.BAD_REQUEST_ERROR,
);
}
await minioClient.putObject(
process.env.MINIO_BUCKETNAME as string,
'service/' + service.name,
convertedFile.buffer,
);
service.promotional_image = 'service/' + service.name;
}

service.service_ic = service_ic;
try {
await appDataSource.manager.update(Service, { service_id: service.service_id }, service);
Expand All @@ -69,6 +112,13 @@ export class ServiceModel {
.select('service')
.from(Service, 'service')
.getMany();
for (const service of services) {
if (service.promotional_image)
service.promotional_image = await minioClient.presignedGetObject(
process.env.MINIO_BUCKETNAME as string,
service.promotional_image as string,
);
}
return services;
}
public static async createServiceSession(
Expand Down
74 changes: 64 additions & 10 deletions interapp-backend/api/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -373,16 +373,20 @@ export class UserModel {
HTTPErrorCode.NOT_FOUND_ERROR,
);
}
const users: Partial<User>[] = await appDataSource.manager
.createQueryBuilder()
.select(['user'])
.from(User, 'user')
.where('user.username IN (:...usernames)', { usernames })
.getMany();
users.forEach((user) => {
delete user.password_hash;
delete user.refresh_token;
});
const users: Pick<User, 'username' | 'user_id' | 'email' | 'verified' | 'service_hours'>[] =
await appDataSource.manager
.createQueryBuilder()
.select([
'user.username',
'user.user_id',
'user.email',
'user.verified',
'user.service_hours',
])
.from(User, 'user')
.where('user.username IN (:...usernames)', { usernames })
.getMany();

return users;
}
public static async addServiceUser(service_id: number, username: string) {
Expand Down Expand Up @@ -423,6 +427,56 @@ export class UserModel {
public static async removeServiceUser(service_id: number, username: string) {
await appDataSource.manager.delete(UserService, { service_id, username });
}
public static async updateServiceUserBulk(
service_id: number,
data: { action: 'add' | 'remove'; username: string }[],
) {
const service = await appDataSource.manager
.createQueryBuilder()
.select(['service'])
.from(Service, 'service')
.where('service.service_id = :service_id', { service_id })
.getOne();
if (!service)
throw new HTTPError(
'Service not found',
`Service with id ${service_id} was not found`,
HTTPErrorCode.NOT_FOUND_ERROR,
);

const findUsers = async (usernames: string[]) => {
if (findUsers.length === 0) return [];
return await appDataSource.manager
.createQueryBuilder()
.select(['user'])
.from(User, 'user')
.where('user.username IN (:...usernames)', { usernames })
.getMany();
};

const toAdd = data
.filter(({ action, username }) => action === 'add')
.map((data) => data.username);
const toRemove = data
.filter(({ action, username }) => action === 'remove')
.map((data) => data.username);

if (toAdd.length !== 0)
await appDataSource.manager.insert(
UserService,
(await findUsers(toAdd)).map((user) => ({
service_id,
username: user.username,
service,
user,
})),
);
if (toRemove.length !== 0)
await appDataSource.manager.delete(
UserService,
toRemove.map((username) => ({ service_id, username })),
);
}
public static async updateServiceHours(username: string, hours: number) {
const user = await appDataSource.manager
.createQueryBuilder()
Expand Down
Loading