feat: Integrate official drive_provider, update user profile features & UI improvements
All checks were successful
CI / container-job (push) Successful in 41s
All checks were successful
CI / container-job (push) Successful in 41s
- adonisrc.ts: Load official drive_provider and unload custom driver_provider. - packages.json: Add @headlessui/vue dependency for tab components. - AvatarController.ts: Rewrite avatar generation logic to always return the same avatar per user. - auth/UserController.ts: Add profile and profileUpdate methods to support user profile editing. - Submitter/datasetController.ts & app/models/file.ts: Adapt code to use the official drive_provider. - app/models/user.ts: Introduce “isAdmin” getter. - config/drive.ts: Create new configuration for the official drive_provider. - providers/vinejs_provider.ts: Adapt allowedExtensions control to use provided options or database enabled extensions. - resource/js/app.ts: Load default Head and Link components. - resources/js/menu.ts: Add settings-profile.edit menu point. - resources/js/Components/action-message.vue: Add new component for improved user feedback after form submissions. - New avatar-input.vue component: Enable profile picture selection. - Components/CardBox.vue: Alter layout to optionally show HeaderIcon in title bar. - FormControl.vue: Define a readonly prop for textareas. - Improve overall UI with updates to NavBar.vue, UserAvatar.vue, UserAvatarCurrentUser.vue, and add v-model support to password-meter.vue. - Remove profile editing logic from AccountInfo.vue and introduce new profile components (show.vue, update-password-form.vue, update-profile-information.vue). - app.edge: Modify page (add @inertiaHead tag) for better meta management. - routes.ts: Add new routes for editing user profiles. - General npm updates.
This commit is contained in:
parent
a41b091214
commit
36cd7a757b
34 changed files with 1396 additions and 407 deletions
|
@ -70,7 +70,8 @@ export default defineConfig({
|
||||||
() => import('#providers/query_builder_provider'),
|
() => import('#providers/query_builder_provider'),
|
||||||
() => import('#providers/token_worker_provider'),
|
() => import('#providers/token_worker_provider'),
|
||||||
// () => import('#providers/validator_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('@adonisjs/core/providers/vinejs_provider'),
|
||||||
() => import('#providers/vinejs_provider'),
|
() => import('#providers/vinejs_provider'),
|
||||||
() => import('@adonisjs/mail/mail_provider'),
|
() => import('@adonisjs/mail/mail_provider'),
|
||||||
|
|
|
@ -1,34 +1,28 @@
|
||||||
import type { HttpContext } from '@adonisjs/core/http';
|
import type { HttpContext } from '@adonisjs/core/http';
|
||||||
import { StatusCodes } from 'http-status-codes';
|
import { StatusCodes } from 'http-status-codes';
|
||||||
// import * as fs from 'fs';
|
|
||||||
// import * as path from 'path';
|
|
||||||
|
|
||||||
const prefixes = ['von', 'van'];
|
const prefixes = ['von', 'van'];
|
||||||
|
|
||||||
// node ace make:controller Author
|
|
||||||
export default class AvatarController {
|
export default class AvatarController {
|
||||||
public async generateAvatar({ request, response }: HttpContext) {
|
public async generateAvatar({ request, response }: HttpContext) {
|
||||||
try {
|
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);
|
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 = `
|
const svgContent = `
|
||||||
<svg width="${size || 50}" height="${size || 50}" xmlns="http://www.w3.org/2000/svg">
|
<svg width="${size || 50}" height="${size || 50}" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect width="100%" height="100%" fill="#${background || '7F9CF5'}"/>
|
<rect width="100%" height="100%" fill="#${backgroundColor}"/>
|
||||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-weight="bold" font-family="Arial, sans-serif" font-size="${
|
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-weight="bold" font-family="Arial, sans-serif" font-size="${
|
||||||
(size / 100) * 40 || 25
|
(size / 100) * 40 || 25
|
||||||
}" fill="#${textColor || 'ffffff'}">${initials}</text>
|
}" fill="#${textColor}">${initials}</text>
|
||||||
</svg>
|
</svg>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// Set response headers for SVG content
|
|
||||||
response.header('Content-type', 'image/svg+xml');
|
response.header('Content-type', 'image/svg+xml');
|
||||||
response.header('Cache-Control', 'no-cache');
|
response.header('Cache-Control', 'no-cache');
|
||||||
response.header('Pragma', 'no-cache');
|
response.header('Pragma', 'no-cache');
|
||||||
|
@ -62,4 +56,49 @@ export default class AvatarController {
|
||||||
|
|
||||||
return initials;
|
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');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,11 @@ import hash from '@adonisjs/core/services/hash';
|
||||||
// import { schema, rules } from '@adonisjs/validator';
|
// import { schema, rules } from '@adonisjs/validator';
|
||||||
import vine from '@vinejs/vine';
|
import vine from '@vinejs/vine';
|
||||||
import BackupCodeStorage, { SecureRandom } from '#services/backup_code_storage';
|
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.
|
// 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 {
|
export default class UserController {
|
||||||
|
@ -28,7 +33,7 @@ export default class UserController {
|
||||||
user: user,
|
user: user,
|
||||||
twoFactorEnabled: user.isTwoFactorEnabled,
|
twoFactorEnabled: user.isTwoFactorEnabled,
|
||||||
// code: await TwoFactorAuthProvider.generateQrCode(user),
|
// code: await TwoFactorAuthProvider.generateQrCode(user),
|
||||||
backupState: backupState,
|
backupState: backupState,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,10 +45,8 @@ export default class UserController {
|
||||||
// });
|
// });
|
||||||
const passwordSchema = vine.object({
|
const passwordSchema = vine.object({
|
||||||
// first step
|
// first step
|
||||||
old_password: vine
|
old_password: vine.string().trim(),
|
||||||
.string()
|
// .regex(/^[a-zA-Z0-9]+$/),
|
||||||
.trim()
|
|
||||||
.regex(/^[a-zA-Z0-9]+$/),
|
|
||||||
new_password: vine.string().confirmed({ confirmationField: 'confirm_password' }).trim().minLength(8).maxLength(255),
|
new_password: vine.string().confirmed({ confirmationField: 'confirm_password' }).trim().minLength(8).maxLength(255),
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
@ -56,7 +59,7 @@ export default class UserController {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
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']);
|
const { old_password, new_password } = request.only(['old_password', 'new_password']);
|
||||||
|
|
||||||
// if (!(old_password && new_password && confirm_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<void>}
|
||||||
|
*/
|
||||||
|
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<void> {
|
public async enableTwoFactorAuthentication({ auth, response, session }: HttpContext): Promise<void> {
|
||||||
// const user: User | undefined = auth?.user;
|
// const user: User | undefined = auth?.user;
|
||||||
const user = (await User.find(auth.user?.id)) as User;
|
const user = (await User.find(auth.user?.id)) as User;
|
||||||
|
|
|
@ -33,7 +33,9 @@ import File from '#models/file';
|
||||||
import ClamScan from 'clamscan';
|
import ClamScan from 'clamscan';
|
||||||
// import { ValidationException } from '@adonisjs/validator';
|
// import { ValidationException } from '@adonisjs/validator';
|
||||||
// import Drive from '@ioc:Adonis/Core/Drive';
|
// 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 { Exception } from '@adonisjs/core/exceptions';
|
||||||
import { MultipartFile } from '@adonisjs/core/types/bodyparser';
|
import { MultipartFile } from '@adonisjs/core/types/bodyparser';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
@ -532,11 +534,18 @@ export default class DatasetController {
|
||||||
const fileName = this.generateFilename(file.extname as string);
|
const fileName = this.generateFilename(file.extname as string);
|
||||||
const mimeType = file.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type
|
const mimeType = file.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type
|
||||||
const datasetFolder = `files/${dataset.id}`;
|
const datasetFolder = `files/${dataset.id}`;
|
||||||
|
const datasetFullPath = path.join(`${datasetFolder}`, fileName);
|
||||||
// const size = file.size;
|
// 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,
|
name: fileName,
|
||||||
overwrite: true, // overwrite in case of conflict
|
overwrite: true, // overwrite in case of conflict
|
||||||
|
disk: 'local',
|
||||||
});
|
});
|
||||||
|
|
||||||
// save file metadata into db
|
// save file metadata into db
|
||||||
const newFile = new File();
|
const newFile = new File();
|
||||||
newFile.pathName = `${datasetFolder}/${fileName}`;
|
newFile.pathName = `${datasetFolder}/${fileName}`;
|
||||||
|
@ -1031,10 +1040,16 @@ export default class DatasetController {
|
||||||
// move to disk:
|
// move to disk:
|
||||||
const fileName = `file-${cuid()}.${fileData.extname}`; //'file-ls0jyb8xbzqtrclufu2z2e0c.pdf'
|
const fileName = `file-${cuid()}.${fileData.extname}`; //'file-ls0jyb8xbzqtrclufu2z2e0c.pdf'
|
||||||
const datasetFolder = `files/${dataset.id}`; // 'files/307'
|
const datasetFolder = `files/${dataset.id}`; // 'files/307'
|
||||||
|
const datasetFullPath = path.join(`${datasetFolder}`, fileName);
|
||||||
// await fileData.moveToDisk(datasetFolder, { name: fileName, overwrite: true }, 'local');
|
// 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,
|
name: fileName,
|
||||||
overwrite: true, // overwrite in case of conflict
|
overwrite: true, // overwrite in case of conflict
|
||||||
|
driver: 'local',
|
||||||
});
|
});
|
||||||
|
|
||||||
//save to db:
|
//save to db:
|
||||||
|
@ -1161,31 +1176,32 @@ export default class DatasetController {
|
||||||
if (validStates.includes(dataset.server_state)) {
|
if (validStates.includes(dataset.server_state)) {
|
||||||
if (dataset.files && dataset.files.length > 0) {
|
if (dataset.files && dataset.files.length > 0) {
|
||||||
for (const file of dataset.files) {
|
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();
|
await file.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const datasetFolder = `files/${params.id}`;
|
const datasetFolder = `files/${params.id}`;
|
||||||
const folderExists = await drive.exists(datasetFolder);
|
// const folderExists = await drive.use('local').exists(datasetFolder);
|
||||||
if (folderExists) {
|
// if (folderExists) {
|
||||||
const dirListing = drive.list(datasetFolder);
|
// const dirListing = drive.list(datasetFolder);
|
||||||
const folderContents = await dirListing.toArray();
|
// const folderContents = await dirListing.toArray();
|
||||||
if (folderContents.length === 0) {
|
// if (folderContents.length === 0) {
|
||||||
await drive.delete(datasetFolder);
|
// await drive.delete(datasetFolder);
|
||||||
}
|
// }
|
||||||
|
await drive.use('local').deleteAll(datasetFolder);
|
||||||
// delete dataset wirh relation in db
|
// delete dataset wirh relation in db
|
||||||
await dataset.delete();
|
await dataset.delete();
|
||||||
session.flash({ message: 'You have deleted 1 dataset!' });
|
session.flash({ message: 'You have deleted 1 dataset!' });
|
||||||
return response.redirect().toRoute('dataset.list');
|
return response.redirect().toRoute('dataset.list');
|
||||||
} else {
|
// } else {
|
||||||
// session.flash({
|
// // session.flash({
|
||||||
// warning: `You cannot delete this dataset! Invalid server_state: "${dataset.server_state}"!`,
|
// // warning: `You cannot delete this dataset! Invalid server_state: "${dataset.server_state}"!`,
|
||||||
// });
|
// // });
|
||||||
return response
|
// return response
|
||||||
.flash({ warning: `You cannot delete this dataset! Dataset folder "${datasetFolder}" doesn't exist!` })
|
// .flash({ warning: `You cannot delete this dataset! Dataset folder "${datasetFolder}" doesn't exist!` })
|
||||||
.redirect()
|
// .redirect()
|
||||||
.back();
|
// .back();
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof errors.E_VALIDATION_ERROR) {
|
if (error instanceof errors.E_VALIDATION_ERROR) {
|
||||||
|
@ -1193,7 +1209,8 @@ export default class DatasetController {
|
||||||
throw error;
|
throw error;
|
||||||
} else if (error instanceof Exception) {
|
} else if (error instanceof Exception) {
|
||||||
// General exception handling
|
// General exception handling
|
||||||
return response.flash('errors', error.message).redirect().back();
|
session.flash({ error: error.message});
|
||||||
|
return response.redirect().back();
|
||||||
} else {
|
} else {
|
||||||
session.flash({ error: 'An error occurred while deleting the dataset.' });
|
session.flash({ error: 'An error occurred while deleting the dataset.' });
|
||||||
return response.redirect().back();
|
return response.redirect().back();
|
||||||
|
|
|
@ -7,7 +7,8 @@ import * as fs from 'fs';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
// import Drive from '@ioc:Adonis/Core/Drive';
|
// import Drive from '@ioc:Adonis/Core/Drive';
|
||||||
// import Drive from '@adonisjs/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 { HasMany } from "@adonisjs/lucid/types/relations";
|
||||||
import type { BelongsTo } 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',
|
serializeAs: 'filePath',
|
||||||
})
|
})
|
||||||
public get 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');
|
// const mainTitle = this.titles?.find((title) => title.type === 'Main');
|
||||||
// return mainTitle ? mainTitle.value : null;
|
// return mainTitle ? mainTitle.value : null;
|
||||||
}
|
}
|
||||||
|
@ -164,7 +166,7 @@ export default class File extends BaseModel {
|
||||||
public async delete() {
|
public async delete() {
|
||||||
if (this.pathName) {
|
if (this.pathName) {
|
||||||
// Delete file from additional storage
|
// 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
|
// Call the original delete method of the BaseModel to remove the record from the database
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid';
|
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 hash from '@adonisjs/core/services/hash';
|
||||||
import Role from './role.js';
|
import Role from './role.js';
|
||||||
import db from '@adonisjs/lucid/services/db';
|
import db from '@adonisjs/lucid/services/db';
|
||||||
|
@ -49,7 +49,6 @@ export default class User extends compose(BaseModel, AuthFinder) {
|
||||||
@column()
|
@column()
|
||||||
public login: string;
|
public login: string;
|
||||||
|
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
public firstName: string;
|
public firstName: string;
|
||||||
|
|
||||||
|
@ -87,6 +86,9 @@ export default class User extends compose(BaseModel, AuthFinder) {
|
||||||
@column({})
|
@column({})
|
||||||
public state: number;
|
public state: number;
|
||||||
|
|
||||||
|
@column({})
|
||||||
|
public avatar: string;
|
||||||
|
|
||||||
// @hasOne(() => TotpSecret, {
|
// @hasOne(() => TotpSecret, {
|
||||||
// foreignKey: 'user_id',
|
// foreignKey: 'user_id',
|
||||||
// })
|
// })
|
||||||
|
@ -104,6 +106,7 @@ export default class User extends compose(BaseModel, AuthFinder) {
|
||||||
// return Boolean(this.totp_secret?.twoFactorSecret);
|
// return Boolean(this.totp_secret?.twoFactorSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@manyToMany(() => Role, {
|
@manyToMany(() => Role, {
|
||||||
pivotForeignKey: 'account_id',
|
pivotForeignKey: 'account_id',
|
||||||
pivotRelatedForeignKey: 'role_id',
|
pivotRelatedForeignKey: 'role_id',
|
||||||
|
@ -121,6 +124,27 @@ export default class User extends compose(BaseModel, AuthFinder) {
|
||||||
})
|
})
|
||||||
public backupcodes: HasMany<typeof BackupCode>;
|
public backupcodes: HasMany<typeof BackupCode>;
|
||||||
|
|
||||||
|
@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<BackupCode[]> {
|
public async getBackupCodes(this: User): Promise<BackupCode[]> {
|
||||||
const test = await this.related('backupcodes').query();
|
const test = await this.related('backupcodes').query();
|
||||||
// return test.map((role) => role.code);
|
// return test.map((role) => role.code);
|
||||||
|
|
180
config/drive.ts
180
config/drive.ts
|
@ -1,151 +1,45 @@
|
||||||
/**
|
// import env from '#start/env'
|
||||||
* Config source: https://git.io/JBt3o
|
// import app from '@adonisjs/core/services/app'
|
||||||
*
|
import { defineConfig, services } from '@adonisjs/drive'
|
||||||
* 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';
|
|
||||||
|
|
||||||
/*
|
const driveConfig = defineConfig({
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| 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: {
|
default: 'public',
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| 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',
|
|
||||||
|
|
||||||
/*
|
services: {
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| 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,
|
|
||||||
|
|
||||||
/*
|
/**
|
||||||
|--------------------------------------------------------------------------
|
* Persist files on the local filesystem
|
||||||
| Base path - Local driver only
|
*/
|
||||||
|--------------------------------------------------------------------------
|
public: services.fs({
|
||||||
|
|
location: '/storage/app/public/',
|
||||||
| Base path is always required when "serveFiles = true". Also make sure
|
serveFiles: true,
|
||||||
| the `basePath` is unique across all the disks using "local" driver and
|
routeBasePath: '/public',
|
||||||
| you are not registering routes with this prefix.
|
visibility: 'public',
|
||||||
|
|
}),
|
||||||
*/
|
local: services.fs({
|
||||||
basePath: '/uploads',
|
location: '/storage/app/data/',
|
||||||
},
|
serveFiles: true,
|
||||||
|
routeBasePath: '/data',
|
||||||
|
visibility: '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,
|
|
||||||
// },
|
|
||||||
|
|
||||||
/*
|
/**
|
||||||
|--------------------------------------------------------------------------
|
* Persist files on Digital Ocean spaces
|
||||||
| GCS Driver
|
*/
|
||||||
|--------------------------------------------------------------------------
|
// spaces: services.s3({
|
||||||
|
|
// credentials: {
|
||||||
| Uses the Google cloud storage to manage files. Make sure to install the GCS
|
// accessKeyId: env.get('SPACES_KEY'),
|
||||||
| drive separately when using it.
|
// secretAccessKey: env.get('SPACES_SECRET'),
|
||||||
|
|
// },
|
||||||
|**************************************************************************
|
// region: env.get('SPACES_REGION'),
|
||||||
| npm i @adonisjs/drive-gcs
|
// bucket: env.get('SPACES_BUCKET'),
|
||||||
|**************************************************************************
|
// endpoint: env.get('SPACES_ENDPOINT'),
|
||||||
|
|
// visibility: 'public',
|
||||||
*/
|
// }),
|
||||||
// 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,
|
|
||||||
// },
|
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
|
export default driveConfig
|
233
config/drive_self.ts
Normal file
233
config/drive_self.ts
Normal file
|
@ -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,
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
});
|
113
package-lock.json
generated
113
package-lock.json
generated
|
@ -58,6 +58,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@adonisjs/assembler": "^7.1.1",
|
"@adonisjs/assembler": "^7.1.1",
|
||||||
"@adonisjs/tsconfig": "^1.4.0",
|
"@adonisjs/tsconfig": "^1.4.0",
|
||||||
|
"@headlessui/vue": "^1.7.23",
|
||||||
"@japa/assert": "^4.0.1",
|
"@japa/assert": "^4.0.1",
|
||||||
"@japa/plugin-adonisjs": "^4.0.0",
|
"@japa/plugin-adonisjs": "^4.0.0",
|
||||||
"@japa/runner": "^4.2.0",
|
"@japa/runner": "^4.2.0",
|
||||||
|
@ -551,14 +552,14 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@adonisjs/logger": {
|
"node_modules/@adonisjs/logger": {
|
||||||
"version": "6.0.5",
|
"version": "6.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/@adonisjs/logger/-/logger-6.0.5.tgz",
|
"resolved": "https://registry.npmjs.org/@adonisjs/logger/-/logger-6.0.6.tgz",
|
||||||
"integrity": "sha512-1QmbLPNC636MeJzqflMA64lUnAn5dbb7W0YQ/ea33papnNqGOfvDQuxqqKlzM6ww9jPZlXTIf/3t7KAWlfHCfQ==",
|
"integrity": "sha512-r5mLmmklSezzu3cu9QaXle2/gPNrgKpiIo+utYlwV3ITsW5JeIX/xcwwMTNM/9f1zU+SwOj5NccPTEFD3feRaw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@poppinss/utils": "^6.8.3",
|
"@poppinss/utils": "^6.9.2",
|
||||||
"abstract-logging": "^2.0.1",
|
"abstract-logging": "^2.0.1",
|
||||||
"pino": "^9.5.0"
|
"pino": "^9.6.0"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.16.0"
|
"node": ">=18.16.0"
|
||||||
|
@ -1849,6 +1850,22 @@
|
||||||
"integrity": "sha512-weN3E+rq0Xb3Z93VHJ+Rc7WOQX9ETJPTAJ+gDcaMHtjft67L58sfS65rAjC5tZUXQ2FdZ/V1/sSzCwZ6v05kJw==",
|
"integrity": "sha512-weN3E+rq0Xb3Z93VHJ+Rc7WOQX9ETJPTAJ+gDcaMHtjft67L58sfS65rAjC5tZUXQ2FdZ/V1/sSzCwZ6v05kJw==",
|
||||||
"license": "OFL-1.1"
|
"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": {
|
"node_modules/@humanwhocodes/config-array": {
|
||||||
"version": "0.13.0",
|
"version": "0.13.0",
|
||||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz",
|
||||||
|
@ -3104,9 +3121,9 @@
|
||||||
"license": "CC0-1.0"
|
"license": "CC0-1.0"
|
||||||
},
|
},
|
||||||
"node_modules/@swc/wasm": {
|
"node_modules/@swc/wasm": {
|
||||||
"version": "1.10.16",
|
"version": "1.10.18",
|
||||||
"resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.10.16.tgz",
|
"resolved": "https://registry.npmjs.org/@swc/wasm/-/wasm-1.10.18.tgz",
|
||||||
"integrity": "sha512-ZfGQkLM3rmohm+JEMdtSi2713AI8z4giK5rCV5UiVAYYM0APjl4C2KaUD4RMcKUkP04oiUjHNkkmk6u1MMdYzA==",
|
"integrity": "sha512-TgoMYjQ2/9UfUaw7WuKj7Svew6kaNOqkjV4nKoc2tf34e+7GxL2KPoXvM2b1RkPxNocv85glcQpS9KMk8FqpBA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0"
|
"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"
|
"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": {
|
"node_modules/@tokenizer/inflate": {
|
||||||
"version": "0.2.6",
|
"version": "0.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.6.tgz",
|
"resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.2.6.tgz",
|
||||||
|
@ -5158,9 +5203,9 @@
|
||||||
"license": "CC-BY-4.0"
|
"license": "CC-BY-4.0"
|
||||||
},
|
},
|
||||||
"node_modules/case-anything": {
|
"node_modules/case-anything": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/case-anything/-/case-anything-3.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/case-anything/-/case-anything-3.1.2.tgz",
|
||||||
"integrity": "sha512-rRYnn5Elur8RuNHKoJ2b0tgn+pjYxL7BzWom+JZ7NKKn1lt/yGV/tUNwOovxYa9l9VL5hnXQdMc+mENbhJzosQ==",
|
"integrity": "sha512-wljhAjDDIv/hM2FzgJnYQg90AWmZMNtESCjTeLH680qTzdo0nErlCxOmgzgX4ZsZAtIvqHyD87ES8QyriXB+BQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
@ -5217,9 +5262,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chart.js": {
|
"node_modules/chart.js": {
|
||||||
"version": "4.4.7",
|
"version": "4.4.8",
|
||||||
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.7.tgz",
|
"resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.8.tgz",
|
||||||
"integrity": "sha512-pwkcKfdzTMAU/+jNosKhNL2bHtJc/sSmYgVbuGTEDhzkrhmyihmP7vUc/5ZK9WopidMDHNe3Wm7jOd/WhuHWuw==",
|
"integrity": "sha512-IkGZlVpXP+83QpMm4uxEiGqSI7jFizwVtF3+n5Pc3k7sMO+tkd0qxh2OzLhenM0K80xtmAONWGBn082EiBQSDA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -6302,9 +6347,9 @@
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.5.101",
|
"version": "1.5.102",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.101.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.102.tgz",
|
||||||
"integrity": "sha512-L0ISiQrP/56Acgu4/i/kfPwWSgrzYZUnQrC0+QPFuhqlLP1Ir7qzPPDVS9BcKIyWTRU8+o6CC8dKw38tSWhYIA==",
|
"integrity": "sha512-eHhqaja8tE/FNpIiBrvBjFV/SSKpyWHLvxuR9dPTdo+3V9ppdLmFB7ZZQ98qNovcngPLYIz0oOBF9P0FfZef5Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
@ -7322,9 +7367,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/flatted": {
|
"node_modules/flatted": {
|
||||||
"version": "3.3.2",
|
"version": "3.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||||
"integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==",
|
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
|
@ -9002,9 +9047,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/launch-editor": {
|
"node_modules/launch-editor": {
|
||||||
"version": "2.9.1",
|
"version": "2.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.10.0.tgz",
|
||||||
"integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==",
|
"integrity": "sha512-D7dBRJo/qcGX9xlvt/6wUYzQxjh5G1RvZPgPv8vi4KRU99DVQL/oW7tnVOCCTm2HGeo3C5HvGE5Yrh6UBoZ0vA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -9319,13 +9364,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/memoize": {
|
"node_modules/memoize": {
|
||||||
"version": "10.0.0",
|
"version": "10.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/memoize/-/memoize-10.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/memoize/-/memoize-10.1.0.tgz",
|
||||||
"integrity": "sha512-H6cBLgsi6vMWOcCpvVCdFFnl3kerEXbrYh9q+lY6VXvQSmM6CkmV08VOwT+WE2tzIEqRPFfAq3fm4v/UIW6mSA==",
|
"integrity": "sha512-MMbFhJzh4Jlg/poq1si90XRlTZRDHVqdlz2mPyGJ6kqMpyHUyVpDd5gpFAvVehW64+RA1eKE9Yt8aSLY7w2Kgg==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"mimic-function": "^5.0.0"
|
"mimic-function": "^5.0.1"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
|
@ -10585,9 +10630,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/postcss": {
|
"node_modules/postcss": {
|
||||||
"version": "8.5.2",
|
"version": "8.5.3",
|
||||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz",
|
||||||
"integrity": "sha512-MjOadfU3Ys9KYoX0AdkBlFEF1Vx37uCCeN4ZHnmwm9FfpbsGWMZeBLMmmpY+6Ocqod7mkdZ0DT31OlbsFrLlkA==",
|
"integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==",
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
"type": "opencollective",
|
"type": "opencollective",
|
||||||
|
@ -13349,13 +13394,13 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "6.1.0",
|
"version": "6.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.1.1.tgz",
|
||||||
"integrity": "sha512-RjjMipCKVoR4hVfPY6GQTgveinjNuyLw+qruksLDvA5ktI1150VmcMBKmQaEWJhg/j6Uaf6dNCNA0AfdzUb/hQ==",
|
"integrity": "sha512-4GgM54XrwRfrOp297aIYspIti66k56v16ZnqHvrIM7mG+HjDlAwS7p+Srr7J6fGvEdOJ5JcQ/D9T7HhtdXDTzA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.24.2",
|
"esbuild": "^0.24.2",
|
||||||
"postcss": "^8.5.1",
|
"postcss": "^8.5.2",
|
||||||
"rollup": "^4.30.1"
|
"rollup": "^4.30.1"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@adonisjs/assembler": "^7.1.1",
|
"@adonisjs/assembler": "^7.1.1",
|
||||||
"@adonisjs/tsconfig": "^1.4.0",
|
"@adonisjs/tsconfig": "^1.4.0",
|
||||||
|
"@headlessui/vue": "^1.7.23",
|
||||||
"@japa/assert": "^4.0.1",
|
"@japa/assert": "^4.0.1",
|
||||||
"@japa/plugin-adonisjs": "^4.0.0",
|
"@japa/plugin-adonisjs": "^4.0.0",
|
||||||
"@japa/runner": "^4.2.0",
|
"@japa/runner": "^4.2.0",
|
||||||
|
|
|
@ -74,7 +74,8 @@ export class LocalDriver implements LocalDriverContract {
|
||||||
*/
|
*/
|
||||||
public async exists(location: string): Promise<boolean> {
|
public async exists(location: string): Promise<boolean> {
|
||||||
try {
|
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) {
|
} catch (error) {
|
||||||
throw CannotGetMetaDataException.invoke(location, 'exists', error);
|
throw CannotGetMetaDataException.invoke(location, 'exists', error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,9 @@ const isMultipartFile = vine.createRule(async (file: MultipartFile | unknown, op
|
||||||
// if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) {
|
// if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) {
|
||||||
// validatedFile.allowedExtensions = 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();
|
validatedFile.allowedExtensions = await getEnabledExtensions();
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
|
|
BIN
resources/js/Components/.FormField.vue.swo
Normal file
BIN
resources/js/Components/.FormField.vue.swo
Normal file
Binary file not shown.
|
@ -12,6 +12,10 @@ const props = defineProps({
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
showHeaderIcon: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
headerIcon: {
|
headerIcon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -63,7 +67,7 @@ const submit = (e) => {
|
||||||
<BaseIcon v-if="icon" :path="icon" class="mr-3" />
|
<BaseIcon v-if="icon" :path="icon" class="mr-3" />
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</div>
|
</div>
|
||||||
<button class="flex items-center py-3 px-4 justify-center ring-blue-700 focus:ring" @click="headerIconClick">
|
<button v-if="showHeaderIcon" class="flex items-center py-3 px-4 justify-center ring-blue-700 focus:ring" @click="headerIconClick">
|
||||||
<BaseIcon :path="computedHeaderIcon" />
|
<BaseIcon :path="computedHeaderIcon" />
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -118,6 +118,9 @@ if (props.ctrlKFocus) {
|
||||||
mainService.isFieldFocusRegistered = false;
|
mainService.isFieldFocusRegistered = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const focus = () => {
|
||||||
|
inputEl?.value.focus();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -130,7 +133,7 @@ if (props.ctrlKFocus) {
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<textarea v-else-if="computedType === 'textarea'" :id="id" v-model="computedValue" :class="inputElClass"
|
<textarea v-else-if="computedType === 'textarea'" :id="id" v-model="computedValue" :class="inputElClass"
|
||||||
:name="name" :placeholder="placeholder" :required="required" />
|
:name="name" :placeholder="placeholder" :required="required" :readonly="isReadOnly"/>
|
||||||
<input v-else :id="id" ref="inputEl" v-model="computedValue" :name="name" :inputmode="inputmode"
|
<input v-else :id="id" ref="inputEl" v-model="computedValue" :name="name" :inputmode="inputmode"
|
||||||
:autocomplete="autocomplete" :required="required" :placeholder="placeholder" :type="computedType"
|
:autocomplete="autocomplete" :required="required" :placeholder="placeholder" :type="computedType"
|
||||||
:class="inputElClass" :readonly="isReadOnly" />
|
:class="inputElClass" :readonly="isReadOnly" />
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { computed, useSlots } from 'vue';
|
import { computed, useSlots } from 'vue';
|
||||||
|
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -15,6 +15,10 @@ defineProps({
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
// class: {
|
||||||
|
// type: Object,
|
||||||
|
// default: {},
|
||||||
|
// },
|
||||||
});
|
});
|
||||||
|
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
|
@ -36,7 +40,7 @@ const wrapperClass = computed(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-6 last:mb-0">
|
<div :class="['last:mb-0', 'mb-6']">
|
||||||
<!-- <label v-if="label" :for="labelFor" class="block font-bold mb-2">{{ label }}</label> -->
|
<!-- <label v-if="label" :for="labelFor" class="block font-bold mb-2">{{ label }}</label> -->
|
||||||
<label v-if="label" :for="labelFor" class="font-bold h-6 mt-3 text-xs leading-8 uppercase">{{ label }}</label>
|
<label v-if="label" :for="labelFor" class="font-bold h-6 mt-3 text-xs leading-8 uppercase">{{ label }}</label>
|
||||||
<div v-bind:class="wrapperClass">
|
<div v-bind:class="wrapperClass">
|
||||||
|
|
|
@ -150,7 +150,7 @@ const showAbout = async () => {
|
||||||
<!-- personal menu -->
|
<!-- personal menu -->
|
||||||
<NavBarMenu>
|
<NavBarMenu>
|
||||||
<NavBarItemLabel v-bind:label="`hello ${user.login}`">
|
<NavBarItemLabel v-bind:label="`hello ${user.login}`">
|
||||||
<UserAvatarCurrentUser class="w-6 h-6 mr-3 inline-flex" />
|
<UserAvatarCurrentUser :user="user" class="w-6 h-6 mr-3 inline-flex" />
|
||||||
</NavBarItemLabel>
|
</NavBarItemLabel>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<!-- <NavBarItem> -->
|
<!-- <NavBarItem> -->
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref, watch } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { checkStrength } from './logic/index';
|
import { checkStrength } from './logic/index';
|
||||||
import { mdiFormTextboxPassword } from '@mdi/js';
|
import { mdiFormTextboxPassword } from '@mdi/js';
|
||||||
import FormField from '@/Components/FormField.vue';
|
import FormField from '@/Components/FormField.vue';
|
||||||
|
@ -7,17 +7,25 @@ import FormControl from '@/Components/FormControl.vue';
|
||||||
|
|
||||||
// Define props
|
// Define props
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
password: string;
|
modelValue: string;
|
||||||
errors: Partial<Record<"new_password" | "old_password" | "confirm_password", string>>;
|
errors: Partial<Record<"new_password" | "old_password" | "confirm_password", string>>;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const emit = defineEmits(['update:password', 'score']);
|
const emit = defineEmits(['update:modelValue', 'score']);
|
||||||
|
|
||||||
// A local reactive variable for password input
|
// // A local reactive variable for password input
|
||||||
const localPassword = ref(props.password);
|
// const localPassword = ref(props.modelValue);
|
||||||
// Watch localPassword and emit changes back to the parent
|
// // Watch localPassword and emit changes back to the parent
|
||||||
watch(localPassword, (newValue) => {
|
// watch(localPassword, (newValue) => {
|
||||||
emit('update:password', newValue);
|
// emit('update:modelValue', newValue);
|
||||||
|
// });
|
||||||
|
const localPassword = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value) => {
|
||||||
|
emit('update:modelValue', value);
|
||||||
|
// const { score } = checkStrength(localPassword.value);
|
||||||
|
// emit('score', score);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
type PasswordMetrics = {
|
type PasswordMetrics = {
|
||||||
|
@ -53,7 +61,7 @@ const passwordMetrics = computed<PasswordMetrics>(() => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<!-- Password input Form -->
|
<!-- Password input Form -->
|
||||||
<FormField label="New password" help="Required. New password" :class="{ 'text-red-400': errors.new_password }">
|
<FormField label="New password" help="Required. New password" :class="{'text-red-400': errors.new_password }">
|
||||||
<FormControl v-model="localPassword" :icon="mdiFormTextboxPassword" name="new_password" type="password" required
|
<FormControl v-model="localPassword" :icon="mdiFormTextboxPassword" name="new_password" type="password" required
|
||||||
:error="errors.new_password">
|
:error="errors.new_password">
|
||||||
<!-- Secure Icon -->
|
<!-- Secure Icon -->
|
||||||
|
@ -72,6 +80,10 @@ const passwordMetrics = computed<PasswordMetrics>(() => {
|
||||||
{{ errors.new_password }}
|
{{ errors.new_password }}
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
<!-- Score Display -->
|
||||||
|
<div class="text-gray-700 text-sm">
|
||||||
|
{{ passwordMetrics.score }} / 6 points max
|
||||||
|
</div>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<!-- Password Strength Bar -->
|
<!-- Password Strength Bar -->
|
||||||
|
@ -93,9 +105,9 @@ const passwordMetrics = computed<PasswordMetrics>(() => {
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!-- Score Display -->
|
<!-- Score Display -->
|
||||||
<div class="text-gray-700 text-sm">
|
<!-- <div class="text-gray-700 text-sm">
|
||||||
{{ passwordMetrics.score }} / 6 points max
|
{{ passwordMetrics.score }} / 6 points max
|
||||||
</div>
|
</div> -->
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="css" scoped>
|
<style lang="css" scoped>
|
||||||
|
|
|
@ -6,9 +6,9 @@ const props = defineProps({
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
avatar: {
|
defaultUrl: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
required: false
|
||||||
},
|
},
|
||||||
api: {
|
api: {
|
||||||
type: String,
|
type: String,
|
||||||
|
@ -16,58 +16,37 @@ const props = defineProps({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const avatar = computed(
|
const avatar = computed(() => {
|
||||||
// () => props.avatar ?? `https://avatars.dicebear.com/api/${props.api}/${props.username?.replace(/[^a-z0-9]+/i, '-')}.svg`
|
return props.defaultUrl ?? generateAvatarUrl(props.username);
|
||||||
|
});
|
||||||
// () => props.avatar ?? `https://avatars.dicebear.com/api/initials/${props.username}.svg`,
|
|
||||||
|
|
||||||
() => {
|
|
||||||
const initials = props.username
|
|
||||||
.split(' ')
|
|
||||||
.map((part) => part.charAt(0).toUpperCase())
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
return props.avatar ?? generateAvatarUrl(props.username);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
const username = computed(() => props.username);
|
const username = computed(() => props.username);
|
||||||
|
|
||||||
const darkenColor = (color) => {
|
const darkenColor = (color) => {
|
||||||
// Convert hex to RGB
|
|
||||||
const r = parseInt(color.slice(0, 2), 16);
|
const r = parseInt(color.slice(0, 2), 16);
|
||||||
const g = parseInt(color.slice(2, 4), 16);
|
const g = parseInt(color.slice(2, 4), 16);
|
||||||
const b = parseInt(color.slice(4, 6), 16);
|
const b = parseInt(color.slice(4, 6), 16);
|
||||||
|
|
||||||
// Calculate darker color by reducing 20% of each RGB component
|
|
||||||
const darkerR = Math.round(r * 0.6);
|
const darkerR = Math.round(r * 0.6);
|
||||||
const darkerG = Math.round(g * 0.6);
|
const darkerG = Math.round(g * 0.6);
|
||||||
const darkerB = Math.round(b * 0.6);
|
const darkerB = Math.round(b * 0.6);
|
||||||
|
|
||||||
// Convert back to hex
|
|
||||||
const darkerColor = ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16);
|
const darkerColor = ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16);
|
||||||
|
|
||||||
return darkerColor.padStart(6, '0'); // Ensure it's 6 digits
|
return darkerColor.padStart(6, '0');
|
||||||
};
|
};
|
||||||
|
|
||||||
const getRandomColor = () => {
|
const getColorFromName = (name) => {
|
||||||
return Math.floor(Math.random() * 16777215).toString(16);
|
let hash = 0;
|
||||||
};
|
for (let i = 0; i < name.length; i++) {
|
||||||
|
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
const adjustOpacity = (hexColor, opacity) => {
|
}
|
||||||
// Remove # if present
|
let color = '#';
|
||||||
hexColor = hexColor.replace('#', '');
|
for (let i = 0; i < 3; i++) {
|
||||||
// Convert hex to RGB
|
const value = (hash >> (i * 8)) & 0xff;
|
||||||
// const r = parseInt(hexColor.slice(0, 2), 16);
|
color += ('00' + value.toString(16)).substr(-2);
|
||||||
// const g = parseInt(hexColor.slice(2, 4), 16);
|
}
|
||||||
// const b = parseInt(hexColor.slice(4, 6), 16);
|
return color.replace('#', '');
|
||||||
|
|
||||||
// const r = parseInt(hexColor.slice(1, 3), 16);
|
|
||||||
// const g = parseInt(hexColor.slice(3, 5), 16);
|
|
||||||
// const b = parseInt(hexColor.slice(5, 7), 16);
|
|
||||||
const [r, g, b] = hexColor.match(/\w\w/g).map(x => parseInt(x, 16));
|
|
||||||
|
|
||||||
return `rgba(${r},${g},${b},${opacity})`;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const lightenColor = (hexColor, percent) => {
|
const lightenColor = (hexColor, percent) => {
|
||||||
|
@ -88,21 +67,12 @@ const lightenColor = (hexColor, percent) => {
|
||||||
return lighterHex.padStart(6, '0');
|
return lighterHex.padStart(6, '0');
|
||||||
};
|
};
|
||||||
|
|
||||||
// backgroundColor = '7F9CF5',
|
|
||||||
const generateAvatarUrl = (name) => {
|
const generateAvatarUrl = (name) => {
|
||||||
const initials = name
|
const originalColor = getColorFromName(name);
|
||||||
.split(' ')
|
const backgroundColor = lightenColor(originalColor, 60);
|
||||||
.map((part) => part.charAt(0).toUpperCase())
|
|
||||||
.join('');
|
|
||||||
|
|
||||||
const originalColor = getRandomColor();
|
|
||||||
|
|
||||||
const backgroundColor = lightenColor(originalColor, 60); // Lighten by 20%
|
|
||||||
|
|
||||||
const textColor = darkenColor(originalColor);
|
const textColor = darkenColor(originalColor);
|
||||||
|
|
||||||
// const avatarUrl = `https://ui-avatars.com/api/?name=${initials}&size=50&background=${backgroundColor}&color=${textColor}`;
|
const avatarUrl = `/api/avatar?name=${name}&size=50`;
|
||||||
const avatarUrl = `/api/avatar?name=${name}&size=50&background=${backgroundColor}&textColor=${textColor}`;
|
|
||||||
return avatarUrl;
|
return avatarUrl;
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,12 +1,29 @@
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from 'vue';
|
// import { computed } from 'vue';
|
||||||
// import { usePage } from '@inertiajs/vue3'
|
// import { usePage } from '@inertiajs/vue3';
|
||||||
import { usePage } from '@inertiajs/vue3';
|
|
||||||
import UserAvatar from '@/Components/UserAvatar.vue';
|
import UserAvatar from '@/Components/UserAvatar.vue';
|
||||||
|
|
||||||
const userName = computed(() => usePage().props.auth?.user.name);
|
defineProps({
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<UserAvatar v-bind:username="'userName'" api="initials" />
|
|
||||||
|
<div v-if="user.avatar">
|
||||||
|
<UserAvatar :default-url="user.avatar ? '/public' + user.avatar : ''"
|
||||||
|
:username="user.first_name + ' ' + user.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<UserAvatar :username="user.first_name + ' ' + user.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
<!-- <template v-else>
|
||||||
|
<UserAvatar :username="user.first_name + ' ' + user.last_name" class="w-24 h-24 mx-auto lg:w-6 lg:h-6" />
|
||||||
|
|
||||||
|
</template> -->
|
||||||
|
|
30
resources/js/Components/action-message.vue
Normal file
30
resources/js/Components/action-message.vue
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { colorsBgLight, colorsOutline } from '@/colors';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
on: Boolean,
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
outline: Boolean,
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
default: 'info',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const componentClass = computed(() => (props.outline ? colorsOutline[props.color] : colorsBgLight[props.color]));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div >
|
||||||
|
<transition leave-active-class="transition ease-in duration-1000" leave-from-class="opacity-100"
|
||||||
|
leave-to-class="opacity-0" :class="componentClass" class="px-3 py-2 last:mb-0 border rounded transition-colors duration-150 text-sm text-gray-600">
|
||||||
|
<div v-show="on">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</transition>
|
||||||
|
</div>
|
||||||
|
</template>
|
79
resources/js/Components/avatar-input.vue
Normal file
79
resources/js/Components/avatar-input.vue
Normal file
|
@ -0,0 +1,79 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative inline-block overflow-hidden rounded-full">
|
||||||
|
<input type="file" ref="avatarInput" @change="onChangeFile" class="hidden" accept="image/*">
|
||||||
|
|
||||||
|
<img :src="avatarUrl" alt="Avatar" class="h-full w-full object-cover">
|
||||||
|
<div class="absolute top-0 h-full w-full bg-black bg-opacity-25 flex items-center justify-center">
|
||||||
|
<button @click.prevent="browse"
|
||||||
|
class="rounded-full hover:bg-white hover:bg-opacity-25 p-2 focus:outline-none text-white transition-colors duration-300">
|
||||||
|
<IconRounded :icon="mdiCameraEnhanceOutline" class="bg-transparent h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
<button v-if="file" @click.prevent="reset"
|
||||||
|
class="rounded-full hover:bg-white hover:bg-opacity-25 p-2 focus:outline-none text-white transition-colors duration-300">
|
||||||
|
<IconRounded :icon="mdiAlphaXCircleOutline " class="bg-transparent h-6 w-6" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, defineProps, defineEmits } from 'vue';
|
||||||
|
import { mdiCameraEnhanceOutline, mdiAlphaXCircleOutline } from '@mdi/js';
|
||||||
|
import IconRounded from './IconRounded.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
|
||||||
|
modelValue: File,
|
||||||
|
defaultSrc: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// vue data properties
|
||||||
|
const file = ref<File | null>(null);
|
||||||
|
const avatarUrl = ref<string>(props.defaultSrc);
|
||||||
|
const avatarInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
|
||||||
|
// const avatarUrl = computed({
|
||||||
|
// get: () => props.modelValue ? props.modelValue : props.defaultSrc,
|
||||||
|
// set: (value: string) => {
|
||||||
|
// emit('update:modelValue', value);
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'update:modelValue', file: File | null): void; (e: 'input', file: File | null): void }>();
|
||||||
|
|
||||||
|
// const avatarInput = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const browse = () => {
|
||||||
|
avatarInput.value?.click();
|
||||||
|
};
|
||||||
|
|
||||||
|
const reset = () => {
|
||||||
|
file.value = null;
|
||||||
|
avatarUrl.value = props.defaultSrc;
|
||||||
|
emit('input', file.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeFile = (e: Event) => {
|
||||||
|
// const target = (<HTMLInputElement>e.target)
|
||||||
|
const target = e.target as HTMLInputElement;
|
||||||
|
if (target.files && target.files[0]) {
|
||||||
|
file.value = target.files[0];
|
||||||
|
}
|
||||||
|
if (file.value) {
|
||||||
|
emit('input', file.value);
|
||||||
|
emit('update:modelValue', file.value);
|
||||||
|
let reader = new FileReader();
|
||||||
|
reader.readAsDataURL(file.value);
|
||||||
|
reader.onload = (e) => {
|
||||||
|
avatarUrl.value = e.target?.result as string;
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -1,43 +1,42 @@
|
||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ref } from 'vue'
|
import { computed, ref } from 'vue';
|
||||||
import { MainService } from '@/Stores/main'
|
import { MainService } from '@/Stores/main';
|
||||||
import { mdiCheckDecagram } from '@mdi/js'
|
import { mdiCheckDecagram } from '@mdi/js';
|
||||||
import BaseLevel from '@/Components/BaseLevel.vue'
|
import BaseLevel from '@/Components/BaseLevel.vue';
|
||||||
import UserAvatarCurrentUser from '@/Components/UserAvatarCurrentUser.vue'
|
import UserAvatarCurrentUser from '@/Components/UserAvatarCurrentUser.vue';
|
||||||
import CardBox from '@/Components/CardBox.vue'
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue'
|
import FormCheckRadioGroup from '@/Components/FormCheckRadioGroup.vue';
|
||||||
import PillTag from '@/Components/PillTag.vue'
|
import PillTag from '@/Components/PillTag.vue';
|
||||||
|
import { usePage } from '@inertiajs/vue3';
|
||||||
|
import type { User } from '@/Dataset';
|
||||||
|
import type { ComputedRef } from 'vue';
|
||||||
|
|
||||||
const mainService = MainService()
|
const mainService = MainService();
|
||||||
|
|
||||||
const userName = computed(() => mainService.userName)
|
const userName = computed(() => mainService.userName);
|
||||||
|
|
||||||
const userSwitchVal = ref([])
|
const user: ComputedRef<User> = computed(() => {
|
||||||
|
return usePage().props.authUser as User;
|
||||||
|
});
|
||||||
|
|
||||||
|
const userSwitchVal = ref([]);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CardBox>
|
<CardBox>
|
||||||
<BaseLevel type="justify-around lg:justify-center">
|
<BaseLevel type="justify-around lg:justify-center">
|
||||||
<UserAvatarCurrentUser class="lg:mx-12" />
|
<UserAvatarCurrentUser :user="user" class="lg:mx-12" />
|
||||||
<div class="space-y-3 text-center md:text-left lg:mx-12">
|
<div class="space-y-3 text-center md:text-left lg:mx-12">
|
||||||
<div class="flex justify-center md:block">
|
<div class="flex justify-center md:block">
|
||||||
<FormCheckRadioGroup
|
<FormCheckRadioGroup v-model="userSwitchVal" name="sample-switch" type="switch"
|
||||||
v-model="userSwitchVal"
|
:options="{ one: 'Notifications' }" />
|
||||||
name="sample-switch"
|
|
||||||
type="switch"
|
|
||||||
:options="{ one: 'Notifications' }"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<h1 class="text-2xl">
|
<h1 class="text-2xl">
|
||||||
Howdy, <b>{{ userName }}</b>!
|
Howdy, <b>{{ userName }}</b>!
|
||||||
</h1>
|
</h1>
|
||||||
<p>Last login <b>12 mins ago</b> from <b>127.0.0.1</b></p>
|
<p>Last login <b>12 mins ago</b> from <b>127.0.0.1</b></p>
|
||||||
<div class="flex justify-center md:block">
|
<div class="flex justify-center md:block">
|
||||||
<PillTag
|
<PillTag text="Verified" type="info" :icon="mdiCheckDecagram" />
|
||||||
text="Verified"
|
|
||||||
type="info"
|
|
||||||
:icon="mdiCheckDecagram"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BaseLevel>
|
</BaseLevel>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { Head, usePage } from '@inertiajs/vue3';
|
import { usePage } from '@inertiajs/vue3';
|
||||||
import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline } from '@mdi/js';
|
import { mdiAccountKey, mdiSquareEditOutline, mdiAlertBoxOutline } from '@mdi/js';
|
||||||
import { computed, ComputedRef } from 'vue';
|
import { computed, ComputedRef } from 'vue';
|
||||||
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||||
|
|
|
@ -36,16 +36,16 @@ import PasswordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
|
||||||
|
|
||||||
const emit = defineEmits(['confirm', 'update:confirmation'])
|
const emit = defineEmits(['confirm', 'update:confirmation'])
|
||||||
|
|
||||||
const enabled = ref(false);
|
// const enabled = ref(false);
|
||||||
const handleScore = (score: number) => {
|
// const handleScore = (score: number) => {
|
||||||
if (score >= 4){
|
// if (score >= 4) {
|
||||||
enabled.value = true;
|
// enabled.value = true;
|
||||||
} else {
|
// } else {
|
||||||
enabled.value = false;
|
// enabled.value = false;
|
||||||
}
|
// }
|
||||||
// strengthLabel.value = scoreLabel;
|
// // strengthLabel.value = scoreLabel;
|
||||||
// score.value = scoreValue;
|
// // score.value = scoreValue;
|
||||||
};
|
// };
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
// user will be returned from controller action
|
// user will be returned from controller action
|
||||||
|
@ -82,20 +82,20 @@ defineProps({
|
||||||
// };
|
// };
|
||||||
|
|
||||||
|
|
||||||
const passwordForm = useForm({
|
// const passwordForm = useForm({
|
||||||
old_password: '',
|
// old_password: '',
|
||||||
new_password: '',
|
// new_password: '',
|
||||||
confirm_password: '',
|
// confirm_password: '',
|
||||||
});
|
// });
|
||||||
const passwordSubmit = async () => {
|
// const passwordSubmit = async () => {
|
||||||
await passwordForm.post(stardust.route('account.password.store'), {
|
// await passwordForm.post(stardust.route('account.password.store'), {
|
||||||
preserveScroll: true,
|
// preserveScroll: true,
|
||||||
onSuccess: () => {
|
// onSuccess: () => {
|
||||||
// console.log(resp);
|
// // console.log(resp);
|
||||||
passwordForm.reset();
|
// passwordForm.reset();
|
||||||
},
|
// },
|
||||||
});
|
// });
|
||||||
};
|
// };
|
||||||
|
|
||||||
const flash: Ref<any> = computed(() => {
|
const flash: Ref<any> = computed(() => {
|
||||||
return usePage().props.flash;
|
return usePage().props.flash;
|
||||||
|
@ -139,40 +139,10 @@ const flash: Ref<any> = computed(() => {
|
||||||
{{ $page.props.flash.message }}
|
{{ $page.props.flash.message }}
|
||||||
</NotificationBar> -->
|
</NotificationBar> -->
|
||||||
|
|
||||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
<div class="grid grid-cols-1 gap-6">
|
||||||
<!-- <div class="grid grid-cols-1 lg:grid-cols-1 gap-6"> -->
|
|
||||||
|
|
||||||
<!-- password form -->
|
<!-- <CardBox id="passwordForm" title="Change Password" :icon="mdiLock" form
|
||||||
<!-- <CardBox title="Edit Profile" :icon="mdiAccountCircle" form @submit.prevent="profileForm.post(route('admin.account.info.store'))"> -->
|
@submit.prevent="passwordSubmit()">
|
||||||
<!-- <CardBox title="Edit Profile" :icon="mdiAccountCircle" form @submit.prevent="profileSubmit()">
|
|
||||||
<FormField label="Login" help="Required. Your login name" :class="{ 'text-red-400': errors.login }">
|
|
||||||
<FormControl v-model="profileForm.login" v-bind:icon="mdiAccount" name="login" required :error="errors.login">
|
|
||||||
<div class="text-red-400 text-sm" v-if="errors.login">
|
|
||||||
{{ errors.login }}
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
</FormField>
|
|
||||||
<FormField label="Email" help="Required. Your e-mail" :class="{ 'text-red-400': errors.email }">
|
|
||||||
<FormControl v-model="profileForm.email" :icon="mdiMail" type="email" name="email" required :error="errors.email">
|
|
||||||
<div class="text-red-400 text-sm" v-if="errors.email">
|
|
||||||
{{ errors.email }}
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
</FormField>
|
|
||||||
|
|
||||||
<template #footer>
|
|
||||||
<BaseButtons>
|
|
||||||
<BaseButton color="info" type="submit" label="Submit" />
|
|
||||||
</BaseButtons>
|
|
||||||
</template>
|
|
||||||
</CardBox> -->
|
|
||||||
|
|
||||||
<!-- password form -->
|
|
||||||
<!-- <CardBox title="Change Password" :icon="mdiLock" form @submit.prevent="passwordForm.post(route('admin.account.password.store'), {
|
|
||||||
preserveScroll: true,
|
|
||||||
onSuccess: () => passwordForm.reset(),
|
|
||||||
}) "> -->
|
|
||||||
<CardBox id="passwordForm" title="Change Password" :icon="mdiLock" form @submit.prevent="passwordSubmit()">
|
|
||||||
<FormValidationErrors v-bind:errors="errors" />
|
<FormValidationErrors v-bind:errors="errors" />
|
||||||
|
|
||||||
<FormField label="Current password" help="Required. Your current password"
|
<FormField label="Current password" help="Required. Your current password"
|
||||||
|
@ -186,22 +156,15 @@ const flash: Ref<any> = computed(() => {
|
||||||
</FormField>
|
</FormField>
|
||||||
<BaseDivider />
|
<BaseDivider />
|
||||||
|
|
||||||
<!-- <FormField label="New password" help="Required. New password"
|
<PasswordMeter v-model="passwordForm.new_password" :errors="passwordForm.errors"
|
||||||
:class="{ 'text-red-400': passwordForm.errors.new_password }">
|
@score="handleScore" />
|
||||||
<FormControl v-model="passwordForm.new_password" :icon="mdiFormTextboxPassword" name="new_password"
|
|
||||||
type="password" required :error="passwordForm.errors.new_password">
|
|
||||||
<div class="text-red-400 text-sm" v-if="passwordForm.errors.new_password">
|
|
||||||
{{ passwordForm.errors.new_password }}
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
|
||||||
</FormField> -->
|
|
||||||
<PasswordMeter v-model:password="passwordForm.new_password" :errors="passwordForm.errors" @score="handleScore" />
|
|
||||||
|
|
||||||
|
|
||||||
<FormField label="Confirm password" help="Required. New password one more time"
|
<FormField label="Confirm password" help="Required. New password one more time"
|
||||||
:class="{ 'text-red-400': passwordForm.errors.confirm_password }">
|
:class="{ 'text-red-400': passwordForm.errors.confirm_password }">
|
||||||
<FormControl v-model="passwordForm.confirm_password" :icon="mdiFormTextboxPassword"
|
<FormControl v-model="passwordForm.confirm_password" :icon="mdiFormTextboxPassword"
|
||||||
name="confirm_password" type="password" required :error="passwordForm.errors.confirm_password">
|
name="confirm_password" type="password" required
|
||||||
|
:error="passwordForm.errors.confirm_password">
|
||||||
<div class="text-red-400 text-sm" v-if="passwordForm.errors.confirm_password">
|
<div class="text-red-400 text-sm" v-if="passwordForm.errors.confirm_password">
|
||||||
{{ passwordForm.errors.confirm_password }}
|
{{ passwordForm.errors.confirm_password }}
|
||||||
</div>
|
</div>
|
||||||
|
@ -219,16 +182,17 @@ const flash: Ref<any> = computed(() => {
|
||||||
|
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type="submit" color="info" label="Change password" :disabled="passwordForm.processing == true || enabled == false" />
|
<BaseButton type="submit" color="info" label="Change password"
|
||||||
|
:disabled="passwordForm.processing == true || enabled == false" />
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
</template>
|
</template>
|
||||||
</CardBox>
|
</CardBox> -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<PersonalTotpSettings :twoFactorEnabled="twoFactorEnabled" :backupState="backupState">
|
<PersonalTotpSettings :twoFactorEnabled="twoFactorEnabled" :backupState="backupState">
|
||||||
</PersonalTotpSettings>
|
</PersonalTotpSettings>
|
||||||
<!-- <PersonalSettings :state="backupState"/> -->
|
<!-- <PersonalSettings :state="backupState"/> -->
|
||||||
|
|
||||||
<!-- <CardBox v-if="!props.twoFactorEnabled" title="Two-Factor Authentication" :icon="mdiInformation" form
|
<!-- <CardBox v-if="!props.twoFactorEnabled" title="Two-Factor Authentication" :icon="mdiInformation" form
|
||||||
@submit.prevent="enableTwoFactorAuthentication()">
|
@submit.prevent="enableTwoFactorAuthentication()">
|
||||||
|
|
|
@ -16,6 +16,7 @@ import {
|
||||||
// import { containerMaxW } from '@/config.js'; // "xl:max-w-6xl xl:mx-auto"
|
// import { containerMaxW } from '@/config.js'; // "xl:max-w-6xl xl:mx-auto"
|
||||||
// import * as chartConfig from '@/Components/Charts/chart.config.js';
|
// import * as chartConfig from '@/Components/Charts/chart.config.js';
|
||||||
import LineChart from '@/Components/Charts/LineChart.vue';
|
import LineChart from '@/Components/Charts/LineChart.vue';
|
||||||
|
import UserCard from '@/Components/unused/UserCard.vue';
|
||||||
import SectionMain from '@/Components/SectionMain.vue';
|
import SectionMain from '@/Components/SectionMain.vue';
|
||||||
import CardBoxWidget from '@/Components/CardBoxWidget.vue';
|
import CardBoxWidget from '@/Components/CardBoxWidget.vue';
|
||||||
import CardBox from '@/Components/CardBox.vue';
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
|
@ -134,6 +135,8 @@ const datasets = computed(() => mainService.datasets);
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<UserCard />
|
||||||
|
|
||||||
<SectionBannerStarOnGitHub />
|
<SectionBannerStarOnGitHub />
|
||||||
|
|
||||||
<SectionTitleLineWithButton :icon="mdiChartPie" title="Trends overview: Publications per month" />
|
<SectionTitleLineWithButton :icon="mdiChartPie" title="Trends overview: Publications per month" />
|
||||||
|
|
135
resources/js/Pages/profile/partials/update-password-form.vue
Normal file
135
resources/js/Pages/profile/partials/update-password-form.vue
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import FormControl from '@/Components/FormControl.vue';
|
||||||
|
import FormField from '@/Components/FormField.vue';
|
||||||
|
import ActionMessage from '@/Components/action-message.vue'
|
||||||
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
|
import BaseButtons from '@/Components/BaseButtons.vue';
|
||||||
|
import { useForm, usePage } from '@inertiajs/vue3';
|
||||||
|
import { ref, Ref, computed } from 'vue';
|
||||||
|
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||||
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
|
import { mdiLock } from '@mdi/js';
|
||||||
|
import PasswordMeter from '@/Components/SimplePasswordMeter/password-meter.vue';
|
||||||
|
// import BaseDivider from '@/Components/BaseDivider.vue';
|
||||||
|
|
||||||
|
// const errors: Ref<any> = computed(() => {
|
||||||
|
// return usePage().props.errors;
|
||||||
|
// });
|
||||||
|
const flash: Ref<any> = computed(() => {
|
||||||
|
return usePage().props.flash;
|
||||||
|
});
|
||||||
|
|
||||||
|
const newPasswordInput: Ref<typeof FormControl | null> = ref(null);
|
||||||
|
const oldPasswordInput: Ref<typeof FormControl | null> = ref(null);
|
||||||
|
|
||||||
|
const enabled = ref(false);
|
||||||
|
const handleScore = (score: number) => {
|
||||||
|
if (score >= 4) {
|
||||||
|
enabled.value = true;
|
||||||
|
} else {
|
||||||
|
enabled.value = false;
|
||||||
|
}
|
||||||
|
// strengthLabel.value = scoreLabel;
|
||||||
|
// score.value = scoreValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
old_password: '',
|
||||||
|
new_password: '',
|
||||||
|
confirm_password: '',
|
||||||
|
});
|
||||||
|
const updatePassword = async () => {
|
||||||
|
await form.put(stardust.route('settings.password.update'), {
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => {
|
||||||
|
form.reset();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
if (form.errors.new_password) {
|
||||||
|
form.reset('new_password', 'confirm_password');
|
||||||
|
enabled.value = false;
|
||||||
|
// newPasswordInput.value.focus();
|
||||||
|
// newPasswordInput.value?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (form.errors.old_password) {
|
||||||
|
form.reset('old_password');
|
||||||
|
// oldPasswordInput.value?.focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<!-- <div class="p-7 text-gray-900 bg-white rounded-lg border border-gray-100 shadow dark:border-gray-600 dark:bg-secondary-dark dark:text-white"> -->
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <div class="mb-4">
|
||||||
|
<h3 class="text-dark text-md">{{ 'Update Password' }}</h3>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<CardBox id="passwordForm" title="Change Password" :icon="mdiLock" form :show-header-icon="false">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<FormField label="Current password" help="Required. Your current password"
|
||||||
|
:class="{ 'text-red-400': form.errors.old_password }">
|
||||||
|
<FormControl label="Current Password" id="current_password" ref="oldPasswordInput"
|
||||||
|
:placeholder="'Please Enter Current Password'" v-model="form.old_password"
|
||||||
|
:error="form.errors.old_password" type="password" class="block w-full" autocomplete="current-password">
|
||||||
|
<div class="text-red-400 text-sm" v-if="form.errors.old_password">
|
||||||
|
{{ form.errors.old_password }}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- <div class="col-span-6 sm:col-span-4"> -->
|
||||||
|
<!-- <FormControl label="New Password" id="password" :placeholder="'Please Enter New Password'"
|
||||||
|
ref="newPasswordInput" v-model="form.new_password" type="password" class="block w-full"
|
||||||
|
autocomplete="new-password" :error="form.errors.new_password">
|
||||||
|
<div class="text-red-400 text-sm" v-if="form.errors.new_password">
|
||||||
|
{{ form.errors.new_password }}
|
||||||
|
</div>
|
||||||
|
</FormControl> -->
|
||||||
|
<PasswordMeter ref="newPasswordInput" v-model="form.new_password" :errors="form.errors"
|
||||||
|
@score="handleScore" />
|
||||||
|
|
||||||
|
<!-- </div> -->
|
||||||
|
|
||||||
|
<FormField label="Confirm password" help="Required. New password one more time"
|
||||||
|
:class="{ 'text-red-400': form.errors.confirm_password }">
|
||||||
|
<FormControl label="Confirm Password" :placeholder="'Please Enter Confirm Password'" id="confirm_password"
|
||||||
|
v-model="form.confirm_password" type="password" class="block w-full" autocomplete="new-password"
|
||||||
|
:error="form.errors.confirm_password">
|
||||||
|
<div class="text-red-400 text-sm" v-if="form.errors.confirm_password">
|
||||||
|
{{ form.errors.confirm_password }}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<!-- <BaseDivider /> -->
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<div class="flex items-center justify-end gap-3 mt-5">
|
||||||
|
<ActionMessage v-if="flash.message" :on="form.recentlySuccessful" color="success">
|
||||||
|
{{ flash.message }}
|
||||||
|
</ActionMessage>
|
||||||
|
<ActionMessage v-if="flash.warning" :on="form.recentlySuccessful" color="warning">
|
||||||
|
{{ flash.warning }}
|
||||||
|
</ActionMessage>
|
||||||
|
<BaseButtons>
|
||||||
|
<BaseButton type="submit" color="info" label="Change password" @click.prevent="updatePassword()"
|
||||||
|
:disabled="form.processing == true || enabled == false" />
|
||||||
|
</BaseButtons>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CardBox>
|
||||||
|
</template>
|
|
@ -0,0 +1,179 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, Ref, ref } from 'vue';
|
||||||
|
import { useForm, usePage } from '@inertiajs/vue3';
|
||||||
|
import ActionMessage from '@/Components/action-message.vue'
|
||||||
|
import FormControl from '@/Components/FormControl.vue';
|
||||||
|
import FormField from '@/Components/FormField.vue';
|
||||||
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
|
import { mdiAccount } from '@mdi/js';
|
||||||
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
|
import BaseButtons from '@/Components/BaseButtons.vue'
|
||||||
|
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||||
|
import AvatarInput from '@/Components/avatar-input.vue';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
defaultUrl: {
|
||||||
|
type: String,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const errors: Ref<any> = computed(() => {
|
||||||
|
return usePage().props.errors || {}
|
||||||
|
});
|
||||||
|
const flash: Ref<any> = computed(() => {
|
||||||
|
return usePage().props.flash;
|
||||||
|
});
|
||||||
|
|
||||||
|
const fullName = computed(() => `${props.user.first_name} ${props.user.last_name}`);
|
||||||
|
const recentlyHasError = ref(false);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
first_name: props.user.first_name,
|
||||||
|
last_name: props.user.last_name,
|
||||||
|
login: props.user.login,
|
||||||
|
email: props.user.email,
|
||||||
|
// mobile: `${props.user.mobile}`,
|
||||||
|
avatar: undefined as File | undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
// const verificationLinkSent = ref(null);
|
||||||
|
const avatarInput: Ref<HTMLInputElement | null> = ref(null);
|
||||||
|
|
||||||
|
const updateProfileInformation = () => {
|
||||||
|
// if (avatarInput.value) {
|
||||||
|
// form.avatar = avatarInput.value?.files ? avatarInput.value.files[0] : undefined;
|
||||||
|
// }
|
||||||
|
|
||||||
|
form.put(stardust.route('settings.profile.update', [props.user.id]), {
|
||||||
|
errorBag: 'updateProfileInformation',
|
||||||
|
preserveScroll: true,
|
||||||
|
onSuccess: () => {
|
||||||
|
// clearPhotoFileInput();
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
if (form.errors.avatar) {
|
||||||
|
if (avatarInput.value) {
|
||||||
|
avatarInput.value.value = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
recentlyHasError.value = true
|
||||||
|
setTimeout(() => {
|
||||||
|
recentlyHasError.value = false
|
||||||
|
}, 5000)
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
// const sendEmailVerification = () => {
|
||||||
|
// verificationLinkSent.value = true;
|
||||||
|
// };
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CardBox id="passwordForm" title="Basic Info" :icon="mdiAccount" form :show-header-icon="false">
|
||||||
|
<!-- <FormValidationErrors v-bind:errors="errors" /> -->
|
||||||
|
|
||||||
|
<AvatarInput class="h-24 w-24 rounded-full" v-model="form.avatar" ref="avatarInput"
|
||||||
|
:default-src="defaultUrl ? defaultUrl : '/api/avatar?name=' + fullName + '&size=50'">
|
||||||
|
</AvatarInput>
|
||||||
|
<div class="text-red-400 text-sm" v-if="errors.avatar && Array.isArray(errors.avatar)">
|
||||||
|
{{ errors.avatar.join(', ') }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField label="First Name" :class="{ 'text-red-400': form.errors.first_name }">
|
||||||
|
<FormControl id="first_name" label="First Name" v-model="form.first_name" :error="form.errors.first_name"
|
||||||
|
type="text" :placeholder="'First Name'" class="w-full" autocomplete="first_name">
|
||||||
|
<div class="text-red-400 text-sm" v-if="errors.first_name && Array.isArray(errors.first_name)">
|
||||||
|
{{ errors.first_name.join(', ') }}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Last Name" :class="{ 'text-red-400': form.errors.last_name }">
|
||||||
|
<FormControl id="last_name" label="Last Name" v-model="form.last_name" :error="form.errors.last_name"
|
||||||
|
type="text" :placeholder="'Last Name'" class="w-full" autocomplete="last_name">
|
||||||
|
<div class="text-red-400 text-sm" v-if="errors.last_name && Array.isArray(errors.last_name)">
|
||||||
|
{{ errors.last_name.join(', ') }}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Username" :class="{ 'text-red-400': form.errors.login }">
|
||||||
|
<FormControl id="username" label="Username" v-model="form.login" class="w-full"
|
||||||
|
:is-read-only="!user.is_admin">
|
||||||
|
<div class="text-red-400 text-sm" v-if="errors.login && Array.isArray(errors.login)">
|
||||||
|
{{ errors.login.join(', ') }}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<FormField label="Enter Email">
|
||||||
|
<FormControl v-model="form.email" type="text" placeholder="Email" :errors="form.errors.email"
|
||||||
|
:is-read-only="!user.is_admin">
|
||||||
|
<div class="text-red-400 text-sm" v-if="errors.email && Array.isArray(errors.email)">
|
||||||
|
{{ errors.email.join(', ') }}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
<!-- Email -->
|
||||||
|
<!-- <div>
|
||||||
|
<FormControl label="Email" id="email" v-model="form.email" :readonly="!user.is_super_admin"
|
||||||
|
:disabled="!user.is_super_admin" class="w-full" />
|
||||||
|
|
||||||
|
<div v-if="user.email_verified_at === null">
|
||||||
|
<p class="text-sm mt-2">
|
||||||
|
{{ 'Your email address is unverified.' }}
|
||||||
|
|
||||||
|
<Link :href="route('verification.send')" method="post" as="button"
|
||||||
|
class="underline text-gray-600 hover:text-gray-900" @click.prevent="sendEmailVerification">
|
||||||
|
{{ 'Click here to re-send the verification email.' }}
|
||||||
|
</Link>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div v-show="verificationLinkSent" class="mt-2 font-medium text-sm text-green-600">
|
||||||
|
{{ 'A new verification link has been sent to your email address.' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- <div class="relative">
|
||||||
|
<FormControl label="Mobile" id="mobile" class="w-full" input-class="w-full pl-5" v-model="form.mobile"
|
||||||
|
:readonly="!user.is_super_admin" :disabled="!user.is_super_admin" />
|
||||||
|
<span class="absolute top-9 left-0 inline-flex items-center ml-2 font-bold text-secondary-light">+</span>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<!-- <div class="col-span-2 flex justify-end items-center mt-5"> -->
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex items-center justify-end gap-3 ">
|
||||||
|
<ActionMessage :on="recentlyHasError" color="warning">
|
||||||
|
<ul class="list-disc list-inside space-y-2 text-sm">
|
||||||
|
<li v-for="(messages, field) in errors" :key="field" class="flex flex-col">
|
||||||
|
<span class="font-semibold capitalize text-gray-700 dark:text-gray-300">{{ field }}:</span>
|
||||||
|
<span class="text-red-600 dark:text-red-400 ml-4">{{ messages.join(', ') }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ActionMessage>
|
||||||
|
<ActionMessage v-if="flash.message" :on="form.recentlySuccessful" color="success">
|
||||||
|
{{ flash.message }}
|
||||||
|
</ActionMessage>
|
||||||
|
<ActionMessage v-if="flash.warning" :on="form.recentlySuccessful" color="warning">
|
||||||
|
{{ flash.warning }}
|
||||||
|
</ActionMessage>
|
||||||
|
<BaseButtons>
|
||||||
|
<BaseButton type="submit" color="info" label="Save Changes"
|
||||||
|
@click.prevent="updateProfileInformation" :disabled="form.processing" />
|
||||||
|
</BaseButtons>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</CardBox>
|
||||||
|
|
||||||
|
<!-- </div>
|
||||||
|
</div> -->
|
||||||
|
</template>
|
139
resources/js/Pages/profile/show.vue
Normal file
139
resources/js/Pages/profile/show.vue
Normal file
|
@ -0,0 +1,139 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
|
||||||
|
/*=========================================================================================
|
||||||
|
File Name: show profile
|
||||||
|
|
||||||
|
----------------------------------------------------------------------------------------
|
||||||
|
Author: Arno Kaimbacher
|
||||||
|
Author URL: https://jakint.at/
|
||||||
|
==========================================================================================*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// import { useUrlSearchParams } from '@vueuse/core';
|
||||||
|
// import { usePage } from '@inertiajs/vue3';
|
||||||
|
import { TabGroup, TabList, Tab, TabPanels, TabPanel } from '@headlessui/vue';
|
||||||
|
import { ref, computed } from 'vue';
|
||||||
|
// import IconRounded from '@/Components/IconRounded.vue';
|
||||||
|
|
||||||
|
|
||||||
|
// import AdminLayout from '@/Layouts/AdminLayout.vue';
|
||||||
|
// import AppLayout from '@/Layouts/AppLayout.vue';
|
||||||
|
import LayoutAuthenticated from '@/Layouts/LayoutAuthenticated.vue';
|
||||||
|
|
||||||
|
import UpdatePasswordForm from '@/Pages/profile/partials/update-password-form.vue';
|
||||||
|
import UpdateProfileInformationForm from '@/Pages/profile/partials/update-profile-information-form.vue';
|
||||||
|
import { usePage } from '@inertiajs/vue3';
|
||||||
|
import SectionMain from '@/Components/SectionMain.vue';
|
||||||
|
import SectionTitleLineWithButton from '@/Components/SectionTitleLineWithButton.vue';
|
||||||
|
import { mdiAccount, mdiArrowLeftBoldOutline, mdiFormTextboxPassword } from '@mdi/js';
|
||||||
|
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||||
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
|
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
user: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
defaultUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{ id: 1, title: 'My Profile', icon: mdiAccount },
|
||||||
|
{ id: 2, title: 'Change Password', icon: mdiFormTextboxPassword },
|
||||||
|
];
|
||||||
|
|
||||||
|
// const params = useUrlSearchParams('history');
|
||||||
|
const selectedTab = ref(0);
|
||||||
|
|
||||||
|
function changeTab(index: number) {
|
||||||
|
selectedTab.value = index;
|
||||||
|
}
|
||||||
|
|
||||||
|
const user = computed(() => usePage().props.user);
|
||||||
|
// const sessions = computed(() => usePage().props.sessions);
|
||||||
|
// const Layout = computed(() => (user.value.is_super_admin ? AdminLayout : AppLayout));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Component :is="LayoutAuthenticated">
|
||||||
|
|
||||||
|
<Head title="Profile"></Head>
|
||||||
|
<SectionMain>
|
||||||
|
<SectionTitleLineWithButton :icon="mdiAccount" title="Profile" main>
|
||||||
|
<BaseButton :route-name="stardust.route('dashboard')" :icon="mdiArrowLeftBoldOutline" label="Back"
|
||||||
|
color="white" rounded-full small />
|
||||||
|
</SectionTitleLineWithButton>
|
||||||
|
|
||||||
|
<!-- <NotificationBar v-if="flash.message" color="success" :icon="mdiAlertBoxOutline">
|
||||||
|
{{ flash.message }}
|
||||||
|
</NotificationBar> -->
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<TabGroup :selectedIndex="selectedTab" @change="changeTab">
|
||||||
|
<TabList class="flex space-x-7 p-1">
|
||||||
|
<Tab v-for="(tab, index) in tabs" as="template" :key="index" v-slot="{ selected }">
|
||||||
|
<button :class="[
|
||||||
|
'inline-flex items-center justify-center font-semibold focus:outline-none disabled:opacity-25 pb-2',
|
||||||
|
selected
|
||||||
|
? 'text-dark border-b-2 border-primary transition-all duration-500 inline-block'
|
||||||
|
: 'text-light',
|
||||||
|
]">
|
||||||
|
<!-- <IconRounded :name="tab.icon" class="h-5 mr-3" /> -->
|
||||||
|
<BaseIcon :path="tab.icon" class="flex-none" w="w-12" />
|
||||||
|
{{ tab.title }}
|
||||||
|
</button>
|
||||||
|
</Tab>
|
||||||
|
</TabList>
|
||||||
|
<transition v-show="selectedTab >= 0" enter-active-class="transition duration-100 ease-out"
|
||||||
|
enter-from-class="transform scale-95 opacity-0" enter-to-class="transform scale-100 opacity-100"
|
||||||
|
leave-active-class="transition duration-75 ease-in"
|
||||||
|
leave-from-class="transform scale-100 opacity-100"
|
||||||
|
leave-to-class="transform scale-95 opacity-0">
|
||||||
|
<TabPanels class="mt-2">
|
||||||
|
<TabPanel :key="Date.now().toString() + 1" :class="[]">
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<UpdateProfileInformationForm class="p-5" :user="user" :default-url="defaultUrl" />
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
<TabPanel :key="Date.now().toString() + 3" :class="[]">
|
||||||
|
<!-- <div class="grid grid-cols-1 md:grid-cols-2 gap-x-5 gap-y-7 items-start"> -->
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||||
|
<UpdatePasswordForm class="p-5" />
|
||||||
|
</div>
|
||||||
|
</TabPanel>
|
||||||
|
|
||||||
|
</TabPanels>
|
||||||
|
</transition>
|
||||||
|
</TabGroup>
|
||||||
|
</div>
|
||||||
|
</SectionMain>
|
||||||
|
</Component>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.cool-link::after {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
width: 0;
|
||||||
|
height: 2px;
|
||||||
|
background: #fda92d;
|
||||||
|
transition: width 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cool-link:hover::after {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<!-- <script>
|
||||||
|
import { defineComponent } from 'vue';
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
layout: false,
|
||||||
|
});
|
||||||
|
</script> -->
|
|
@ -2,7 +2,7 @@ import '../css/app.css';
|
||||||
import { createApp, h } from 'vue';
|
import { createApp, h } from 'vue';
|
||||||
import { Inertia } from '@inertiajs/inertia';
|
import { Inertia } from '@inertiajs/inertia';
|
||||||
|
|
||||||
import { createInertiaApp } from '@inertiajs/vue3';
|
import { Head, Link, createInertiaApp } from '@inertiajs/vue3';
|
||||||
// import DefaultLayout from '@/Layouts/Default.vue';
|
// import DefaultLayout from '@/Layouts/Default.vue';
|
||||||
import { createPinia } from 'pinia';
|
import { createPinia } from 'pinia';
|
||||||
import { StyleService } from '@/Stores/style.service';
|
import { StyleService } from '@/Stores/style.service';
|
||||||
|
@ -36,6 +36,7 @@ createInertiaApp({
|
||||||
progress: {
|
progress: {
|
||||||
// color: '#4B5563',
|
// color: '#4B5563',
|
||||||
color: '#22C55E',
|
color: '#22C55E',
|
||||||
|
showSpinner: true,
|
||||||
},
|
},
|
||||||
// Webpack
|
// Webpack
|
||||||
// resolve: async (name: string) => {
|
// resolve: async (name: string) => {
|
||||||
|
@ -59,6 +60,9 @@ createInertiaApp({
|
||||||
.use(EmitterPlugin);
|
.use(EmitterPlugin);
|
||||||
// .component('inertia-link', Link)
|
// .component('inertia-link', Link)
|
||||||
|
|
||||||
|
app.component('Head', Head);
|
||||||
|
app.component('Link', Link);
|
||||||
|
|
||||||
// Listen for navigation event to handle layout changes
|
// Listen for navigation event to handle layout changes
|
||||||
// window.addEventListener('inertia:navigate', () => {
|
// window.addEventListener('inertia:navigate', () => {
|
||||||
// layoutService.isAsideMobileExpanded = false;
|
// layoutService.isAsideMobileExpanded = false;
|
||||||
|
|
|
@ -27,6 +27,11 @@ export default [
|
||||||
icon: mdiLock,
|
icon: mdiLock,
|
||||||
label: 'Security',
|
label: 'Security',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
route: 'settings.profile.edit',
|
||||||
|
icon: mdiLock,
|
||||||
|
label: 'Profile',
|
||||||
|
},
|
||||||
// {
|
// {
|
||||||
// route: 'dataset.create',
|
// route: 'dataset.create',
|
||||||
// icon: mdiPublish,
|
// icon: mdiPublish,
|
||||||
|
@ -152,9 +157,9 @@ export default [
|
||||||
// label: 'Create Dataset',
|
// label: 'Create Dataset',
|
||||||
// },
|
// },
|
||||||
{
|
{
|
||||||
href: 'https://gitea.geologie.ac.at/geolba/tethys',
|
href: 'https://gitea.geosphere.at/geolba/tethys.backend',
|
||||||
icon: mdiGithub,
|
icon: mdiGithub,
|
||||||
label: 'Gitea',
|
label: 'Forgejo',
|
||||||
target: '_blank',
|
target: '_blank',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -25,6 +25,7 @@
|
||||||
@vite(['resources/js/app.ts'])
|
@vite(['resources/js/app.ts'])
|
||||||
|
|
||||||
@routes('test')
|
@routes('test')
|
||||||
|
@inertiaHead
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -244,6 +244,20 @@ router
|
||||||
|
|
||||||
router.get('/settings/user/security', [UserController, 'accountInfo']).as('settings.user').use(middleware.auth());
|
router.get('/settings/user/security', [UserController, 'accountInfo']).as('settings.user').use(middleware.auth());
|
||||||
router.post('/settings/user/store', [UserController, 'accountInfoStore']).as('account.password.store').use(middleware.auth());
|
router.post('/settings/user/store', [UserController, 'accountInfoStore']).as('account.password.store').use(middleware.auth());
|
||||||
|
router.get('/settings/profile/edit', [UserController, 'profile']).as('settings.profile.edit').use(middleware.auth());
|
||||||
|
router
|
||||||
|
.put('/settings/profile/:id/update', [UserController, 'profileUpdate'])
|
||||||
|
.as('settings.profile.update')
|
||||||
|
.where('id', router.matchers.number())
|
||||||
|
.use(middleware.auth());
|
||||||
|
router
|
||||||
|
.put('/settings/password/update', [UserController, 'passwordUpdate'])
|
||||||
|
.as('settings.password.update')
|
||||||
|
.use(middleware.auth());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Submitter routes
|
// Submitter routes
|
||||||
router
|
router
|
||||||
|
|
|
@ -19,7 +19,7 @@ type Options = {
|
||||||
// extnames: string[];
|
// extnames: string[];
|
||||||
// clientNameSizeLimit?: number;
|
// clientNameSizeLimit?: number;
|
||||||
allowedExtensions: string[];
|
allowedExtensions: string[];
|
||||||
allowedMimeTypes: string[];
|
allowedMimeTypes?: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// async function allowedMimetypeExtensions(file: VineMultipartFile | unknown, options: Options | unknown, field: FieldContext) {
|
// async function allowedMimetypeExtensions(file: VineMultipartFile | unknown, options: Options | unknown, field: FieldContext) {
|
||||||
|
|
Loading…
Add table
Reference in a new issue