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
22 changes: 22 additions & 0 deletions server/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,31 @@ services:
networks:
- notangles_network_new

minio:
container_name: notangles-images
hostname: notangles_usr_images
restart: always # Is this necessary for pfps?
image: minio/minio:RELEASE.2025-04-22T22-12-26Z
ports:
- '9000:9000'
- '9001:9001'
environment:
- MINIO_ROOT_USER=${MINIO_ROOT_USER}
- MINIO_ROOT_PASSWORD=${MINIO_ROOT_PASSWORD}
volumes:
- minio:/data
command: server /data --console-address ":9001"
networks:
- notangles_usr_images

volumes:
postgres:
name: server-new
minio:
name: usr-images

networks:
notangles_network_new:
driver: bridge
notangles_usr_images:
driver: bridge
2 changes: 2 additions & 0 deletions server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"graphql": "16.11.0",
"graphql-request": "7.2.0",
"graphql-tag": "2.12.6",
"minio": "^8.0.5",
"nestjs-minio-client": "^2.2.0",
"openid-client": "6.6.2",
"passport": "0.7.0",
"passport-custom": "1.1.1",
Expand Down
351 changes: 351 additions & 0 deletions server/pnpm-lock.yaml

Large diffs are not rendered by default.

8 changes: 8 additions & 0 deletions server/src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { GuestStrategy } from './guest.strategy';
import { getConfig, OidcStrategy } from './oidc.strategy';
import { SessionSerializer } from './session.serializer';
import { GraphqlService } from 'src/graphql/graphql.service';
import { MinioModule } from 'nestjs-minio-client';

const OidcStrategyFactory = {
provide: 'OidcStrategy',
Expand All @@ -22,6 +23,13 @@ const OidcStrategyFactory = {

@Module({
imports: [
MinioModule.register({
endPoint: process.env.MINIO_ENDPOINT ?? 'localhost',
port: 9000,
useSSL: false,
accessKey: process.env.MINIO_ACCESS_KEY!,
secretKey: process.env.MINIO_SECRET_KEY!,
}),
PassportModule.register({ session: true, defaultStrategy: 'oidc' }),
],
controllers: [AuthController],
Expand Down
5 changes: 3 additions & 2 deletions server/src/auth/auth.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export class AuthService {
if (provider === undefined || subject === undefined) {
throw new Error('Provider and subject must be defined for auth users');
}
const currentYear = new Date().getFullYear().toString();
const currentYear = new Date().getFullYear();
const { availableTerms } =
await this.graphql.getAvailableTermsFrom(currentYear);
if (availableTerms.length === 0) {
Expand Down Expand Up @@ -75,7 +75,7 @@ export class AuthService {
}

private async createGuest(params: OnboardParams): Promise<User> {
const currentYear = new Date().getFullYear().toString();
const currentYear = new Date().getFullYear();
const { availableTerms } =
await this.graphql.getAvailableTermsFrom(currentYear);
if (availableTerms.length === 0) {
Expand All @@ -96,6 +96,7 @@ export class AuthService {
name: this.TIMETABLE_DEFAULT_NAME,
year,
term,
primary: true,
};
}),
},
Expand Down
2 changes: 1 addition & 1 deletion server/src/graphql/graphql.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export class GraphqlService {
}

async getAvailableTermsFrom(
currentYear: string = new Date().getFullYear().toString(),
currentYear: number = new Date().getFullYear(),
): Promise<{ availableTerms: string[] }> {
const result = await this.sdk.GetAvailableTerms({
currentYear: currentYear,
Expand Down
2 changes: 1 addition & 1 deletion server/src/graphql/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export const CLASS_DETAILS = gql(`
`);

export const GET_AVAILABLE_TERMS = gql(`
query GetAvailableTerms($currentYear: String!) {
query GetAvailableTerms($currentYear: Int!) {
classes(
where: {term: {_in: ["T1", "T2", "T3", "U1"]}, year: {_gte: $currentYear}}
distinct_on: offering_period
Expand Down
11 changes: 11 additions & 0 deletions server/src/user/user.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,20 @@ import { GraphqlService } from 'src/graphql/graphql.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { UserService } from './user.service';
import { UserController } from './user.controller';
import { MinioModule } from 'nestjs-minio-client';

@Module({
imports: [
MinioModule.register({
endPoint: process.env.MINIO_ENDPOINT ?? 'localhost',
port: 9000,
useSSL: false,
accessKey: process.env.MINIO_ACCESS_KEY!,
secretKey: process.env.MINIO_SECRET_KEY!,
}),
],
providers: [UserService, PrismaService, GraphqlService],
controllers: [UserController],
exports: [UserService],
})
export class UserModule {}
80 changes: 78 additions & 2 deletions server/src/user/user.service.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,15 @@
import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/prisma/prisma.service';
import { UserInfo, UserSettings } from './types';
import { MinioService } from 'nestjs-minio-client';
import * as crypto from 'crypto';

@Injectable({})
export class UserService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly minio: MinioService,
) {}

async deleteUser(userId: string): Promise<void> {
await this.prisma.user.delete({
Expand All @@ -30,16 +35,87 @@ export class UserService {
};
}

async uploadImage(image: string): Promise<{ url: string }> {
// expected image format: data:<mime-type>;base64,<data>
const validMimeTypes = ['image/jpeg', 'image/png', 'image/jpg'];
const mimeType = image.match(/[^:]\w+\/[\w-+\d.]+(?=;|,)/);

if (!mimeType || !validMimeTypes.includes(mimeType[0])) {
throw new Error('Invalid image mime type');
}

const tempFileName = Date.now().toString();
const hash = crypto.createHash('md5');
hash.update(tempFileName);
const hashedFileName = hash.digest('hex');
const fileName = hashedFileName;

const bucketName = process.env.MINIO_BUCKET_NAME;

try {
await this.minio.client.bucketExists(bucketName!);
} catch (err) {
throw new Error('Error checking if bucket exists: ' + err);
}

try {
await this.minio.client.putObject(bucketName!, fileName, image);
} catch (err) {
throw new Error('Error uploading file to MinIO: ' + err);
}

return {
url: `${process.env.MINIO_PUBLIC_URL}/${bucketName}/${fileName}`,
};
}

async setProfilePicture(
userId: string,
profilePictureUrl: string,
): Promise<void> {
const data = await this.prisma.user.findUniqueOrThrow({
where: {
id: userId,
},
select: {
profilePictureUrl: true,
},
});

const bucketName = process.env.MINIO_BUCKET_NAME;
const fullUrl = data.profilePictureUrl;
const temp = fullUrl?.lastIndexOf('/');
const fileName = fullUrl?.substring(temp! + 1);

if (data.profilePictureUrl) {
try {
await this.minio.client.removeObject(bucketName!, fileName!);
} catch (err) {
throw new Error('Error removing object from MinIO: ' + err);
}
}

if (!profilePictureUrl) {
// empty string is passed in, set the field to null (client will show the default image)
await this.prisma.user.update({
where: {
id: userId,
},
data: {
profilePictureUrl: '',
},
});
return;
}

const minioImageLink = await this.uploadImage(profilePictureUrl);

await this.prisma.user.update({
where: {
id: userId,
},
data: {
profilePictureUrl,
profilePictureUrl: minioImageLink.url,
},
});
}
Expand Down
Loading