import UserRepo, { BasicUserCredentials, GuideTourResult, PasswordResponse, TOTPQuery, UserAccountQuery, UserAiUsageResult, UserAPIKeyResult, UserInfosQueryResponse, UserQueryResponse, UserSubscription } from "../repositories/user.repo"; import {FetchQueryResponse} from "../../config/SharedInterface"; import System, {EncryptedKey, EncryptedUserKey, UserKey} from "./System"; import {FastifyInstance} from "fastify"; import EmailTemplate from "./EmailTemplate"; import path from "path"; import fs from "fs"; import {Secret, TOTP} from "otpauth"; import Book, {BookProps} from "./Book"; import {JWT} from "@fastify/jwt"; import {AIBrand} from "./AI"; import {ConflictError, NotFoundError} from "../error"; import {getLanguage} from "../context"; import Subscription, { SubscriptionInfo } from "./Subscription"; export type UserLanguage = 'fr' | 'en'; export interface LoginData{ valid:boolean; token?:string; id?:string; message?:string; } export interface UserAPIKey { brand: AIBrand, key: string; } interface UserAccount{ firstName:string; lastName:string; username:string authorName:string; email:string; } export interface TOTPData{ totpCode:string; } export interface GuideTour { [key: string]: boolean; } export interface UserSubscriptionProps { userId: string; subType: string; subTier: number; startDate: string; endDate: string; status: number; } interface UserSubscriptionInfo { subType: string; subTier: number; status: boolean; } export default class User{ private readonly id:string; private firstName: string; private lastName: string; private username: string; private email: string; private platform: string; private accountVerified: boolean; private authorName: string; private writingLang: number; private writingLevel: number; private ritePoints: number; private groupId: number; private balancedCredits: number; private termsAccepted: boolean; constructor(id:string){ this.id = id; this.firstName = ''; this.lastName = ''; this.username = ''; this.email = ''; this.platform = ''; this.accountVerified = false; this.authorName = ''; this.writingLang = 0; this.writingLevel = 1; this.ritePoints = 0; this.groupId = 0; this.balancedCredits = 0; this.termsAccepted = false; } public async getUserInfos(): Promise { const data: UserInfosQueryResponse = await UserRepo.fetchUserInfos(this.id); const userKey: string = await this.getUserKey(data.user_meta); this.firstName = System.decryptDataWithUserKey(data.first_name, userKey); this.lastName = System.decryptDataWithUserKey(data.last_name, userKey); this.username = System.decryptDataWithUserKey(data.username, userKey); this.email = System.decryptDataWithUserKey(data.email, userKey); this.platform = data.plateform; this.accountVerified = data.account_verified === 1; this.authorName = data.author_name ? System.decryptDataWithUserKey(data.author_name, userKey) : ''; this.writingLang = data.writing_lang ? data.writing_lang : 1; this.writingLevel = data.writing_level ? data.writing_level : 1; this.groupId = data.user_group ? data.user_group : 0; this.ritePoints = data.rite_points ? data.rite_points : 0; this.balancedCredits = data.credits_balance ? data.credits_balance : 0; this.termsAccepted = data.term_accepted === 1; } public static async returnUserInfos(userId: string, plateforme: string) { const user: User = new User(userId); await user.getUserInfos(); const books: BookProps[] = await Book.getBooks(userId); const guideTourResult: GuideTourResult[] = await UserRepo.fetchGuideTour(userId, plateforme); const guideTour: GuideTour[] = []; const requestSubscription: UserSubscriptionProps[] = await User.getSubscription(userId); const userSubscriptions:SubscriptionInfo = await Subscription.getSubscriptionInfo(userId); const requestKeys: UserAPIKey[] = await User.getAPIKey(userId); let moneySpentThisMonth: number = 0; if (requestKeys.length > 0) { moneySpentThisMonth = await User.moneySpentThisMonth(userId) } const subscriptions: UserSubscriptionInfo[] = []; for (const userSubscriptionProp of requestSubscription) { subscriptions.push({ subType: userSubscriptionProp.subType, subTier: userSubscriptionProp.subTier, status: userSubscriptionProp.status === 1 }); } if (guideTourResult) { guideTourResult.forEach((tour: GuideTourResult): void => { guideTour.push({ [tour.step_tour]: true }); }); } return { id: user.getId(), name: user.getFirstName(), lastName: user.getLastName(), username: user.getUsername(), email: user.getEmail(), accountVerified: user.isAccountVerified(), authorName: user.getAuthorName(), writingLang: user.getWritingLang(), writingLevel: user.getWritingLevel(), ritePoints: user.getRitePoints(), groupId: user.getGroupId(), termsAccepted: user.isTermsAccepted(), guideTour: guideTour, subscription: subscriptions, aiUsage: moneySpentThisMonth, creditsBalance: userSubscriptions.totalCredits, apiKeys: { gemini: !!requestKeys.find((key: UserAPIKey): boolean => { return key.brand === 'gemini' }), openai: !!requestKeys.find((key: UserAPIKey): boolean => { return key.brand === 'openai' }), anthropic: !!requestKeys.find((key: UserAPIKey): boolean => { return key.brand === 'anthropic' }), }, books: books.map((book: BookProps) => { return { bookId: book.id, title: book.title, subTitle: book.subTitle, }; }) } } public static async isTwoFactorEnabled(userId: string): Promise { return UserRepo.isTwoFactorEnabled(userId); } public static async changePassword(userId: string, password: string, newPassword: string, email: string): Promise { if (await User.validUser(email, password)) { const newPasswordHash: string = await System.hashPassword(newPassword); return await UserRepo.updatePassword(userId, System.hashElement(email), newPasswordHash); } else { throw new Error('Your password is incorrect'); } } public static async deleteAccount(userId: string, email: string): Promise { return UserRepo.deleteAccount(userId, System.hashElement(email)); } public static async resetPassword(newPassword: string, email: string, verifyCode: string): Promise { const encryptPassword: string = await System.hashPassword(newPassword); const user: PasswordResponse = await UserRepo.verifyUserAuth(System.hashElement(email), verifyCode) const userId: string = user.user_id; return await UserRepo.updatePassword(userId, System.hashElement(email), encryptPassword); } public static async verifyCode(verifyCode: string, email: string): Promise { return await UserRepo.fetchVerifyCode(System.hashElement(email), verifyCode); } static async subscribeToBeta(email: string): Promise { let checkEmail: string[] = email.split('@'); checkEmail = checkEmail[1].split('.'); if (checkEmail[0] === 'gmail' || checkEmail[0] === 'hotmail' || checkEmail[0] === 'outlook') { if (await UserRepo.checkBetaSubscriber(email)) { throw new Error(`L'adresse courriel est déjà enregistré.`); } const insertEmail: boolean = await UserRepo.insertBetaSubscriber(email); if (insertEmail) { System.sendEmailTo(email, 'Inscription à la bêta.', '', EmailTemplate('newsletter', {})); return insertEmail; } else { throw new Error('Une erreur est survenue lors de l\'inscription à la bêta.'); } } else { throw new Error('Votre adresse email n\'est pas valide.'); } } public static async addUser(firstName: string, lastName: string, username: string, email: string, notEncryptPassword: string, userId: string | null = null, provider: string = 'credential', socialId: string | null = null, tempAdd: boolean = false): Promise { const originEmail:string = System.hashElement(email); const originUsername:string = System.hashElement(username); await this.checkUser(originUsername, originEmail); const lang: "fr" | "en" = getLanguage() if (tempAdd) { const password:string = notEncryptPassword ? await System.hashPassword(notEncryptPassword) : ''; const newUserId: string = System.createUniqueId(); await UserRepo.insertTempUser(newUserId, firstName, lastName, username, originUsername, email, originEmail, password); await User.sendVerifyCode(username, email); return newUserId; } else { if (!userId) { throw new Error(lang === 'fr' ? 'L\'identifiant utilisateur est requis.' : 'User ID is required.'); } const encryptData: EncryptedUserKey = await System.generateAndEncryptUserKey(userId); const userKey: string = encryptData.userKey; const encryptFirstName: string = System.encryptDataWithUserKey(firstName, userKey); const encryptLastName: string = System.encryptDataWithUserKey(lastName, userKey); const encryptUsername: string = System.encryptDataWithUserKey(username, userKey); const encryptEmail: string = System.encryptDataWithUserKey(email, userKey); const originalEmail: string = System.hashElement(email); const originalUsername: string = System.hashElement(username); const meta: string = System.encryptDateKey(userId); const insertedUserId: string = await UserRepo.insertUser(userId, encryptFirstName, encryptLastName, encryptUsername, originalUsername, encryptEmail, originalEmail, notEncryptPassword, meta, socialId, provider); await System.storeUserKey(userId, encryptData.encryptedKey); await UserRepo.deleteTempUser(userId); return insertedUserId; } } public static async updateUserInfos(userKey: string, userId: string, firstName: string, lastName: string, username: string, email: string, authorName?: string): Promise { const encryptFirstName:string = System.encryptDataWithUserKey(firstName,userKey); const encryptLastName:string = System.encryptDataWithUserKey(lastName,userKey); const encryptUsername:string = System.encryptDataWithUserKey(username,userKey); const encryptEmail:string = System.encryptDataWithUserKey(email,userKey); const originalEmail:string = System.hashElement(email); const originalUsername:string = System.hashElement(username); const userMeta:string = System.encryptDateKey(userId); let encryptAuthorName:string = ''; let originalAuthorName: string = ''; if (authorName){ encryptAuthorName = System.encryptDataWithUserKey(authorName,userKey); originalAuthorName = System.hashElement(authorName); } return UserRepo.updateUserInfos(userId, encryptFirstName, encryptLastName, encryptUsername, originalUsername, encryptEmail, originalEmail, userMeta, originalAuthorName, encryptAuthorName); } public async getUserKey(metaKey:string,isNeedToBeDecrypt:boolean=true,keys?:UserKey[]):Promise{ let meta:string = ''; if (isNeedToBeDecrypt){ meta = System.decryptDateKey(this.id,metaKey); } else { meta = metaKey; } if (keys){ return System.getClosestPreviousKey(keys,meta); } else { const userKey:FetchQueryResponse = await System.getUserKey(this.id,meta); if (userKey.data){ return userKey.data?.encrypted_key; } else { return ''; } } } public static async checkUserName(username:string):Promise{ return UserRepo.fetchUserName(username); } public static async checkEmail(email:string):Promise{ return UserRepo.fetchEmail(email); } static getLevelDetail(level: number, lang: "fr" | "en"): string { if (lang === "fr") { switch (level) { case 1: // Débutant français return `**Niveau : Débutant** **Objectif** : Texte écrit par un élève de secondaire qui apprend encore à écrire en français. **Lexique** : - **Vocabulaire limité** : Répétition des mêmes mots simples, manque de synonymes. - **Exemples typiques** : "il y a", "c'est", "très", "beaucoup", "faire", "aller", "dire". - **Caractéristiques** : Anglicismes possibles, répétitions non voulues. **Structure** : - **Phrases courtes par nécessité** : 8-15 mots (le français tolère plus de mots). - **Constructions simples** : Sujet-Verbe-Complément, quelques "qui/que" basiques. - **Coordinations basiques** : "et", "mais", "alors", "après". - **Erreurs typiques** : "Il y a des arbres qui sont grands" au lieu de "Les arbres sont grands". **Style littéraire** : - **Descriptions factuelle** : "La forêt était sombre et silencieuse." - **Pas de figures de style** : Tentatives maladroites si présentes. - **Français scolaire** : Formulations apprises par cœur. **Exemples comparatifs** : ✅ "Marc a couru dans la forêt. Il avait très peur des bruits qu'il entendait." ❌ "Marc s'élança dans les profondeurs sylvestres, son cœur battant la chamade..." `; case 2: // Intermédiaire français return `**Niveau : Intermédiaire** **Objectif** : Texte écrit par un étudiant de cégep/université qui expérimente le style français. **Lexique** : - **Vocabulaire enrichi** : Recherche de synonymes, mots plus soutenus. - **Exemples** : "s'élancer", "contempler", "mystérieux", "murmurer", "lueur". - **Tentatives stylistiques** : Parfois réussies, parfois précieuses. **Structure** : - **Phrases moyennes à longues** : 12-25 mots, avec subordinations. - **Constructions élaborées** : Relatives multiples, quelques inversions. - **Style français émergent** : Tentatives de périodes, de nuances. - **Connecteurs plus variés** : "cependant", "néanmoins", "tandis que". **Style littéraire** : - **Premières métaphores** : Simples mais françaises ("tel un fantôme"). - **Descriptions étoffées** : Adjectifs et compléments circonstanciels. - **Registre soutenu recherché** : Parfois forcé. **Exemples comparatifs** : ✅ "Marc s'élança dans la forêt obscure, tandis que chaque bruissement mystérieux faisait naître en lui une crainte sourde." ❌ "Marc se précipita avec véhémence dans les méandres ténébreux de la sylve ancestrale..." `; case 3: // Avancé français return `**Niveau : Avancé** **Objectif** : Texte écrit par un écrivain professionnel maîtrisant la rhétorique française. **Lexique** : - **Richesse lexicale maîtrisée** : Précision du mot juste, nuances subtiles. - **Registres variés** : Du familier au soutenu selon l'effet recherché. - **Synonymie élégante** : Évitement des répétitions par art, non par contrainte. **Structure** : - **Périodes françaises** : Phrases longues et rythmées (20-40 mots possibles). - **Architecture complexe** : Subordinations enchâssées, incises, inversions maîtrisées. - **Rythme classique** : Alternance entre brièveté saisissante et amplitude oratoire. - **Ponctuation expressive** : Points-virgules, deux-points, parenthèses. **Style littéraire** : - **Figures maîtrisées** : Métaphores filées, antithèses, chiasmes. - **Tradition française** : Élégance, clarté, harmonie des sonorités. - **Sous-entendus et allusions** : Subtilité dans l'évocation. **Exemples comparatifs** : ✅ "Marc courut." (Simplicité voulue) ✅ "Dans l'entrelacement des ombres que la lune, filtrant à travers la ramure, dessinait sur le sol moussu, Marc discernait les échos de sa propre terreur ; terreur ancestrale, née de ce dialogue éternel entre l'homme et la nuit." `; default: return ``; } } else { // English Canadian switch (level) { case 1: // Beginner English return `**Level: Beginner** **Target**: Text written by a high school student still learning English composition. **Vocabulary**: - **Limited word choice**: Repetition of basic words, simple alternatives. - **Typical examples**: "said", "went", "got", "thing", "stuff", "really", "very". - **Characteristics**: Overuse of "and then", simple connectors. **Structure**: - **Short, choppy sentences**: 5-10 words (English naturally shorter). - **Basic constructions**: Subject-Verb-Object, simple coordination. - **Limited variety**: Mostly declarative sentences. - **Common errors**: Run-on sentences with "and", fragments. **Literary style**: - **Plain description**: "The forest was dark. It was scary." - **No literary devices**: Attempts at metaphors usually failed. - **Direct emotion**: "He was afraid" (English directness). **Comparative examples**: ✅ "Mark ran into the forest. He was scared. The trees looked big and dark." ❌ "Mark ventured forth into the sylvan depths, his heart pounding with trepidation..." `; case 2: // Intermediate English return `**Level: Intermediate** **Target**: Text written by a college student developing English writing skills. **Vocabulary**: - **Expanded vocabulary**: Some sophisticated words, better word choice. - **Examples**: "ventured", "glimpsed", "mysterious", "whispered", "shadowy". - **Characteristics**: Occasional overreach, some pretentious word choices. **Structure**: - **Varied sentence length**: 8-18 words, some complex sentences. - **Better flow**: Proper use of conjunctions, some subordination. - **Paragraph development**: Topic sentences, basic transitions. - **English rhythm**: Shorter than French but with variation. **Literary style**: - **Simple imagery**: Basic metaphors and similes that work. - **Show don't tell**: Attempts at indirect description. - **English conciseness**: Getting to the point while adding style. **Comparative examples**: ✅ "Mark plunged into the dark forest. Every sound made him jump, like whispers from invisible watchers." ❌ "Mark precipitously advanced into the tenebrous woodland whilst phantasmagorical emanations..." `; case 3: // Advanced English return `**Level: Advanced** **Target**: Text written by a professional English-speaking author. **Vocabulary**: - **Precise word choice**: Economy of language, powerful verbs. - **Understated elegance**: Sophisticated but not showy. - **Anglo-Saxon vs. Latin**: Strategic use of both registers. **Structure**: - **Masterful variety**: From punchy fragments to flowing periods. - **English rhythm**: Natural speech patterns, emphasis through structure. - **Controlled complexity**: Never convoluted, always clear. - **Strategic brevity**: Power in conciseness. **Literary style**: - **Subtle imagery**: Metaphors that illuminate, don't decorate. - **Understated power**: English preference for implication over elaboration. - **Voice and tone**: Distinctive style through restraint and precision. - **Anglo tradition**: Clarity, wit, emotional resonance through simplicity. **Comparative examples**: ✅ "Mark ran. The forest swallowed him." (English power in brevity) ✅ "The shadows moved between the trees like living things, and Mark felt something ancient watching from the darkness—something that had been waiting." (Controlled complexity) `; default: return ``; } } } static getAudienceDetail(level: number, lang: "fr" | "en"): string { if (lang === "fr") { switch (level) { case 1: // Débutant return `**Niveau : Débutant** **Objectif** : Texte accessible à un collégien (18-22 ans). **Lexique** : - **Priorité absolue** : Mots du quotidien, fréquents dans la langue parlée. - **Exemples autorisés** : "marcher", "voir", "dire", "beau", "peur", "lumière". - **Mots à éviter** : "luminescence", "éthéré", "volutes", "chuchotements", "centenaires". **Structure** : - **Longueur moyenne maximale par phrase** : 10 mots. - **Interdit** : Phrases composées, subordonnées, ou parenthèses. - **Style des descriptions** : Directes et visuelles (ex: "La forêt était sombre" → pas de "La forêt respirait la nuit"). **Style littéraire** : - **Interdits** : Métaphores, similes, ou figures de style. - **Priorité** : Clarté et simplicité absolue. **Exemples comparatifs** : ❌ "La brume enveloppait les arbres avec une grâce éthérée" → ✅ "La brume couvrait les arbres." ❌ "Elle courait, les jambes tremblantes, vers la lumière" → ✅ "Elle courait vers une lumière." `; case 2: // Intermédiaire return ` **Niveau : Intermédiaire** **Objectif** : Texte pour un étudiant universitaire (22-30 ans). **Lexique** : - **Priorité** : Vocabulaire varié mais compréhensible (ex: "murmurer", "luminescent", "mystère", "sentinelles"). - **Mots à éviter** : Termes littéraires complexes (ex: "volutes", "centenaires", "éthéré"). **Structure** : - **Longueur moyenne maximale par phrase** : 18 mots. - **Autorisé** : Phrases composées simples (ex: "Elle courait, mais la forêt était dense"). - **Descriptions** : Métaphores basiques (ex: "La forêt respirait la nuit"). **Style littéraire** : - **Autorisé** : Similes simples (ex: "comme un fantôme"). - **Interdit** : Figures de style complexes (antithèses, anaphores). **Exemples comparatifs** : ❌ "Les arbres, semblables à des sentinelles de pierre, veillaient" → ✅ "Les arbres ressemblaient à des gardiens." ❌ "La lueur dansante évoquait un espoir éphémère" → ✅ "La lumière clignotait comme un espoir." `; case 3: // Avancé return ` **Niveau : Avancé** **Objectif** : Texte littéraire d'un professionnel littéraire, ayant un diplôme universitaire. **Lexique** : - **Priorité** : Vocabulaire riche et littéraire (ex: "volutes", "centenaires", "éthéré", "luminescence"). - **Mots à éviter** : Termes simples jugés "banals" (ex: "marcher" → préférer "arpenter", "errer"). **Structure** : - **Longueur maximale par phrase** : aucune limite. - **Autorisé** : Phrases complexes et enchaînées (ex: "Parmi les ombres dansantes, elle discerna une lueur vacillante, symbole de l'espoir qui s'évanouissait"). - **Descriptions** : Métaphores poétiques et figures de style avancées. **Style littéraire** : - **Priorité** : Atmosphère immersive et langage élégant. - **Autorisé** : Antithèses, anaphores, et métaphores multi-niveaux. **Exemples comparatifs** : ✅ "Les volutes de brume, tels des spectres en quête d'écho, enveloppaient les troncs centenaires dans un ballet spectral." ✅ "Luminescence vacillante, espoir éphémère : les deux dansaient une dernière valse avant l'obscurité." `; default: return ``; } } else { // English Canadian switch (level) { case 1: // Beginner (Grade 11/Secondary 5 level) return ` **Level: Beginner** **Target**: Text accessible to a Grade 11 student (Secondary 5 equivalent). **Vocabulary**: - **Absolute priority**: Common everyday words, frequent in spoken language. - **Allowed examples**: "walk", "see", "say", "nice", "scared", "light". - **Words to avoid**: "luminescence", "ethereal", "tendrils", "whispers", "ancient". **Structure**: - **Maximum average sentence length**: 10 words. - **Forbidden**: Compound sentences, subordinate clauses, or parentheses. - **Description style**: Direct and visual (e.g., "The forest was dark" → not "The forest breathed with darkness"). **Literary style**: - **Forbidden**: Metaphors, similes, or figures of speech. - **Priority**: Absolute clarity and simplicity. **Comparative examples**: ❌ "The mist enveloped the trees with ethereal grace" → ✅ "The mist covered the trees." ❌ "She ran, her legs trembling, toward the light" → ✅ "She ran toward a light." `; case 2: // Intermediate (CEGEP/College level) return ` **Level: Intermediate** **Target**: Text for a CEGEP/College student. **Vocabulary**: - **Priority**: Varied but understandable vocabulary (e.g., "murmur", "luminous", "mystery", "sentinels"). - **Words to avoid**: Complex literary terms (e.g., "tendrils", "ancient", "ethereal"). **Structure**: - **Maximum average sentence length**: 18 words. - **Allowed**: Simple compound sentences (e.g., "She ran, but the forest was thick"). - **Descriptions**: Basic metaphors (e.g., "The forest breathed in the night"). **Literary style**: - **Allowed**: Simple similes (e.g., "like a ghost"). - **Forbidden**: Complex figures of speech (antithesis, anaphora). **Comparative examples**: ❌ "The trees, like stone sentinels, stood watch" → ✅ "The trees looked like guardians." ❌ "The dancing light evoked fleeting hope" → ✅ "The light flickered like hope." `; case 3: // Advanced (University/Professional writer level) return ` **Level: Advanced** **Target**: Literary text from a professional writer or bestselling author with university education. **Vocabulary**: - **Priority**: Rich and literary vocabulary (e.g., "tendrils", "ancient", "ethereal", "luminescence"). - **Words to avoid**: Simple terms deemed "mundane" (e.g., "walk" → prefer "traverse", "wander"). **Structure**: - **Maximum sentence length**: No limit. - **Allowed**: Complex and chained sentences (e.g., "Among the dancing shadows, she discerned a flickering light, a symbol of hope that was fading away"). - **Descriptions**: Poetic metaphors and advanced figures of speech. **Literary style**: - **Priority**: Immersive atmosphere and elegant language. - **Allowed**: Antithesis, anaphora, and multi-layered metaphors. **Comparative examples**: ✅ "Tendrils of mist, like spectres seeking echo, enveloped the ancient trunks in a ghostly ballet." ✅ "Flickering luminescence, fleeting hope: both danced a final waltz before darkness fell." `; default: return ``; } } } public static async checkUser(username: string, email: string): Promise { const validUsername:boolean = await User.checkUserName(username); const validEmail:boolean = await User.checkEmail(email); const lang: "fr" | "en" = getLanguage() if (validUsername){ throw new ConflictError(lang === 'fr' ? "Le nom d'utilisateur est déjà utilisé." : 'Username is already taken.'); } if (validEmail){ throw new ConflictError(lang === 'fr' ? "L'adresse courriel est déjà utilisée." : 'Email address is already in use.'); } } public static async login(email: string, password: string, jwtInstance: FastifyInstance): Promise { const lang: "fr" | "en" = getLanguage() if (await User.validUser(email, password)) { const user: BasicUserCredentials = await UserRepo.fetchUserByCredentials(System.hashElement(email)); return jwtInstance.jwt.sign({userId: user.user_id}) } else { throw new NotFoundError(lang === 'fr' ? "Nom d'utilisateur ou mot de passe invalide." : "Invalid username or password."); } } public static async setUserAccountInformation(userId: string, authorName: string, username: string): Promise { const user:User = new User(userId); await user.getUserInfos(); const meta:string = System.encryptDateKey(userId); const userKey:string = await user.getUserKey(meta); return User.updateUserInfos(userKey, userId, user.firstName, user.lastName, username, user.getEmail(), authorName); } public static async validUser(email:string,password:string):Promise{ const validUser: PasswordResponse | null = await UserRepo.verifyUserAuth(System.hashElement(email)); if (!validUser) { return false; } const stockedPassword: string = validUser.password; return System.verifyPassword(stockedPassword, password); } public static async socialLogin(firstName: string, lastName: string, email: string | null, socialId: string, provider: string, jwtInstance: JWT): Promise { const validUserId: string | null = await UserRepo.fetchBySocialId(socialId); let userId: string = ''; let newEmail: string = ''; if (email === null) { newEmail = socialId + '@' + provider + '.com'; } else { newEmail = email; } if (!validUserId) { const emailValidationUserId: string | null = await UserRepo.fetchByEmail(System.hashElement(newEmail)); if (!emailValidationUserId) { const randomNum: number = Math.floor(Math.random() * 10000); const fourDigitNum: string = randomNum.toString().padStart(4, '0'); const username: string = newEmail.split('@')[0] + fourDigitNum; userId = System.createUniqueId(); await this.addUser(firstName, lastName, username, newEmail, "", userId, provider, socialId); } else { userId = emailValidationUserId; await UserRepo.updateSocialId(userId, socialId, provider); } } else { userId = validUserId; } return jwtInstance.sign({userId: userId}) } public static async confirmAccount(verifyCode: string, email: string): Promise { await UserRepo.fetchVerifyCode(System.hashElement(email), verifyCode); const account: UserQueryResponse = await UserRepo.fetchTempAccount(email, verifyCode); const userId: string = account.user_id; const firstName: string = account.first_name; const lastName: string = account.last_name; const username: string = account.username; const emailAddr: string = account.email; const password: string = account.password; const insertUser: string = await this.addUser(firstName, lastName, username, emailAddr, password, userId); const projectDirectory: string = path.join(process.cwd(), 'uploads', userId.toString(), 'cover'); if (!insertUser) { throw new Error('Une erreur est survenue lors de la confirmation du compte.'); } await fs.promises.mkdir(projectDirectory, {recursive: true}) .catch(error => console.error(`Erreur lors de la création du dossier: ${error}`)); return true; } public static async sendVerifyCode(username: string, email: string): Promise { const verifyCode: string = System.createVerifyCode(); const validUpdate: boolean = await UserRepo.updateVerifyCode(username, username === "" ? System.hashElement(email) : email, verifyCode); if (validUpdate) { const options = {code: verifyCode} System.sendEmailTo(email, 'Votre code de vérification', '', EmailTemplate('verify-code', options)); } else { throw new Error('Une erreur est survenue lors de l\'envoi du code de vérification'); } } public static validateTOTPCode(code: string, token: string): boolean { try { const totp = new TOTP({ secret: Secret.fromBase32(code) }); const isValid = totp.validate({ token, window: 1 }); return isValid !== null; } catch (error) { return false; } } public static async checkTOTPExist(userId:string,email:string):Promise{ return UserRepo.checkTOTPExist(userId,email); } public static async handleTOTP(userId: string, email: string, verifyCode: string): Promise { const user = new User(userId); const meta:string = System.encryptDateKey(userId); const userKey:string = await user.getUserKey(meta); const encryptedCode:string = System.encryptDataWithUserKey(verifyCode,userKey); const encryptedEmail:string = System.hashElement(email); return UserRepo.insertTOTP(encryptedCode,encryptedEmail,userId,meta,true); } public static async activateTOTP(userId: string, email: string, token: string): Promise { const data: TOTPQuery = await UserRepo.fetchTempTOTP(userId, System.hashElement(email)); const user = new User(userId); const meta: string = data.totp_meta; const userKey: string = await user.getUserKey(meta); const code: string = System.decryptDataWithUserKey(data.totp_code, userKey); if (User.validateTOTPCode(code, token)) { return UserRepo.insertTOTP(data.totp_code, data.email, userId, meta, false); } else { throw new Error('Your token is incorrect'); } } public static async validateTOTP(userId: string, email: string, token: string): Promise { const data: TOTPQuery = await UserRepo.fetchTOTP(userId, System.hashElement(email)); const user = new User(userId); const meta: string = data.totp_meta; const userKey: string = await user.getUserKey(meta); const code: string = System.decryptDataWithUserKey(data.totp_code, userKey); if (User.validateTOTPCode(code, token)) { return true; } else { throw new Error('Your token is incorrect'); } } public static async getUserAccountInformation(userId: string): Promise { const user:User = new User(userId); const data: UserAccountQuery = await UserRepo.fetchAccountInformation(userId); const userKey: string = await user.getUserKey(data.user_meta); const userName: string = data.first_name ? System.decryptDataWithUserKey(data.first_name, userKey) : ''; const lastName: string = data.last_name ? System.decryptDataWithUserKey(data.last_name, userKey) : ''; const username: string = data.username ? System.decryptDataWithUserKey(data.username, userKey) : ''; const authorName: string = data.author_name ? System.decryptDataWithUserKey(data.author_name, userKey) : ''; const email: string = data.email ? System.decryptDataWithUserKey(data.email, userKey) : ''; return { firstName: userName, lastName: lastName, username: username, authorName: authorName, email: email }; } public static async getSubscription(userId: string): Promise { const result: UserSubscription[] = await UserRepo.fetchSubscription(userId); const userSubscription: UserSubscriptionProps[] = []; for (const sub of result) { userSubscription.push({ userId: sub.user_id, subType: sub.sub_type, subTier: sub.sub_tier, startDate: sub.start_date, endDate: sub.end_date, status: sub.status }) } return userSubscription; } public async convertTimeToPoint(time:number):Promise{ const points:number = Math.ceil(time/30); await UserRepo.updatePoints(points,this.id); } static async setUserPreferences(userId: string, writingLang: number, writingLevel: number): Promise { if (writingLang === 0 || writingLevel === 0) { throw new Error('Veuillez choisir une langue et un niveau de rédaction valides.'); } if (await UserRepo.isUserPreferencesExist(userId)) { return UserRepo.updateUserPreference(userId, writingLang, writingLevel) } else { const preferenceId: string = System.createUniqueId(); return await UserRepo.insertUserPreference(preferenceId, userId, writingLang, writingLevel); } } static async acceptTerms(userId: string, version: string): Promise { const log: boolean = await UserRepo.logAcceptTerms(userId, version); if (log) { return await UserRepo.updateTermsAccepted(userId); } else { return false; } } public getId(): string { return this.id; } public getFirstName(): string { return this.firstName; } public getLastName(): string { return this.lastName; } public getUsername(): string { return this.username; } public getEmail(): string { return this.email; } public isAccountVerified(): boolean { return this.accountVerified; } public getWritingLang(): number { return this.writingLang; } public getWritingLevel(): number { return this.writingLevel; } public getRitePoints(): number { return this.ritePoints; } public isTermsAccepted(): boolean { return this.termsAccepted; } public getGroupId(): number { return this.groupId; } public getBalancedCredits(): number { return this.balancedCredits; } public getAuthorName(): string { return this.authorName; } static async updateAIUserKey(userId: string, anthropicKey: string | null, openaiKey: string | null, geminiKey: string | null): Promise { const user = new User(userId); const meta: string = System.encryptDateKey(userId); const userKey: string = await user.getUserKey(meta); const groupedKey: { brand: 'anthropic' | 'openai' | 'gemini', key: string }[] = []; const anthropicKeyEncrypted: string = anthropicKey ? System.encryptDataWithUserKey(anthropicKey, userKey) : ''; groupedKey.push({brand: 'anthropic', key: anthropicKeyEncrypted}); const openaiKeyEncrypted: string = openaiKey ? System.encryptDataWithUserKey(openaiKey, userKey) : ''; groupedKey.push({brand: 'openai', key: openaiKeyEncrypted}); const geminiKeyEncrypted: string = geminiKey ? System.encryptDataWithUserKey(geminiKey, userKey) : ''; groupedKey.push({brand: 'gemini', key: geminiKeyEncrypted}); for (const key of groupedKey) { const updateAI: boolean = await UserRepo.updateAIUserKey(userId, key.brand, key.key); if (!updateAI) { return false; } } return true; } static async getAPIKey(userId: string): Promise { const user: User = new User(userId); const meta: string = System.encryptDateKey(userId); const userKey: string = await user.getUserKey(meta); const apiKeys: UserAPIKeyResult[] = await UserRepo.fetchAPIKey(userId); const decryptKeys: UserAPIKey[] = []; for (const apiKey of apiKeys) { if (apiKey.key && apiKey.key.length > 0) { decryptKeys.push({ brand: apiKey.brand, key: System.decryptDataWithUserKey(apiKey.key, userKey) }) } } return decryptKeys; } private static async moneySpentThisMonth(userId: string): Promise { const {last, first} = System.getMonthBounds(); const usage: UserAiUsageResult = await UserRepo.fetchUserAiUsage(userId, first, last); return usage.total_price; } }