import type { HttpContext } from '@adonisjs/core/http'; import User from '#models/user'; // import { RenderResponse } from '@ioc:EidelLev/Inertia'; import TwoFactorAuthProvider from '#app/services/TwoFactorAuthProvider'; import hash from '@adonisjs/core/services/hash'; // import { schema, rules } from '@adonisjs/validator'; import vine from '@vinejs/vine'; import BackupCodeStorage, { SecureRandom } from '#services/backup_code_storage'; import path from 'path'; import crypto from 'crypto'; // import drive from '#services/drive'; import drive from '@adonisjs/drive/services/main'; import logger from '@adonisjs/core/services/logger'; // Here we are generating secret and recovery codes for the user that’s enabling 2FA and storing them to our database. export default class UserController { /** * Show the user a form to change their personal information & password. * * @return — \Inertia\Response */ public async accountInfo({ inertia, auth }: HttpContext) { // const user = auth.user; const user = (await User.find(auth.user?.id)) as User; // const id = request.param('id'); // const user = await User.query().where('id', id).firstOrFail(); let storage = new BackupCodeStorage(new SecureRandom()); // const codes= user.isTwoFactorEnabled? (await user.getBackupCodes()).map((role) => role.code) : []; let backupState = await storage.getBackupCodesState(user); return inertia.render('Auth/AccountInfo', { user: user, twoFactorEnabled: user.isTwoFactorEnabled, // code: await TwoFactorAuthProvider.generateQrCode(user), backupState: backupState, }); } public async accountInfoStore({ auth, request, response, session }: HttpContext) { // const passwordSchema = schema.create({ // old_password: schema.string({ trim: true }, [rules.required()]), // new_password: schema.string({ trim: true }, [rules.minLength(8), rules.maxLength(255), rules.confirmed('confirm_password')]), // confirm_password: schema.string({ trim: true }, [rules.required()]), // }); const passwordSchema = vine.object({ // first step old_password: vine.string().trim(), // .regex(/^[a-zA-Z0-9]+$/), new_password: vine.string().confirmed({ confirmationField: 'confirm_password' }).trim().minLength(8).maxLength(255), }); try { // await request.validate({ schema: passwordSchema }); const validator = vine.compile(passwordSchema); await request.validateUsing(validator); } catch (error) { // return response.badRequest(error.messages); throw error; } try { const user = (await auth.user) as User; const { old_password, new_password } = request.only(['old_password', 'new_password']); // if (!(old_password && new_password && confirm_password)) { // return response.status(400).send({ warning: 'Old password and new password are required.' }); // } // Verify if the provided old password matches the user's current password const isSame = await hash.verify(user.password, old_password); if (!isSame) { return response.flash('warning', 'Old password is incorrect.').redirect().back(); } // Hash the new password before updating the user's password user.password = new_password; await user.save(); // return response.status(200).send({ message: 'Password updated successfully.' }); session.flash({ message: 'Password updated successfully.' }); return response.redirect().toRoute('settings.user'); } catch (error) { // return response.status(500).send({ message: 'Internal server error.' }); return response.flash('warning', `Invalid server state. Internal server error.`).redirect().back(); } } public async profile({ inertia, auth }: HttpContext) { const user = await User.find(auth.user?.id); // let test = await drive.use().getUrl(user?.avatar); // user?.preload('roles'); const avatarFullPathUrl = user?.avatar ? await drive.use('public').getUrl(user.avatar) : null; return inertia.render('profile/show', { user: user, defaultUrl: avatarFullPathUrl, }); } /** * Update the user's profile information. * * @param {HttpContext} ctx - The HTTP context object. * @returns {Promise} */ public async profileUpdate({ auth, request, response, session }: HttpContext) { if (!auth.user) { session.flash('error', 'You must be logged in to update your profile.'); return response.redirect().toRoute('login'); } const updateProfileValidator = vine.withMetaData<{ userId: number }>().compile( vine.object({ first_name: vine.string().trim().minLength(4).maxLength(255), last_name: vine.string().trim().minLength(4).maxLength(255), login: vine.string().trim().minLength(4).maxLength(255), email: vine .string() .trim() .maxLength(255) .email() .normalizeEmail() .isUnique({ table: 'accounts', column: 'email', whereNot: (field) => field.meta.userId }), avatar: vine .myfile({ size: '2mb', extnames: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'], }) // .allowedMimetypeExtensions({ // allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'], // }) .optional(), }), ); const user = await User.find(auth.user.id); if (!user) { session.flash('error', 'User not found.'); return response.redirect().toRoute('login'); } try { // validate update form await request.validateUsing(updateProfileValidator, { meta: { userId: user.id, }, }); const { login, email, first_name, last_name } = request.only(['login', 'email', 'first_name', 'last_name']); const sanitizedData: { [key: string]: any } = { login: login?.trim(), email: email?.toLowerCase().trim(), first_name: first_name?.trim(), last_name: last_name?.trim(), // avatar: "", }; const toCamelCase = (str: string) => str.replace(/_([a-z])/g, (g) => g[1].toUpperCase()); const hasInputChanges = Object.keys(sanitizedData).some((key) => { const camelKey = toCamelCase(key); return sanitizedData[key] !== (user.$attributes as { [key: string]: any })[camelKey]; }); let hasAvatarChanged = false; const avatar = request.file('avatar'); if (avatar) { const fileHash = crypto .createHash('sha256') .update(avatar.clientName + avatar.size) .digest('hex'); const fileName = `avatar-${fileHash}.${avatar.extname}`; const avatarFullPath = path.join('/uploads', `${user.login}`, fileName); if (user.avatar != avatarFullPath) { if (user.avatar) { await drive.use('public').delete(user.avatar); } hasAvatarChanged = user.avatar !== avatarFullPath; await avatar.moveToDisk(avatarFullPath, 'public', { name: fileName, overwrite: true, // overwrite in case of conflict disk: 'public', }); sanitizedData.avatar = avatarFullPath; } } if (!hasInputChanges && !hasAvatarChanged) { session.flash('message', 'No changes were made.'); return response.redirect().back(); } await user.merge(sanitizedData).save(); session.flash('message', 'User has been updated successfully'); return response.redirect().toRoute('settings.profile.edit'); } catch (error) { logger.error('Profile update failed:', error); // session.flash('errors', 'Profile update failed. Please try again.'); // return response.redirect().back(); throw error; } } public async passwordUpdate({ auth, request, response, session }: HttpContext) { // const passwordSchema = schema.create({ // old_password: schema.string({ trim: true }, [rules.required()]), // new_password: schema.string({ trim: true }, [rules.minLength(8), rules.maxLength(255), rules.confirmed('confirm_password')]), // confirm_password: schema.string({ trim: true }, [rules.required()]), // }); const passwordSchema = vine.object({ // first step old_password: vine.string().trim(), // .regex(/^[a-zA-Z0-9]+$/), new_password: vine.string().confirmed({ confirmationField: 'confirm_password' }).trim().minLength(8).maxLength(255), }); try { // await request.validate({ schema: passwordSchema }); const validator = vine.compile(passwordSchema); await request.validateUsing(validator); } catch (error) { // return response.badRequest(error.messages); throw error; } try { const user = (await auth.user) as User; const { old_password, new_password } = request.only(['old_password', 'new_password']); // if (!(old_password && new_password && confirm_password)) { // return response.status(400).send({ warning: 'Old password and new password are required.' }); // } // Verify if the provided old password matches the user's current password const isSame = await hash.verify(user.password, old_password); if (!isSame) { session.flash('warning', 'Old password is incorrect.'); return response.redirect().back(); // return response.flash('warning', 'Old password is incorrect.').redirect().back(); } // Hash the new password before updating the user's password user.password = new_password; await user.save(); // return response.status(200).send({ message: 'Password updated successfully.' }); session.flash({ message: 'Password updated successfully.' }); return response.redirect().toRoute('settings.profile.edit'); } catch (error) { // return response.status(500).send({ message: 'Internal server error.' }); return response.flash('warning', `Invalid server state. Internal server error.`).redirect().back(); } } public async enableTwoFactorAuthentication({ auth, response, session }: HttpContext): Promise { // const user: User | undefined = auth?.user; const user = (await User.find(auth.user?.id)) as User; user.twoFactorSecret = TwoFactorAuthProvider.generateSecret(user); user.twoFactorRecoveryCodes = await TwoFactorAuthProvider.generateRecoveryCodes(); await user.save(); session.flash('message', 'Two factor authentication enabled.'); return response.redirect().back(); // return inertia.render('Auth/AccountInfo', { // // status: { // // type: 'success', // // message: 'Two factor authentication enabled.', // // }, // user: user, // twoFactorEnabled: user.isTwoFactorEnabled, // code: await TwoFactorAuthProvider.generateQrCode(user), // recoveryCodes: user.twoFactorRecoveryCodes, // }); } public async disableTwoFactorAuthentication({ auth, response, session }: HttpContext): Promise { const user: User | undefined = auth.user; if (user) { user.twoFactorSecret = null; user.twoFactorRecoveryCodes = null; await user.save(); session.flash('message', 'Two-factor authentication disabled.'); } else { session.flash('error', 'User not found.'); } return response.redirect().back(); // return inertia.render('Auth/AccountInfo', { // // status: { // // type: 'success', // // message: 'Two factor authentication disabled.', // // }, // user: user, // twoFactorEnabled: user.isTwoFactorEnabled, // }); } // public async fetchRecoveryCodes({ auth, view }) { // const user = auth?.user; // return view.render('pages/settings', { // twoFactorEnabled: user.isTwoFactorEnabled, // recoveryCodes: user.twoFactorRecoveryCodes, // }); // } }