diff --git a/.env.example b/.env.example index 75757ba6..3b99fdf7 100644 --- a/.env.example +++ b/.env.example @@ -70,4 +70,4 @@ DISCORD_WEBHOOK_CONTACT= # Some more random variables API_RATE_LIMIT=12 -API_CART_LIFESPAN=3600 \ No newline at end of file +API_CART_LIFESPAN=3600 diff --git a/.gitignore b/.gitignore index 07a848ba..ff8b81ce 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,6 @@ pnpm-error.log openapi.json -/docs/build \ No newline at end of file +/docs/build + +/assets/badges \ No newline at end of file diff --git a/assets/badges/back-fullaccess.png b/assets/badges/back-fullaccess.png deleted file mode 100644 index ab018a4b..00000000 Binary files a/assets/badges/back-fullaccess.png and /dev/null differ diff --git a/assets/badges/back-orgaprice.png b/assets/badges/back-orgaprice.png deleted file mode 100644 index 84499203..00000000 Binary files a/assets/badges/back-orgaprice.png and /dev/null differ diff --git a/assets/badges/back-restricted.png b/assets/badges/back-restricted.png deleted file mode 100644 index c14e8553..00000000 Binary files a/assets/badges/back-restricted.png and /dev/null differ diff --git a/assets/badges/badge-fullaccess.png b/assets/badges/badge-fullaccess.png deleted file mode 100644 index ab018a4b..00000000 Binary files a/assets/badges/badge-fullaccess.png and /dev/null differ diff --git a/assets/badges/badge-orgaprice.png b/assets/badges/badge-orgaprice.png deleted file mode 100644 index 84499203..00000000 Binary files a/assets/badges/badge-orgaprice.png and /dev/null differ diff --git a/assets/badges/badge-restricted.png b/assets/badges/badge-restricted.png deleted file mode 100644 index c14e8553..00000000 Binary files a/assets/badges/badge-restricted.png and /dev/null differ diff --git a/assets/defaultbadge/blank.png b/assets/defaultbadge/blank.png new file mode 100644 index 00000000..c3dce812 Binary files /dev/null and b/assets/defaultbadge/blank.png differ diff --git a/src/controllers/admin/badges/generateBadges.ts b/src/controllers/admin/badges/generateBadges.ts index e8a5f960..9541c99d 100644 --- a/src/controllers/admin/badges/generateBadges.ts +++ b/src/controllers/admin/badges/generateBadges.ts @@ -10,7 +10,7 @@ import { hasPermission } from '../../../middlewares/authentication'; const getCommisionPermission = (commissionRole: string, commissionId: string) => { switch (commissionId) { case 'vieux': { - return 'restricted'; + return 'invite'; } case 'coord': { @@ -33,10 +33,6 @@ const getCommisionPermission = (commissionRole: string, commissionId: string) => if (commissionRole === 'respo') return 'fullaccess'; } - case 'ssl': { - if (commissionRole === 'respo') return 'fullaccess'; - } - default: { return 'orgaprice'; } @@ -73,6 +69,9 @@ export default [ case 'orgas': { await database.user .findMany({ + orderBy: { + firstname: 'desc', + }, where: { permissions: { contains: 'orga' }, }, @@ -108,6 +107,8 @@ export default [ user.orga.roles[mainCommissionIndex].commission.id, user.orga.roles[mainCommissionIndex].commission.nameOnBadge, ), + place: user.place, + firstaid: !!user.permissions.includes('firstaid'), }); } }); @@ -161,6 +162,7 @@ export default [ firstName: user[0].firstname, lastName: user[0].lastname, image: `${env.front.website}/uploads/files/orga/${user[0].orga.photoFilename}.webp`, + firstaid: !!user[0].permissions.includes('firstaid'), commissionName: getCommissionName( user[0].orga.roles[mainCommissionIndex].commissionRole, user[0].orga.roles[mainCommissionIndex].commission.id, @@ -173,22 +175,12 @@ export default [ case 'singlecustom': { listBadgeToGenerate.push({ - type: getCommisionPermission(field.commissionRole ?? 'member', field.commissionId ?? 'vieux'), + type: field.permission, firstName: field.firstname ?? '', lastName: field.lastname ?? '', - image: '', - commissionName: await database.commission - .findUnique({ - where: { id: field.commissionId ?? 'vieux' }, - select: { nameOnBadge: true }, - }) - .then((commission) => - getCommissionName( - field.commissionRole ?? 'member', - field.commissionId ?? 'vieux', - commission.nameOnBadge, - ), - ), + image: field.image ?? '', + commissionName: field.commissionId, + place: field.place, }); break; } diff --git a/src/controllers/admin/openapi.yml b/src/controllers/admin/openapi.yml index 10d5e1ab..d38c580d 100644 --- a/src/controllers/admin/openapi.yml +++ b/src/controllers/admin/openapi.yml @@ -103,7 +103,7 @@ description: Le nombre de badges à créer permission: type: string - enum: [restricted, orgaprice, fullaccess] + enum: [restricted, orgaprice, fullaccess, invite] description: La permission associée au badge email: type: string diff --git a/src/operations/user.ts b/src/operations/user.ts index df7a17ef..6b1ca6bf 100644 --- a/src/operations/user.ts +++ b/src/operations/user.ts @@ -132,6 +132,7 @@ export const fetchUsers = async ( { username: { contains: query.search } }, { email: { contains: query.search } }, { id: { contains: query.search } }, + { discordId: { contains: query.search } }, { team: { name: { contains: query.search }, diff --git a/src/types.ts b/src/types.ts index 42af333b..c189dbc5 100755 --- a/src/types.ts +++ b/src/types.ts @@ -64,6 +64,7 @@ export enum Permission { admin = 'admin', repo = 'repo', orga = 'orga', + firstaid = 'firstaid', } export { TransactionState, UserAge, UserType, ItemCategory, Log, RepoItemType } from '@prisma/client'; @@ -236,7 +237,7 @@ export type Tournament = PrimitiveTournament & { /************/ export type BadgeType = 'orgas' | 'custom' | 'single' | 'singlecustom'; -export type BadgePermission = 'restricted' | 'orgaprice' | 'fullaccess'; +export type BadgePermission = 'restricted' | 'orgaprice' | 'fullaccess' | 'invite'; export interface Badge { type: BadgePermission; @@ -244,6 +245,8 @@ export interface Badge { lastName: string; image: string; commissionName: string; + place?: string; + firstaid?: boolean; } export interface BadgeField { @@ -256,6 +259,8 @@ export interface BadgeField { firstname?: string; lastname?: string; name?: string; + image?: string; + place?: string; } /**********/ diff --git a/src/utils/badge.ts b/src/utils/badge.ts index f12adaae..3be8a9f1 100644 --- a/src/utils/badge.ts +++ b/src/utils/badge.ts @@ -3,19 +3,27 @@ import { readFileSync } from 'fs'; import PDFkit from 'pdfkit'; import sharp from 'sharp'; import { Badge } from '../types'; +import env from './env'; -const loadImageBadgeRestricted = () => - `data:image/png;base64,${readFileSync(`assets/badges/badge-restricted.png`, 'base64')}`; -const loadImageBadgeOrgaPrice = () => - `data:image/png;base64,${readFileSync(`assets/badges/badge-orgaprice.png`, 'base64')}`; -const loadImageBadgeFullAccess = () => - `data:image/png;base64,${readFileSync(`assets/badges/badge-fullaccess.png`, 'base64')}`; +const getImage = (filename: string) => { + try { + return `data:image/png;base64,${readFileSync(`assets/badges/${filename}`, 'base64')}`; + } catch { + return `data:image/png;base64,${readFileSync(`assets/defaultbadge/blank.png`, 'base64')}`; + } +}; + +const loadImageBadgeRestricted = () => getImage('badge-restricted.png'); +const loadImageBadgeOrgaPrice = () => getImage('badge-orgaprice.png'); +const loadImageBadgeFullAccess = () => getImage('badge-fullaccess.png'); +const loadImageBadgeInvite = () => getImage('badge-invite.png'); -const loadBackRestricted = () => `data:image/png;base64,${readFileSync(`assets/badges/back-restricted.png`, 'base64')}`; -const loadBackOrgaPrice = () => `data:image/png;base64,${readFileSync(`assets/badges/back-orgaprice.png`, 'base64')}`; -const loadBackFullAccess = () => `data:image/png;base64,${readFileSync(`assets/badges/back-fullaccess.png`, 'base64')}`; +const loadBackRestricted = () => getImage('back-restricted.png'); +const loadBackOrgaPrice = () => getImage('back-orgaprice.png'); +const loadBackFullAccess = () => getImage('back-fullaccess.png'); +const loadBackInvite = () => getImage('back-invite.png'); -type BadgePermission = 'restricted' | 'orgaprice' | 'fullaccess'; +type BadgePermission = 'restricted' | 'orgaprice' | 'fullaccess' | 'invite'; const getBack = (permission: BadgePermission): string => { switch (permission) { @@ -31,6 +39,10 @@ const getBack = (permission: BadgePermission): string => { return loadBackFullAccess(); } + case 'invite': { + return loadBackInvite(); + } + default: { return loadBackRestricted(); } @@ -51,6 +63,10 @@ const getBadge = (permission: BadgePermission): string => { return loadImageBadgeFullAccess(); } + case 'invite': { + return loadImageBadgeInvite(); + } + default: { return loadImageBadgeRestricted(); } @@ -114,6 +130,7 @@ export const generateBadge = async (badges: Badge[]) => { // Informations about badge const image = await fetchImage(badges[index].image); + const firstaid = await fetchImage(`${env.front.website}/uploads/files/badges/first-aid.png`); // Coordonates const x = pictureX + col * columnOffset; const y = pictureY + row * rowOffset; @@ -127,14 +144,15 @@ export const generateBadge = async (badges: Badge[]) => { } // Background - document.image(getBadge(badges[index].type), x, y, { width: pictureSize }); // After the image because of... 42 + document.image(await getBadge(badges[index].type), x, y, { width: pictureSize }); // After the image because of... 42 + + // FirstAid + if (badges[index].firstaid) { + document.image(firstaid, x + 60, y + 216, { width: pictureSize - 120 }); + } } } - // Place the text containing the name is the bottom middle in bold and in uppercase - // Define a text format - const textFormat = document.font(fontFamily).fill([239, 220, 235]).fontSize(fontSize); - // 'for' because I dont like to repeat but I like potatoes and pain au chocolat for (let col = 0; col < columns; col++) { for (let row = 0; row < rows; row++) { @@ -142,6 +160,12 @@ export const generateBadge = async (badges: Badge[]) => { if (index >= badges.length) break; + // Place the text containing the name is the bottom middle in bold and in uppercase + // Define a text format + const color: PDFKit.Mixins.ColorValue = badges[index].type === 'fullaccess' ? [239, 220, 235] : [23, 18, 74]; + + const textFormat = document.font(fontFamily).fill(color).fontSize(fontSize); + // Informations about badge const lastName = `${badges[index].lastName || ' '}`; const firstName = `${badges[index].firstName || ' '}`; @@ -153,7 +177,7 @@ export const generateBadge = async (badges: Badge[]) => { textFormat.text( lastName.toUpperCase(), offsetX - textFormat.widthOfString(lastName.toUpperCase()) / 2, - offsetY - 277 - lastNameHeight / 2, + offsetY - 282 - lastNameHeight / 2, ); // Firstname const firstNameHeight = textFormat.heightOfString(firstName); @@ -187,7 +211,21 @@ export const generateBadge = async (badges: Badge[]) => { const y = pictureY + row * rowOffset; // Background - document.image(getBack(badges[index].type), x, y, { width: pictureSize }); // After the image because of... 42 + document.image(await getBack(badges[index].type), x, y, { width: pictureSize }); // After the image because of... 42 + + const color: PDFKit.Mixins.ColorValue = [23, 18, 74]; + const textFormat = document.font(fontFamily).fill(color).fontSize(fontSize); + + // Offsets + const offsetX = textX + (3 - col) * columnOffset; + const offsetY = textY + row * rowOffset; + + // Place + const place = badges[index].place + ? `${badges[index].place}` + : `Z${(index + 501).toString().padStart(3, '0')}`; + const placeHeight = textFormat.heightOfString(place); + textFormat.text(place, offsetX - 75, offsetY - 231 - placeHeight / 2); } } diff --git a/src/utils/env.ts b/src/utils/env.ts index 65911060..9b91e6d9 100755 --- a/src/utils/env.ts +++ b/src/utils/env.ts @@ -3,7 +3,7 @@ import crypto from 'crypto'; import dotenv, { DotenvPopulateInput } from 'dotenv'; if (process.env.NODE_ENV === 'test') { - // Make sure to only load the 3 accepted variables in test + // Make sure to only load the accepted variables in test const environmentVariables: DotenvPopulateInput = {}; dotenv.config({ path: '.env.test', processEnv: environmentVariables }); process.env.DATABASE_URL = environmentVariables.DATABASE_URL ?? process.env.DATABASE_URL; diff --git a/src/utils/validators.ts b/src/utils/validators.ts index d359728a..69fcfec6 100644 --- a/src/utils/validators.ts +++ b/src/utils/validators.ts @@ -3,8 +3,8 @@ import { UserAge, UserType, Permission, Error as ResponseError } from '../types' // Matches with LoL EUW summoner name const usernameRegex = /^[0-9\p{L} _#-]{3,22}$/u; -const nameRegex = /^[\p{L}\d _'#-]{1,100}$/u; -const lastnameRegex = /^[\p{L} _'-]{1,100}$/u; +const nameRegex = /^[\p{L}\d _'#-]{3,30}$/u; +const lastnameRegex = /^[\p{L} _'-]{1,50}$/u; const passwordRegex = /^.{6,100}$/; const placeRegex = /^[A-Z]\d{1,3}$/; diff --git a/tests/admin/badges/generateBadges.test.ts b/tests/admin/badges/generateBadges.test.ts index 97f5c539..5a077a4d 100644 --- a/tests/admin/badges/generateBadges.test.ts +++ b/tests/admin/badges/generateBadges.test.ts @@ -208,10 +208,11 @@ describe('POST /admin/badges', () => { fields: [ { type: 'singlecustom', - commissionRole: 'member', - commissionId: 'vieux', + commissionId: 'dev / annimation', + permission: 'orgaprice', firstname: 'John', lastname: 'Doe', + firstaid: true, }, ], }) diff --git a/tests/users/createCart.test.ts b/tests/users/createCart.test.ts index 06d23380..82b8a812 100644 --- a/tests/users/createCart.test.ts +++ b/tests/users/createCart.test.ts @@ -309,6 +309,24 @@ describe('POST /users/current/carts', () => { .expect(404, { error: Error.ItemNotFound }); }); + it('should fail as the user is not a player or a coach or a spectator', async () => { + const attendantUser = await createFakeUser({ type: UserType.attendant }); + + await request(app) + .post(`/users/current/carts`) + .set('Authorization', `Bearer ${token}`) + .send({ + tickets: { userIds: [attendantUser.id] }, + supplements: [], + }) + .expect(403, { error: Error.NotPlayerOrCoachOrSpectator }); + + // Delete the user to not make the results wrong for the success test + await database.cartItem.deleteMany({ where: { forUserId: attendantUser.id } }); + await database.cart.deleteMany({ where: { userId: attendantUser.id } }); + await database.user.delete({ where: { id: attendantUser.id } }); + }); + it('should fail as the user is already paid', async () => { const paidUser = await createFakeUser({ paid: true, type: UserType.player }); @@ -327,6 +345,26 @@ describe('POST /users/current/carts', () => { await database.user.delete({ where: { id: paidUser.id } }); }); + it('should fail as the user is not in the same team', async () => { + const otherTeam = await createFakeTeam({ members: 1, tournament: 'ssbu', name: 'reallydontcare' }); + const userInOtherTeam = getCaptain(otherTeam); + + await request(app) + .post(`/users/current/carts`) + .set('Authorization', `Bearer ${token}`) + .send({ + tickets: { userIds: [userInOtherTeam.id] }, + supplements: [], + }) + .expect(403, { error: Error.NotInSameTeam }); + + // Delete the user to not make the results wrong for the success test + await database.cartItem.deleteMany({ where: { forUserId: userInOtherTeam.id } }); + await database.cart.deleteMany({ where: { userId: userInOtherTeam.id } }); + await database.team.delete({ where: { captainId: userInOtherTeam.id } }); + await database.user.delete({ where: { id: userInOtherTeam.id } }); + }); + it('should fail with an internal server error (inner try/catch)', () => { sandbox.stub(cartOperations, 'createCart').throws('Unexpected error');