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

Features/society dropdown #54

Merged
merged 20 commits into from
Dec 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
88b8564
Merge branch 'main' of https://github.com/devsoc-unsw/trainee-fool-24t3
Gyoumi Dec 13, 2024
6afd04f
Merge branch 'main' of https://github.com/devsoc-unsw/trainee-fool-24t3
Gyoumi Dec 13, 2024
39dcab2
Merge branch 'main' of https://github.com/devsoc-unsw/trainee-fool-24t3
Gyoumi Dec 13, 2024
2f2b00e
Merge branch 'main' of https://github.com/devsoc-unsw/trainee-fool-24t3
Gyoumi Dec 13, 2024
b50b3c2
Merge branch 'main' of https://github.com/devsoc-unsw/trainee-fool-24t3
Gyoumi Dec 14, 2024
b7d2c33
Merge branch 'main' of https://github.com/devsoc-unsw/trainee-fool-24t3
Gyoumi Dec 14, 2024
c3e9747
added redirection upon registration and logging in
Gyoumi Dec 13, 2024
e10e23c
began working on file upload for event creation
Gyoumi Dec 13, 2024
b5d24d0
frontend side of upload image banner
Gyoumi Dec 13, 2024
2632bfa
rebasing latest pr
Gyoumi Dec 13, 2024
c286eb0
Added image uploading and linked to supabase storage. added input box…
Gyoumi Dec 14, 2024
4c09494
create event page + society dropdown complete
Gyoumi Dec 14, 2024
1a063c6
deleting otp upon success is important
Gyoumi Dec 14, 2024
5fce307
🧹 fix frontend errors
lachlanshoesmith Dec 14, 2024
41d0a31
🧪 make storage-related env vars optional for test env
lachlanshoesmith Dec 14, 2024
49f7dbb
❌ rm lockfile
lachlanshoesmith Dec 14, 2024
f3578b4
🧪 fix borked tests
lachlanshoesmith Dec 15, 2024
19bf8f4
🧪 fix borked tests pt 2
lachlanshoesmith Dec 15, 2024
53c43c1
Merge branch 'features/society-dropdown' of https://github.com/devsoc…
Gyoumi Dec 15, 2024
788a229
🔧 fixed otp test
Gyoumi Dec 15, 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
2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
"license": "ISC",
"dependencies": {
"@prisma/client": "^5.22.0",
"@supabase/storage-js": "^2.7.1",
"base64-arraybuffer": "^1.0.2",
"bcrypt": "^5.1.1",
"connect-redis": "^7.1.1",
"cors": "^2.8.5",
Expand Down
68 changes: 68 additions & 0 deletions backend/src/config/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { StorageClient } from '@supabase/storage-js';
import { decode } from 'base64-arraybuffer';

const STORAGE_URL = process.env['STORAGE_URL'];
const SERVICE_KEY = process.env['SERVICE_ROLE'];

if (process.env['NODE_ENV'] !== 'test' && !STORAGE_URL) {
throw new Error('Storage URL not found.');
}
if (process.env['NODE_ENV'] !== 'test' && !SERVICE_KEY) {
throw new Error('Service key not found.');
}

export let storageClient: StorageClient | null = null;
if (STORAGE_URL && SERVICE_KEY) {
storageClient = new StorageClient(STORAGE_URL, {
apikey: SERVICE_KEY,
Authorization: `Bearer ${SERVICE_KEY}`,
});
}

export const uploadFile = async (
file: string,
fileType: string,
societyId: number,
eventName: string
) => {
if (!storageClient) {
throw new Error('Storage client not initialised.');
}
const { data, error } = await storageClient
.from('images')
.upload(
`${societyId}/${eventName}/banner.${fileType.split('/')[1]}`,
decode(file),
{
contentType: fileType,
upsert: true,
}
);
if (error) {
throw new Error(error.message);
}
console.log(data);
return data.path;
};

export const getFile = async (path: string) => {
if (!storageClient) {
throw new Error('Storage client not initialised.');
}
const { data, error } = await storageClient.from('images').download(path);
if (error) {
throw new Error(error.message);
}

return data;
};

export const getFileUrl = async (path: string) => {
if (!storageClient) {
throw new Error('Storage client not initialised.');
}

const { data } = await storageClient.from('images').getPublicUrl(path);

return data;
};
118 changes: 92 additions & 26 deletions backend/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ import {
findUserFromId,
updateUserPasswordFromEmail,
} from './routes/User/user';
import {
getFile,
getFileUrl,
storageClient,
uploadFile,
} from './config/storage';

declare module 'express-session' {
interface SessionData {
Expand Down Expand Up @@ -78,7 +84,7 @@ app.use(
credentials: true,
})
);
app.use(express.json());
app.use(express.json({ limit: '20mb' }));

if (process.env['SESSION_SECRET'] === undefined) {
console.error('Session secret not provided in .env file');
Expand Down Expand Up @@ -191,7 +197,7 @@ app.post('/auth/otp/generate', async (req: Request, res: Response) => {
});
}

if (process.env['CI']) {
if (process.env['NODE_ENV'] === 'test') {
return res.status(200).json({ message: token });
}
return res.status(200).json({ message: 'ok' });
Expand Down Expand Up @@ -256,6 +262,8 @@ app.post('/auth/password/forgot', async (req: Request, res: Response) => {

await updateUserPasswordFromEmail(email, password, SALT_ROUNDS);

await redisClient.del(email);

return res.status(200).json({ message: 'ok' });
} catch (error) {
return res.status(400).json({
Expand Down Expand Up @@ -626,10 +634,33 @@ app.post(
return res.status(400).json({ message: 'Invalid date' });
}

//console.log(event);

// upload image to storage and get link
let imagePath = '';
try {
if (event.banner && storageClient) {
const metaData = event.banner.metaData;
const base64Data = event.banner.buffer.split(',')[1];
if (base64Data) {
imagePath = await uploadFile(
base64Data,
metaData.type,
event.societyId,
event.name
);
} else {
throw new Error('Invalid base64 string.');
}
}
} catch (error) {
return res.status(400).json({ message: `Unable to upload image.` });
}

try {
const eventRes = await prisma.event.create({
data: {
banner: event.banner,
banner: imagePath,
name: event.name,
startDateTime: dayjs(event.startDateTime).toISOString(),
endDateTime: dayjs(event.endDateTime).toISOString(),
Expand All @@ -644,6 +675,7 @@ app.post(
});
return res.status(200).json(eventRes);
} catch (e) {
console.log(e);
return res.status(400).json({ message: 'Invalid event input' });
}
}
Expand Down Expand Up @@ -675,21 +707,53 @@ app.put('/event', async (req: TypedRequest<UpdateEventBody>, res: Response) => {
return res.status(400).json({ message: 'Invalid date' });
}

// upload image to storage and get link
let imagePath;
try {
if (Object.keys(event.banner).length > 0) {
const base64Data = req.body.banner.buffer.split(',')[1];
if (base64Data) {
const metaData = req.body.banner.metaData;

imagePath = await uploadFile(
base64Data,
metaData.type,
event.societyId,
event.name
);
} else {
throw new Error('Invalid base64 string.');
}
} else {
throw new Error('No banner submitted.');
}
} catch (error) {
return res.status(400).json({ error: (error as Error).message });
}

try {
const eventRes = await prisma.event.update({
where: {
id: eventID,
},
data: {
banner: req.body.banner,
banner: imagePath,
name: req.body.name,
startDateTime: dayjs(req.body.startDateTime).toISOString(),
endDateTime: dayjs(req.body.endDateTime).toISOString(),
location: req.body.location,
description: req.body.description,
},
});
return res.status(200).json(eventRes);
// we are choosing to send the image back as a url
let imageFile;
try {
imageFile = await getFileUrl(event.banner); // getFile(event.banner) if we wanted raw file
} catch (error) {
return res.status(400).json({ error: (error as Error).message });
}

return res.status(200).json({ ...event, banner: imageFile });
} catch (e) {
return res.status(400).json({ message: 'Invalid event input' });
}
Expand Down Expand Up @@ -1175,16 +1239,16 @@ app.get('/user/keywords', async (req, res: Response) => {

const userID = sessionFromDB.userId;

let userKeywords = await prisma.user.findFirst({
where: {
id: userID,
},
select: {
keywords: true,
}
});
if (userKeywords === null) return res.status(404).json([]);
let userKeywords = await prisma.user.findFirst({
where: {
id: userID,
},
select: {
keywords: true,
},
});

if (userKeywords === null) return res.status(404).json([]);

return res.status(200).json(userKeywords.keywords);
});
Expand Down Expand Up @@ -1224,21 +1288,21 @@ app.post(
});
return res.status(200).json(newKeyword);
} catch (e) {
return res.status(400).json({ message: "Invalid keyword input." });
return res.status(400).json({ message: 'Invalid keyword input.' });
}
}
);

// associates a user with a keyword
app.post(
"/user/keyword",
'/user/keyword',
async (req: TypedRequest<keywordIdBody>, res: Response) => {
//get userid from session
const sessionFromDB = await validateSession(
req.session ? req.session : null
);
if (!sessionFromDB) {
return res.status(401).json({ message: "Invalid session provided." });
return res.status(401).json({ message: 'Invalid session provided.' });
}
const userID = sessionFromDB.userId;

Expand All @@ -1253,7 +1317,7 @@ app.post(
});

if (!keywordExists) {
return res.status(404).json({ message: "Invalid keyword." });
return res.status(404).json({ message: 'Invalid keyword.' });
}

//Connect keyword and user
Expand Down Expand Up @@ -1283,19 +1347,20 @@ app.post(
},
});

return res.status(200).json({ message: "ok" });
});
return res.status(200).json({ message: 'ok' });
}
);

// disassociates a user with a keyword
app.delete(
"/user/keyword",
'/user/keyword',
async (req: TypedRequest<keywordIdBody>, res: Response) => {
//get userid from session
const sessionFromDB = await validateSession(
req.session ? req.session : null
);
if (!sessionFromDB) {
return res.status(401).json({ message: "Invalid session provided." });
return res.status(401).json({ message: 'Invalid session provided.' });
}
const userID = sessionFromDB.userId;

Expand All @@ -1310,7 +1375,7 @@ app.delete(
});

if (!societyId) {
return res.status(400).json({ message: "Invalid keyword." });
return res.status(400).json({ message: 'Invalid keyword.' });
}

//Disconnect keyword and user
Expand Down Expand Up @@ -1340,8 +1405,9 @@ app.delete(
},
});

return res.status(200).json({ message: "ok" });
});
return res.status(200).json({ message: 'ok' });
}
);

app.get('/hello', () => {
console.log('Hello World!');
Expand Down
11 changes: 10 additions & 1 deletion backend/src/requestTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export interface UserIdBody {
}

export interface CreateEventBody {
banner: string;
banner: Base64Image;
name: string;
startDateTime: Date;
endDateTime: Date;
Expand All @@ -33,6 +33,15 @@ export interface CreateEventBody {
societyId: number;
}

interface Base64Image {
buffer: string,
metaData: {
name: string,
type: string,
size: number,
}
}

export interface UpdateEventBody extends CreateEventBody {
id: number;
}
Expand Down
10 changes: 4 additions & 6 deletions backend/tests/otp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import app from '../src/index';
import { createClient } from 'redis';
import prisma from '../src/prisma';

describe.skip('Password change', () => {
describe('Password change', () => {
test('Forgot password OTP', async () => {
const redisClient = createClient({
url: `redis://localhost:${process.env['REDIS_PORT']}`,
Expand Down Expand Up @@ -45,6 +45,7 @@ describe.skip('Password change', () => {
const expToken = await redisClient.get(newUser.email);

expect(expToken).not.toBeNull();

expect(expToken).toEqual(expResToken);

await new Promise((resolve) => setTimeout(resolve, 1000));
Expand Down Expand Up @@ -106,14 +107,11 @@ describe.skip('Password change', () => {
const forgotRes = await request(app).post('/auth/password/forgot').send({
email: '[email protected]',
token: fResToken,
newPassword: 'oraclefan1',
password: 'oraclefan1',
});

//console.log(forgotRes.error.text);
expect(forgotRes.status).toBe(200);

await new Promise((resolve) => setTimeout(resolve, 1000));

const fCheckVerTokens = await redisClient.get(newUser.email);
expect(fCheckVerTokens).toBeNull();

Expand Down Expand Up @@ -194,7 +192,7 @@ describe.skip('Password change', () => {
username: 'richard grayson',
password: 'iheartkori',
});
expect(updateResFail.status).toBe(400);
expect(oldPassResFail.status).toBe(400);

const newPassRes = await request(app).post('/auth/login').send({
username: 'richard grayson',
Expand Down
Loading
Loading