diff --git a/adonisrc.ts b/adonisrc.ts index 8b5d4af..97e4528 100644 --- a/adonisrc.ts +++ b/adonisrc.ts @@ -70,7 +70,8 @@ export default defineConfig({ () => import('#providers/query_builder_provider'), () => import('#providers/token_worker_provider'), // () => import('#providers/validator_provider'), - () => import('#providers/drive/provider/drive_provider'), + // () => import('#providers/drive/provider/drive_provider'), + () => import('@adonisjs/drive/drive_provider'), // () => import('@adonisjs/core/providers/vinejs_provider'), () => import('#providers/vinejs_provider'), () => import('@adonisjs/mail/mail_provider'), diff --git a/app/Controllers/Http/Api/AvatarController.ts b/app/Controllers/Http/Api/AvatarController.ts index 96b158c..0b337d9 100644 --- a/app/Controllers/Http/Api/AvatarController.ts +++ b/app/Controllers/Http/Api/AvatarController.ts @@ -1,34 +1,28 @@ import type { HttpContext } from '@adonisjs/core/http'; import { StatusCodes } from 'http-status-codes'; -// import * as fs from 'fs'; -// import * as path from 'path'; const prefixes = ['von', 'van']; -// node ace make:controller Author export default class AvatarController { public async generateAvatar({ request, response }: HttpContext) { try { - const { name, background, textColor, size } = request.only(['name', 'background', 'textColor', 'size']); + const { name, size } = request.only(['name', 'size']); - // Generate initials - // const initials = name - // .split(' ') - // .map((part) => part.charAt(0).toUpperCase()) - // .join(''); const initials = this.getInitials(name); - // Define SVG content with dynamic values for initials, background color, text color, and size + const originalColor = this.getColorFromName(name); + const backgroundColor = this.lightenColor(originalColor, 60); + const textColor = this.darkenColor(originalColor); + const svgContent = ` - + ${initials} + }" fill="#${textColor}">${initials} `; - // Set response headers for SVG content response.header('Content-type', 'image/svg+xml'); response.header('Cache-Control', 'no-cache'); response.header('Pragma', 'no-cache'); @@ -62,4 +56,49 @@ export default class AvatarController { return initials; } + + private getColorFromName(name: string) { + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash); + } + let color = '#'; + for (let i = 0; i < 3; i++) { + const value = (hash >> (i * 8)) & 0xff; + color += ('00' + value.toString(16)).substr(-2); + } + return color.replace('#', ''); + } + + private lightenColor(hexColor: string, percent: number) { + let r = parseInt(hexColor.substring(0, 2), 16); + let g = parseInt(hexColor.substring(2, 4), 16); + let b = parseInt(hexColor.substring(4, 6), 16); + + r = Math.floor((r * (100 + percent)) / 100); + g = Math.floor((g * (100 + percent)) / 100); + b = Math.floor((b * (100 + percent)) / 100); + + r = r < 255 ? r : 255; + g = g < 255 ? g : 255; + b = b < 255 ? b : 255; + + const lighterHex = ((r << 16) | (g << 8) | b).toString(16); + + return lighterHex.padStart(6, '0'); + } + + private darkenColor(hexColor: string) { + const r = parseInt(hexColor.slice(0, 2), 16); + const g = parseInt(hexColor.slice(2, 4), 16); + const b = parseInt(hexColor.slice(4, 6), 16); + + const darkerR = Math.round(r * 0.6); + const darkerG = Math.round(g * 0.6); + const darkerB = Math.round(b * 0.6); + + const darkerColor = ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16); + + return darkerColor.padStart(6, '0'); + } } diff --git a/app/Controllers/Http/Auth/UserController.ts b/app/Controllers/Http/Auth/UserController.ts index 442aaca..46a8d48 100644 --- a/app/Controllers/Http/Auth/UserController.ts +++ b/app/Controllers/Http/Auth/UserController.ts @@ -6,6 +6,11 @@ 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 { @@ -28,7 +33,7 @@ export default class UserController { user: user, twoFactorEnabled: user.isTwoFactorEnabled, // code: await TwoFactorAuthProvider.generateQrCode(user), - backupState: backupState, + backupState: backupState, }); } @@ -40,10 +45,8 @@ export default class UserController { // }); const passwordSchema = vine.object({ // first step - old_password: vine - .string() - .trim() - .regex(/^[a-zA-Z0-9]+$/), + old_password: vine.string().trim(), + // .regex(/^[a-zA-Z0-9]+$/), new_password: vine.string().confirmed({ confirmationField: 'confirm_password' }).trim().minLength(8).maxLength(255), }); try { @@ -54,9 +57,9 @@ export default class UserController { // return response.badRequest(error.messages); throw error; } - + try { - const user = await auth.user as User; + 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)) { @@ -82,6 +85,171 @@ export default class UserController { } } + 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; @@ -115,7 +283,7 @@ export default class UserController { } else { session.flash('error', 'User not found.'); } - + return response.redirect().back(); // return inertia.render('Auth/AccountInfo', { // // status: { diff --git a/app/Controllers/Http/Submitter/DatasetController.ts b/app/Controllers/Http/Submitter/DatasetController.ts index d26b2ce..6c28008 100644 --- a/app/Controllers/Http/Submitter/DatasetController.ts +++ b/app/Controllers/Http/Submitter/DatasetController.ts @@ -33,7 +33,9 @@ import File from '#models/file'; import ClamScan from 'clamscan'; // import { ValidationException } from '@adonisjs/validator'; // import Drive from '@ioc:Adonis/Core/Drive'; -import drive from '#services/drive'; +// import drive from '#services/drive'; +import drive from '@adonisjs/drive/services/main'; +import path from 'path'; import { Exception } from '@adonisjs/core/exceptions'; import { MultipartFile } from '@adonisjs/core/types/bodyparser'; import * as crypto from 'crypto'; @@ -532,11 +534,18 @@ export default class DatasetController { const fileName = this.generateFilename(file.extname as string); const mimeType = file.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type const datasetFolder = `files/${dataset.id}`; + const datasetFullPath = path.join(`${datasetFolder}`, fileName); // const size = file.size; - await file.move(drive.makePath(datasetFolder), { + // await file.move(drive.makePath(datasetFolder), { + // name: fileName, + // overwrite: true, // overwrite in case of conflict + // }); + await file.moveToDisk(datasetFullPath, 'local', { name: fileName, overwrite: true, // overwrite in case of conflict + disk: 'local', }); + // save file metadata into db const newFile = new File(); newFile.pathName = `${datasetFolder}/${fileName}`; @@ -1031,10 +1040,16 @@ export default class DatasetController { // move to disk: const fileName = `file-${cuid()}.${fileData.extname}`; //'file-ls0jyb8xbzqtrclufu2z2e0c.pdf' const datasetFolder = `files/${dataset.id}`; // 'files/307' + const datasetFullPath = path.join(`${datasetFolder}`, fileName); // await fileData.moveToDisk(datasetFolder, { name: fileName, overwrite: true }, 'local'); - await fileData.move(drive.makePath(datasetFolder), { + // await fileData.move(drive.makePath(datasetFolder), { + // name: fileName, + // overwrite: true, // overwrite in case of conflict + // }); + await fileData.moveToDisk(datasetFullPath, { name: fileName, overwrite: true, // overwrite in case of conflict + driver: 'local', }); //save to db: @@ -1161,31 +1176,32 @@ export default class DatasetController { if (validStates.includes(dataset.server_state)) { if (dataset.files && dataset.files.length > 0) { for (const file of dataset.files) { - // overwritten delete method also delets file on filespace + // overwritten delete method also delets file on filespace and db object await file.delete(); } } const datasetFolder = `files/${params.id}`; - const folderExists = await drive.exists(datasetFolder); - if (folderExists) { - const dirListing = drive.list(datasetFolder); - const folderContents = await dirListing.toArray(); - if (folderContents.length === 0) { - await drive.delete(datasetFolder); - } + // const folderExists = await drive.use('local').exists(datasetFolder); + // if (folderExists) { + // const dirListing = drive.list(datasetFolder); + // const folderContents = await dirListing.toArray(); + // if (folderContents.length === 0) { + // await drive.delete(datasetFolder); + // } + await drive.use('local').deleteAll(datasetFolder); // delete dataset wirh relation in db await dataset.delete(); session.flash({ message: 'You have deleted 1 dataset!' }); return response.redirect().toRoute('dataset.list'); - } else { - // session.flash({ - // warning: `You cannot delete this dataset! Invalid server_state: "${dataset.server_state}"!`, - // }); - return response - .flash({ warning: `You cannot delete this dataset! Dataset folder "${datasetFolder}" doesn't exist!` }) - .redirect() - .back(); - } + // } else { + // // session.flash({ + // // warning: `You cannot delete this dataset! Invalid server_state: "${dataset.server_state}"!`, + // // }); + // return response + // .flash({ warning: `You cannot delete this dataset! Dataset folder "${datasetFolder}" doesn't exist!` }) + // .redirect() + // .back(); + // } } } catch (error) { if (error instanceof errors.E_VALIDATION_ERROR) { @@ -1193,7 +1209,8 @@ export default class DatasetController { throw error; } else if (error instanceof Exception) { // General exception handling - return response.flash('errors', error.message).redirect().back(); + session.flash({ error: error.message}); + return response.redirect().back(); } else { session.flash({ error: 'An error occurred while deleting the dataset.' }); return response.redirect().back(); diff --git a/app/models/file.ts b/app/models/file.ts index 0b85737..3d9145f 100644 --- a/app/models/file.ts +++ b/app/models/file.ts @@ -7,7 +7,8 @@ import * as fs from 'fs'; import crypto from 'crypto'; // import Drive from '@ioc:Adonis/Core/Drive'; // import Drive from '@adonisjs/drive'; -import drive from '#services/drive'; +// import drive from '#services/drive'; +import drive from '@adonisjs/drive/services/main'; import type { HasMany } from "@adonisjs/lucid/types/relations"; import type { BelongsTo } from "@adonisjs/lucid/types/relations"; @@ -87,7 +88,8 @@ export default class File extends BaseModel { serializeAs: 'filePath', }) public get filePath() { - return `/storage/app/public/${this.pathName}`; + // return `/storage/app/public/${this.pathName}`; + return `/storage/app/data/${this.pathName}`; // const mainTitle = this.titles?.find((title) => title.type === 'Main'); // return mainTitle ? mainTitle.value : null; } @@ -164,7 +166,7 @@ export default class File extends BaseModel { public async delete() { if (this.pathName) { // Delete file from additional storage - await drive.delete(this.pathName); + await drive.use('local').delete(this.pathName); } // Call the original delete method of the BaseModel to remove the record from the database diff --git a/app/models/user.ts b/app/models/user.ts index 58f1676..5dbaab4 100644 --- a/app/models/user.ts +++ b/app/models/user.ts @@ -1,6 +1,6 @@ import { DateTime } from 'luxon'; import { withAuthFinder } from '@adonisjs/auth/mixins/lucid'; -import { column, manyToMany, hasMany, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm'; +import { column, manyToMany, hasMany, SnakeCaseNamingStrategy, computed, beforeFetch, beforeFind } from '@adonisjs/lucid/orm'; import hash from '@adonisjs/core/services/hash'; import Role from './role.js'; import db from '@adonisjs/lucid/services/db'; @@ -49,7 +49,6 @@ export default class User extends compose(BaseModel, AuthFinder) { @column() public login: string; - @column() public firstName: string; @@ -87,6 +86,9 @@ export default class User extends compose(BaseModel, AuthFinder) { @column({}) public state: number; + @column({}) + public avatar: string; + // @hasOne(() => TotpSecret, { // foreignKey: 'user_id', // }) @@ -104,6 +106,7 @@ export default class User extends compose(BaseModel, AuthFinder) { // return Boolean(this.totp_secret?.twoFactorSecret); } + @manyToMany(() => Role, { pivotForeignKey: 'account_id', pivotRelatedForeignKey: 'role_id', @@ -121,6 +124,27 @@ export default class User extends compose(BaseModel, AuthFinder) { }) public backupcodes: HasMany; + @computed({ + serializeAs: 'is_admin', + }) + public get isAdmin(): boolean { + const roles = this.roles; + const isAdmin = roles?.map((role: Role) => role.name).includes('administrator'); + return isAdmin; + } + + // public toJSON() { + // return { + // ...super.toJSON(), + // roles: [] + // }; + // } + @beforeFind() + @beforeFetch() + public static preloadRoles(user: User) { + user.preload('roles') + } + public async getBackupCodes(this: User): Promise { const test = await this.related('backupcodes').query(); // return test.map((role) => role.code); diff --git a/config/drive.ts b/config/drive.ts index 5679c62..cfb91f9 100644 --- a/config/drive.ts +++ b/config/drive.ts @@ -1,151 +1,45 @@ -/** - * Config source: https://git.io/JBt3o - * - * Feel free to let us know via PR, if you find something broken in this config - * file. - */ -import { defineConfig } from '#providers/drive/src/types/define_config'; -import env from '#start/env'; -// import { driveConfig } from '@adonisjs/core/build/config'; -// import { driveConfig } from "@adonisjs/drive/build/config.js"; -// import Application from '@ioc:Adonis/Core/Application'; +// import env from '#start/env' +// import app from '@adonisjs/core/services/app' +import { defineConfig, services } from '@adonisjs/drive' -/* -|-------------------------------------------------------------------------- -| Drive Config -|-------------------------------------------------------------------------- -| -| The `DriveConfig` relies on the `DisksList` interface which is -| defined inside the `contracts` directory. -| -*/ -export default defineConfig({ - /* - |-------------------------------------------------------------------------- - | Default disk - |-------------------------------------------------------------------------- - | - | The default disk to use for managing file uploads. The value is driven by - | the `DRIVE_DISK` environment variable. - | - */ - disk: env.get('DRIVE_DISK', 'local'), +const driveConfig = defineConfig({ + + default: 'public', + - disks: { - /* - |-------------------------------------------------------------------------- - | Local - |-------------------------------------------------------------------------- - | - | Uses the local file system to manage files. Make sure to turn off serving - | files when not using this disk. - | - */ - local: { - driver: 'local', - visibility: 'public', - - /* - |-------------------------------------------------------------------------- - | Storage root - Local driver only - |-------------------------------------------------------------------------- - | - | Define an absolute path to the storage directory from where to read the - | files. - | - */ - // root: Application.tmpPath('uploads'), - root: '/storage/app/public', - - /* - |-------------------------------------------------------------------------- - | Serve files - Local driver only - |-------------------------------------------------------------------------- - | - | When this is set to true, AdonisJS will configure a files server to serve - | files from the disk root. This is done to mimic the behavior of cloud - | storage services that has inbuilt capabilities to serve files. - | - */ - serveFiles: true, - - /* - |-------------------------------------------------------------------------- - | Base path - Local driver only - |-------------------------------------------------------------------------- - | - | Base path is always required when "serveFiles = true". Also make sure - | the `basePath` is unique across all the disks using "local" driver and - | you are not registering routes with this prefix. - | - */ - basePath: '/uploads', - }, - - /* - |-------------------------------------------------------------------------- - | S3 Driver - |-------------------------------------------------------------------------- - | - | Uses the S3 cloud storage to manage files. Make sure to install the s3 - | drive separately when using it. - | - |************************************************************************** - | npm i @adonisjs/drive-s3 - |************************************************************************** - | - */ - // s3: { - // driver: 's3', - // visibility: 'public', - // key: Env.get('S3_KEY'), - // secret: Env.get('S3_SECRET'), - // region: Env.get('S3_REGION'), - // bucket: Env.get('S3_BUCKET'), - // endpoint: Env.get('S3_ENDPOINT'), - // - // // For minio to work - // // forcePathStyle: true, - // }, - - /* - |-------------------------------------------------------------------------- - | GCS Driver - |-------------------------------------------------------------------------- - | - | Uses the Google cloud storage to manage files. Make sure to install the GCS - | drive separately when using it. - | - |************************************************************************** - | npm i @adonisjs/drive-gcs - |************************************************************************** - | - */ - // gcs: { - // driver: 'gcs', - // visibility: 'public', - // keyFilename: Env.get('GCS_KEY_FILENAME'), - // bucket: Env.get('GCS_BUCKET'), - - /* - |-------------------------------------------------------------------------- - | Uniform ACL - Google cloud storage only - |-------------------------------------------------------------------------- - | - | When using the Uniform ACL on the bucket, the "visibility" option is - | ignored. Since, the files ACL is managed by the google bucket policies - | directly. - | - |************************************************************************** - | Learn more: https://cloud.google.com/storage/docs/uniform-bucket-level-access - |************************************************************************** - | - | The following option just informs drive whether your bucket is using uniform - | ACL or not. The actual setting needs to be toggled within the Google cloud - | console. - | - */ - // usingUniformAcl: false, - // }, + services: { + + /** + * Persist files on the local filesystem + */ + public: services.fs({ + location: '/storage/app/public/', + serveFiles: true, + routeBasePath: '/public', + visibility: 'public', + }), + local: services.fs({ + location: '/storage/app/data/', + serveFiles: true, + routeBasePath: '/data', + visibility: 'public', + }), + + + /** + * Persist files on Digital Ocean spaces + */ + // spaces: services.s3({ + // credentials: { + // accessKeyId: env.get('SPACES_KEY'), + // secretAccessKey: env.get('SPACES_SECRET'), + // }, + // region: env.get('SPACES_REGION'), + // bucket: env.get('SPACES_BUCKET'), + // endpoint: env.get('SPACES_ENDPOINT'), + // visibility: 'public', + // }), }, -}); + }) + + export default driveConfig \ No newline at end of file diff --git a/config/drive_self.ts b/config/drive_self.ts new file mode 100644 index 0000000..f1c8152 --- /dev/null +++ b/config/drive_self.ts @@ -0,0 +1,233 @@ +/** + * Config source: https://git.io/JBt3o + * + * Feel free to let us know via PR, if you find something broken in this config + * file. + */ +import { defineConfig } from '#providers/drive/src/types/define_config'; +import env from '#start/env'; +// import { driveConfig } from '@adonisjs/core/build/config'; +// import { driveConfig } from "@adonisjs/drive/build/config.js"; +// import Application from '@ioc:Adonis/Core/Application'; + +/* +|-------------------------------------------------------------------------- +| Drive Config +|-------------------------------------------------------------------------- +| +| The `DriveConfig` relies on the `DisksList` interface which is +| defined inside the `contracts` directory. +| +*/ +export default defineConfig({ + /* + |-------------------------------------------------------------------------- + | Default disk + |-------------------------------------------------------------------------- + | + | The default disk to use for managing file uploads. The value is driven by + | the `DRIVE_DISK` environment variable. + | + */ + disk: env.get('DRIVE_DISK', 'local'), + + disks: { + /* + |-------------------------------------------------------------------------- + | Local + |-------------------------------------------------------------------------- + | + | Uses the local file system to manage files. Make sure to turn off serving + | files when not using this disk. + | + */ + local: { + driver: 'local', + visibility: 'public', + + /* + |-------------------------------------------------------------------------- + | Storage root - Local driver only + |-------------------------------------------------------------------------- + | + | Define an absolute path to the storage directory from where to read the + | files. + | + */ + // root: Application.tmpPath('uploads'), + root: '/storage/app/data', + + /* + |-------------------------------------------------------------------------- + | Serve files - Local driver only + |-------------------------------------------------------------------------- + | + | When this is set to true, AdonisJS will configure a files server to serve + | files from the disk root. This is done to mimic the behavior of cloud + | storage services that has inbuilt capabilities to serve files. + | + */ + serveFiles: true, + + /* + |-------------------------------------------------------------------------- + | Base path - Local driver only + |-------------------------------------------------------------------------- + | + | Base path is always required when "serveFiles = true". Also make sure + | the `basePath` is unique across all the disks using "local" driver and + | you are not registering routes with this prefix. + | + */ + basePath: '/files', + }, + + local: { + driver: 'local', + visibility: 'public', + + /* + |-------------------------------------------------------------------------- + | Storage root - Local driver only + |-------------------------------------------------------------------------- + | + | Define an absolute path to the storage directory from where to read the + | files. + | + */ + // root: Application.tmpPath('uploads'), + root: '/storage/app/data', + + /* + |-------------------------------------------------------------------------- + | Serve files - Local driver only + |-------------------------------------------------------------------------- + | + | When this is set to true, AdonisJS will configure a files server to serve + | files from the disk root. This is done to mimic the behavior of cloud + | storage services that has inbuilt capabilities to serve files. + | + */ + serveFiles: true, + + /* + |-------------------------------------------------------------------------- + | Base path - Local driver only + |-------------------------------------------------------------------------- + | + | Base path is always required when "serveFiles = true". Also make sure + | the `basePath` is unique across all the disks using "local" driver and + | you are not registering routes with this prefix. + | + */ + basePath: '/files', + }, + + fs: { + driver: 'local', + visibility: 'public', + + /* + |-------------------------------------------------------------------------- + | Storage root - Local driver only + |-------------------------------------------------------------------------- + | + | Define an absolute path to the storage directory from where to read the + | files. + | + */ + // root: Application.tmpPath('uploads'), + root: '/storage/app/public', + + /* + |-------------------------------------------------------------------------- + | Serve files - Local driver only + |-------------------------------------------------------------------------- + | + | When this is set to true, AdonisJS will configure a files server to serve + | files from the disk root. This is done to mimic the behavior of cloud + | storage services that has inbuilt capabilities to serve files. + | + */ + serveFiles: true, + + /* + |-------------------------------------------------------------------------- + | Base path - Local driver only + |-------------------------------------------------------------------------- + | + | Base path is always required when "serveFiles = true". Also make sure + | the `basePath` is unique across all the disks using "local" driver and + | you are not registering routes with this prefix. + | + */ + basePath: '/public', + }, + + /* + |-------------------------------------------------------------------------- + | S3 Driver + |-------------------------------------------------------------------------- + | + | Uses the S3 cloud storage to manage files. Make sure to install the s3 + | drive separately when using it. + | + |************************************************************************** + | npm i @adonisjs/drive-s3 + |************************************************************************** + | + */ + // s3: { + // driver: 's3', + // visibility: 'public', + // key: Env.get('S3_KEY'), + // secret: Env.get('S3_SECRET'), + // region: Env.get('S3_REGION'), + // bucket: Env.get('S3_BUCKET'), + // endpoint: Env.get('S3_ENDPOINT'), + // + // // For minio to work + // // forcePathStyle: true, + // }, + + /* + |-------------------------------------------------------------------------- + | GCS Driver + |-------------------------------------------------------------------------- + | + | Uses the Google cloud storage to manage files. Make sure to install the GCS + | drive separately when using it. + | + |************************************************************************** + | npm i @adonisjs/drive-gcs + |************************************************************************** + | + */ + // gcs: { + // driver: 'gcs', + // visibility: 'public', + // keyFilename: Env.get('GCS_KEY_FILENAME'), + // bucket: Env.get('GCS_BUCKET'), + + /* + |-------------------------------------------------------------------------- + | Uniform ACL - Google cloud storage only + |-------------------------------------------------------------------------- + | + | When using the Uniform ACL on the bucket, the "visibility" option is + | ignored. Since, the files ACL is managed by the google bucket policies + | directly. + | + |************************************************************************** + | Learn more: https://cloud.google.com/storage/docs/uniform-bucket-level-access + |************************************************************************** + | + | The following option just informs drive whether your bucket is using uniform + | ACL or not. The actual setting needs to be toggled within the Google cloud + | console. + | + */ + // usingUniformAcl: false, + // }, + }, +}); diff --git a/package-lock.json b/package-lock.json index 5691231..7baeb0a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "devDependencies": { "@adonisjs/assembler": "^7.1.1", "@adonisjs/tsconfig": "^1.4.0", + "@headlessui/vue": "^1.7.23", "@japa/assert": "^4.0.1", "@japa/plugin-adonisjs": "^4.0.0", "@japa/runner": "^4.2.0", @@ -551,14 +552,14 @@ } }, "node_modules/@adonisjs/logger": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/@adonisjs/logger/-/logger-6.0.5.tgz", - "integrity": "sha512-1QmbLPNC636MeJzqflMA64lUnAn5dbb7W0YQ/ea33papnNqGOfvDQuxqqKlzM6ww9jPZlXTIf/3t7KAWlfHCfQ==", + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/@adonisjs/logger/-/logger-6.0.6.tgz", + "integrity": "sha512-r5mLmmklSezzu3cu9QaXle2/gPNrgKpiIo+utYlwV3ITsW5JeIX/xcwwMTNM/9f1zU+SwOj5NccPTEFD3feRaw==", "license": "MIT", "dependencies": { - "@poppinss/utils": "^6.8.3", + "@poppinss/utils": "^6.9.2", "abstract-logging": "^2.0.1", - "pino": "^9.5.0" + "pino": "^9.6.0" }, "engines": { "node": ">=18.16.0" @@ -1849,6 +1850,22 @@ "integrity": "sha512-weN3E+rq0Xb3Z93VHJ+Rc7WOQX9ETJPTAJ+gDcaMHtjft67L58sfS65rAjC5tZUXQ2FdZ/V1/sSzCwZ6v05kJw==", "license": "OFL-1.1" }, + "node_modules/@headlessui/vue": { + "version": "1.7.23", + "resolved": "https://registry.npmjs.org/@headlessui/vue/-/vue-1.7.23.tgz", + "integrity": "sha512-JzdCNqurrtuu0YW6QaDtR2PIYCKPUWq28csDyMvN4zmGccmE7lz40Is6hc3LA4HFeCI7sekZ/PQMTNmn9I/4Wg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/vue-virtual": "^3.0.0-beta.60" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -3104,9 +3121,9 @@ "license": "CC0-1.0" }, "node_modules/@swc/wasm": { - "version": "1.10.16", - "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.10.16.tgz", - "integrity": "sha512-ZfGQkLM3rmohm+JEMdtSi2713AI8z4giK5rCV5UiVAYYM0APjl4C2KaUD4RMcKUkP04oiUjHNkkmk6u1MMdYzA==", + "version": "1.10.18", + "resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.10.18.tgz", + "integrity": "sha512-TgoMYjQ2/9UfUaw7WuKj7Svew6kaNOqkjV4nKoc2tf34e+7GxL2KPoXvM2b1RkPxNocv85glcQpS9KMk8FqpBA==", "dev": true, "license": "Apache-2.0" }, @@ -3135,6 +3152,34 @@ "tailwindcss": ">=3.0.0 || >= 3.0.0-alpha.1 || >= 4.0.0-alpha.20 || >= 4.0.0-beta.1" } }, + "node_modules/@tanstack/virtual-core": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.0.tgz", + "integrity": "sha512-NBKJP3OIdmZY3COJdWkSonr50FMVIi+aj5ZJ7hI/DTpEKg2RMfo/KvP8A3B/zOSpMgIe52B5E2yn7rryULzA6g==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/vue-virtual": { + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/@tanstack/vue-virtual/-/vue-virtual-3.13.0.tgz", + "integrity": "sha512-EPgcTc41KGJAK2N2Ux2PeUnG3cPpdkldTib05nwq+0zdS2Ihpbq8BsWXz/eXPyNc5noDBh1GBgAe36yMYiW6WA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tanstack/virtual-core": "3.13.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "vue": "^2.7.0 || ^3.0.0" + } + }, "node_modules/@tokenizer/inflate": { "version": "0.2.6", "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.6.tgz", @@ -5158,9 +5203,9 @@ "license": "CC-BY-4.0" }, "node_modules/case-anything": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-3.1.0.tgz", - "integrity": "sha512-rRYnn5Elur8RuNHKoJ2b0tgn+pjYxL7BzWom+JZ7NKKn1lt/yGV/tUNwOovxYa9l9VL5hnXQdMc+mENbhJzosQ==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/case-anything/-/case-anything-3.1.2.tgz", + "integrity": "sha512-wljhAjDDIv/hM2FzgJnYQg90AWmZMNtESCjTeLH680qTzdo0nErlCxOmgzgX4ZsZAtIvqHyD87ES8QyriXB+BQ==", "license": "MIT", "engines": { "node": ">=18" @@ -5217,9 +5262,9 @@ } }, "node_modules/chart.js": { - "version": "4.4.7", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz", - "integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==", + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz", + "integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==", "dev": true, "license": "MIT", "dependencies": { @@ -6302,9 +6347,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.101", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.101.tgz", - "integrity": "sha512-L0ISiQrP/56Acgu4/i/kfPwWSgrzYZUnQrC0+QPFuhqlLP1Ir7qzPPDVS9BcKIyWTRU8+o6CC8dKw38tSWhYIA==", + "version": "1.5.102", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.102.tgz", + "integrity": "sha512-eHhqaja8tE/FNpIiBrvBjFV/SSKpyWHLvxuR9dPTdo+3V9ppdLmFB7ZZQ98qNovcngPLYIz0oOBF9P0FfZef5Q==", "dev": true, "license": "ISC" }, @@ -7322,9 +7367,9 @@ } }, "node_modules/flatted": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", - "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", "dev": true, "license": "ISC" }, @@ -9002,9 +9047,9 @@ } }, "node_modules/launch-editor": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", - "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz", + "integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==", "dev": true, "license": "MIT", "dependencies": { @@ -9319,13 +9364,13 @@ } }, "node_modules/memoize": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.0.0.tgz", - "integrity": "sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/memoize/-/memoize-10.1.0.tgz", + "integrity": "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg==", "devOptional": true, "license": "MIT", "dependencies": { - "mimic-function": "^5.0.0" + "mimic-function": "^5.0.1" }, "engines": { "node": ">=18" @@ -10585,9 +10630,9 @@ } }, "node_modules/postcss": { - "version": "8.5.2", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz", - "integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "funding": [ { "type": "opencollective", @@ -13349,13 +13394,13 @@ } }, "node_modules/vite": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz", - "integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==", + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.1.1.tgz", + "integrity": "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA==", "license": "MIT", "dependencies": { "esbuild": "^0.24.2", - "postcss": "^8.5.1", + "postcss": "^8.5.2", "rollup": "^4.30.1" }, "bin": { diff --git a/package.json b/package.json index f8d95ea..2065045 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "devDependencies": { "@adonisjs/assembler": "^7.1.1", "@adonisjs/tsconfig": "^1.4.0", + "@headlessui/vue": "^1.7.23", "@japa/assert": "^4.0.1", "@japa/plugin-adonisjs": "^4.0.0", "@japa/runner": "^4.2.0", diff --git a/providers/drive/drivers/local.ts b/providers/drive/drivers/local.ts index b008752..7073cd0 100644 --- a/providers/drive/drivers/local.ts +++ b/providers/drive/drivers/local.ts @@ -74,7 +74,8 @@ export class LocalDriver implements LocalDriverContract { */ public async exists(location: string): Promise { try { - return await this.adapter.pathExists(this.makePath(location)); + let path_temp = this.makePath(location); //'/storage/app/files/421' + return await this.adapter.pathExists(path_temp); } catch (error) { throw CannotGetMetaDataException.invoke(location, 'exists', error); } diff --git a/providers/vinejs_provider.ts b/providers/vinejs_provider.ts index c83f258..e4aad4c 100644 --- a/providers/vinejs_provider.ts +++ b/providers/vinejs_provider.ts @@ -79,7 +79,9 @@ const isMultipartFile = vine.createRule(async (file: MultipartFile | unknown, op // if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) { // validatedFile.allowedExtensions = validationOptions.extnames; // } - if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) { + if (validatedFile.allowedExtensions === undefined && validationOptions.extnames !== undefined) { + validatedFile.allowedExtensions = validationOptions.extnames; // await getEnabledExtensions(); + } else if (validatedFile.allowedExtensions === undefined && validationOptions.extnames === undefined) { validatedFile.allowedExtensions = await getEnabledExtensions(); } /** diff --git a/resources/js/Components/.FormField.vue.swo b/resources/js/Components/.FormField.vue.swo new file mode 100644 index 0000000..b00c9b9 Binary files /dev/null and b/resources/js/Components/.FormField.vue.swo differ diff --git a/resources/js/Components/CardBox.vue b/resources/js/Components/CardBox.vue index e00ec72..c2d200e 100644 --- a/resources/js/Components/CardBox.vue +++ b/resources/js/Components/CardBox.vue @@ -12,6 +12,10 @@ const props = defineProps({ type: String, default: null, }, + showHeaderIcon: { + type: Boolean, + default: true, + }, headerIcon: { type: String, default: null, @@ -63,7 +67,7 @@ const submit = (e) => { {{ title }} - diff --git a/resources/js/Components/FormControl.vue b/resources/js/Components/FormControl.vue index 32085b7..c768b1d 100644 --- a/resources/js/Components/FormControl.vue +++ b/resources/js/Components/FormControl.vue @@ -118,6 +118,9 @@ if (props.ctrlKFocus) { mainService.isFieldFocusRegistered = false; }); } +const focus = () => { + inputEl?.value.focus(); +};