- added backup codes for 2 factor authentication
Some checks failed
CI Pipeline / japa-tests (push) Failing after 58s
Some checks failed
CI Pipeline / japa-tests (push) Failing after 58s
- npm updates - coverage validation: elevation ust be positive, depth must be negative - vinejs-provider.js: get enabled extensions from database, not via validOptions.extnames - vue components for backup codes: e.g.: PersonalSettings.vue - validate spaital coverage in leaflet map: draw.component.vue, map.component.vue - add backup code authentication into Login.vue - preset to use no preferred reviewer: Release.vue - 2 new vinejs validation rules: file_scan.ts and file-length.ts
This commit is contained in:
parent
ac473b1e72
commit
005df2e454
|
@ -67,7 +67,8 @@ COPY --chown=node:node . .
|
||||||
# In this stage, we will start building dependencies
|
# In this stage, we will start building dependencies
|
||||||
FROM dependencies AS build
|
FROM dependencies AS build
|
||||||
# We run "node ace build" to build the app (dist folder) for production
|
# We run "node ace build" to build the app (dist folder) for production
|
||||||
RUN node ace build # --production
|
RUN node ace build --ignore-ts-errors
|
||||||
|
# RUN node ace build --production
|
||||||
# RUN node ace build --ignore-ts-errors
|
# RUN node ace build --ignore-ts-errors
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -29,7 +29,8 @@ export default defineConfig({
|
||||||
() => import('#start/rules/unique'),
|
() => import('#start/rules/unique'),
|
||||||
() => import('#start/rules/translated_language'),
|
() => import('#start/rules/translated_language'),
|
||||||
() => import('#start/rules/unique_person'),
|
() => import('#start/rules/unique_person'),
|
||||||
() => import('#start/rules/file_length')
|
() => import('#start/rules/file_length'),
|
||||||
|
() => import('#start/rules/file_scan')
|
||||||
],
|
],
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import type { HttpContext } from '@adonisjs/core/http';
|
import type { HttpContext } from '@adonisjs/core/http';
|
||||||
// import TotpSecret from 'App/Models/TotpSecret';
|
|
||||||
import User from '#models/user';
|
import User from '#models/user';
|
||||||
import TwoFactorAuthProvider from '#app/services/TwoFactorAuthProvider';
|
import TwoFactorAuthProvider from '#app/services/TwoFactorAuthProvider';
|
||||||
import { StatusCodes } from 'http-status-codes';
|
import { StatusCodes } from 'http-status-codes';
|
||||||
import { InvalidArgumentException } from 'node-exceptions';
|
import { InvalidArgumentException } from 'node-exceptions';
|
||||||
import { TotpState } from '#contracts/enums';
|
import { TotpState } from '#contracts/enums';
|
||||||
|
import BackupCodeStorage, { SecureRandom } from '#services/backup_code_storage';
|
||||||
|
import BackupCode from '#models/backup_code';
|
||||||
|
|
||||||
// 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,15 +28,20 @@ export default class UserController {
|
||||||
case TotpState.STATE_DISABLED:
|
case TotpState.STATE_DISABLED:
|
||||||
// user.twoFactorSecret = null;
|
// user.twoFactorSecret = null;
|
||||||
// user.twoFactorRecoveryCodes = null;
|
// user.twoFactorRecoveryCodes = null;
|
||||||
user.twoFactorSecret = "";
|
await BackupCode.deleteCodes(user);
|
||||||
user.twoFactorRecoveryCodes = [""];
|
user.twoFactorSecret = '';
|
||||||
|
// user.twoFactorRecoveryCodes = [''];
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
user.state = TotpState.STATE_DISABLED;
|
user.state = TotpState.STATE_DISABLED;
|
||||||
await user.save();
|
await user.save();
|
||||||
|
|
||||||
|
let storage = new BackupCodeStorage(new SecureRandom());
|
||||||
|
let backupState = await storage.getBackupCodesState(user);
|
||||||
|
|
||||||
return response.status(StatusCodes.OK).json({
|
return response.status(StatusCodes.OK).json({
|
||||||
state: TotpState.STATE_DISABLED,
|
state: TotpState.STATE_DISABLED,
|
||||||
|
backupState: backupState,
|
||||||
});
|
});
|
||||||
case TotpState.STATE_CREATED:
|
case TotpState.STATE_CREATED:
|
||||||
user.twoFactorSecret = TwoFactorAuthProvider.generateSecret(user);
|
user.twoFactorSecret = TwoFactorAuthProvider.generateSecret(user);
|
||||||
|
@ -56,7 +61,7 @@ export default class UserController {
|
||||||
if (!code) {
|
if (!code) {
|
||||||
throw new InvalidArgumentException('code is missing');
|
throw new InvalidArgumentException('code is missing');
|
||||||
}
|
}
|
||||||
const success = await TwoFactorAuthProvider.enable(user, code)
|
const success = await TwoFactorAuthProvider.enable(user, code);
|
||||||
|
|
||||||
return response.status(StatusCodes.OK).json({
|
return response.status(StatusCodes.OK).json({
|
||||||
state: success ? TotpState.STATE_ENABLED : TotpState.STATE_CREATED,
|
state: success ? TotpState.STATE_ENABLED : TotpState.STATE_CREATED,
|
||||||
|
@ -79,4 +84,31 @@ export default class UserController {
|
||||||
// recoveryCodes: user.twoFactorRecoveryCodes,
|
// recoveryCodes: user.twoFactorRecoveryCodes,
|
||||||
// });
|
// });
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @NoAdminRequired
|
||||||
|
* @PasswordConfirmationRequired
|
||||||
|
*
|
||||||
|
* @return JSONResponse
|
||||||
|
*/
|
||||||
|
public async createCodes({ auth, response }: HttpContext) {
|
||||||
|
// $user = $this->userSession->getUser();
|
||||||
|
const user = (await User.find(auth.user?.id)) as User;
|
||||||
|
|
||||||
|
// let codes = TwoFactorAuthProvider.generateRecoveryCodes();
|
||||||
|
let storage = new BackupCodeStorage(new SecureRandom());
|
||||||
|
// $codes = $this->storage->createCodes($user);
|
||||||
|
const codes = await storage.createCodes(user);
|
||||||
|
|
||||||
|
let backupState = await storage.getBackupCodesState(user);
|
||||||
|
// return new JSONResponse([
|
||||||
|
// 'codes' => $codes,
|
||||||
|
// 'state' => $this->storage->getBackupCodesState($user),
|
||||||
|
// ]);
|
||||||
|
return response.status(StatusCodes.OK).json({
|
||||||
|
codes: codes,
|
||||||
|
// state: success ? TotpState.STATE_ENABLED : TotpState.STATE_CREATED,
|
||||||
|
backupState: backupState, //storage.getBackupCodesState(user),
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import type { HttpContext } from '@adonisjs/core/http';
|
import type { HttpContext } from '@adonisjs/core/http';
|
||||||
import User from '#models/user';
|
import User from '#models/user';
|
||||||
|
import BackupCode from '#models/backup_code';
|
||||||
// import Hash from '@ioc:Adonis/Core/Hash';
|
// import Hash from '@ioc:Adonis/Core/Hash';
|
||||||
// import InvalidCredentialException from 'App/Exceptions/InvalidCredentialException';
|
// import InvalidCredentialException from 'App/Exceptions/InvalidCredentialException';
|
||||||
import { authValidator } from '#validators/auth';
|
import { authValidator } from '#validators/auth';
|
||||||
|
import hash from '@adonisjs/core/services/hash';
|
||||||
|
|
||||||
import TwoFactorAuthProvider from '#app/services/TwoFactorAuthProvider';
|
import TwoFactorAuthProvider from '#app/services/TwoFactorAuthProvider';
|
||||||
// import { Authenticator } from '@adonisjs/auth';
|
// import { Authenticator } from '@adonisjs/auth';
|
||||||
|
@ -31,7 +33,7 @@ export default class AuthController {
|
||||||
// await auth.use('web').attempt(email, plainPassword);
|
// await auth.use('web').attempt(email, plainPassword);
|
||||||
|
|
||||||
// const user = await auth.use('web').verifyCredentials(email, password);
|
// const user = await auth.use('web').verifyCredentials(email, password);
|
||||||
const user = await User.verifyCredentials(email, password)
|
const user = await User.verifyCredentials(email, password);
|
||||||
|
|
||||||
if (user.isTwoFactorEnabled) {
|
if (user.isTwoFactorEnabled) {
|
||||||
// session.put("login.id", user.id);
|
// session.put("login.id", user.id);
|
||||||
|
@ -59,8 +61,7 @@ export default class AuthController {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async twoFactorChallenge({ request, session, auth, response }: HttpContext) {
|
public async twoFactorChallenge({ request, session, auth, response }: HttpContext) {
|
||||||
const { code, recoveryCode, login_id } = request.only(['code', 'recoveryCode', 'login_id']);
|
const { code, backup_code, login_id } = request.only(['code', 'backup_code', 'login_id']);
|
||||||
// const user = await User.query().where('id', session.get('login.id')).firstOrFail();
|
|
||||||
const user = await User.query().where('id', login_id).firstOrFail();
|
const user = await User.query().where('id', login_id).firstOrFail();
|
||||||
|
|
||||||
if (code) {
|
if (code) {
|
||||||
|
@ -70,16 +71,45 @@ export default class AuthController {
|
||||||
await auth.use('web').login(user);
|
await auth.use('web').login(user);
|
||||||
response.redirect('/apps/dashboard');
|
response.redirect('/apps/dashboard');
|
||||||
} else {
|
} else {
|
||||||
session.flash('message', 'Your tow factor code is incorrect');
|
session.flash('message', 'Your two-factor code is incorrect');
|
||||||
return response.redirect().back();
|
return response.redirect().back();
|
||||||
}
|
}
|
||||||
} else if (recoveryCode) {
|
} else if (backup_code) {
|
||||||
const codes = user?.twoFactorRecoveryCodes ?? [];
|
const codes: BackupCode[] = await user.getBackupCodes();
|
||||||
if (codes.includes(recoveryCode)) {
|
|
||||||
user.twoFactorRecoveryCodes = codes.filter((c) => c !== recoveryCode);
|
// const verifiedBackupCodes = await Promise.all(
|
||||||
await user.save();
|
// codes.map(async (backupCode) => {
|
||||||
await auth.use('web').login(user);
|
// let isVerified = await hash.verify(backupCode.code, backup_code);
|
||||||
response.redirect('/apps/dashboard');
|
// if (isVerified) {
|
||||||
|
// return backupCode;
|
||||||
|
// }
|
||||||
|
// }),
|
||||||
|
// );
|
||||||
|
// const backupCodeToDelete = verifiedBackupCodes.find(Boolean);
|
||||||
|
|
||||||
|
let backupCodeToDelete = null;
|
||||||
|
for (const backupCode of codes) {
|
||||||
|
const isVerified = await hash.verify(backupCode.code, backup_code);
|
||||||
|
if (isVerified) {
|
||||||
|
backupCodeToDelete = backupCode;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (backupCodeToDelete) {
|
||||||
|
if (backupCodeToDelete.used === false) {
|
||||||
|
backupCodeToDelete.used = true;
|
||||||
|
await backupCodeToDelete.save();
|
||||||
|
console.log(`BackupCode with id ${backupCodeToDelete.id} has been marked as used.`);
|
||||||
|
await auth.use('web').login(user);
|
||||||
|
response.redirect('/apps/dashboard');
|
||||||
|
} else {
|
||||||
|
session.flash('message', 'BackupCode already used');
|
||||||
|
return response.redirect().back();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
session.flash('message', 'BackupCode not found');
|
||||||
|
return response.redirect().back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,7 @@ import TwoFactorAuthProvider from '#app/services/TwoFactorAuthProvider';
|
||||||
import hash from '@adonisjs/core/services/hash';
|
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';
|
||||||
|
|
||||||
// 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 {
|
||||||
|
@ -19,10 +20,15 @@ export default class UserController {
|
||||||
// const id = request.param('id');
|
// const id = request.param('id');
|
||||||
// const user = await User.query().where('id', id).firstOrFail();
|
// const user = await User.query().where('id', id).firstOrFail();
|
||||||
|
|
||||||
|
let storage = new BackupCodeStorage(new SecureRandom());
|
||||||
|
// const codes= user.isTwoFactorEnabled? (await user.getBackupCodes()).map((role) => role.code) : [];
|
||||||
|
let backupState = await storage.getBackupCodesState(user);
|
||||||
|
|
||||||
return inertia.render('Auth/AccountInfo', {
|
return inertia.render('Auth/AccountInfo', {
|
||||||
user: user,
|
user: user,
|
||||||
twoFactorEnabled: user.isTwoFactorEnabled,
|
twoFactorEnabled: user.isTwoFactorEnabled,
|
||||||
// code: await TwoFactorAuthProvider.generateQrCode(user),
|
// code: await TwoFactorAuthProvider.generateQrCode(user),
|
||||||
|
backupState: backupState,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -327,13 +327,13 @@ export default class DatasetController {
|
||||||
x_max: vine.number(),
|
x_max: vine.number(),
|
||||||
y_min: vine.number(),
|
y_min: vine.number(),
|
||||||
y_max: vine.number(),
|
y_max: vine.number(),
|
||||||
elevation_absolut: vine.number().optional(),
|
elevation_absolut: vine.number().positive().optional(),
|
||||||
elevation_min: vine.number().optional().requiredIfExists('elevation_max'),
|
elevation_min: vine.number().positive().optional().requiredIfExists('elevation_max'),
|
||||||
elevation_max: vine.number().optional().requiredIfExists('elevation_min'),
|
elevation_max: vine.number().positive().optional().requiredIfExists('elevation_min'),
|
||||||
// type: vine.enum(Object.values(DescriptionTypes)),
|
// type: vine.enum(Object.values(DescriptionTypes)),
|
||||||
depth_absolut: vine.number().optional(),
|
depth_absolut: vine.number().negative().optional(),
|
||||||
depth_min: vine.number().optional().requiredIfExists('depth_max'),
|
depth_min: vine.number().negative().optional().requiredIfExists('depth_max'),
|
||||||
depth_max: vine.number().optional().requiredIfExists('depth_min'),
|
depth_max: vine.number().negative().optional().requiredIfExists('depth_min'),
|
||||||
}),
|
}),
|
||||||
references: vine
|
references: vine
|
||||||
.array(
|
.array(
|
||||||
|
|
51
app/models/backup_code.ts
Normal file
51
app/models/backup_code.ts
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
import BaseModel from './base_model.js';
|
||||||
|
import { column, SnakeCaseNamingStrategy, belongsTo } from '@adonisjs/lucid/orm';
|
||||||
|
import User from './user.js';
|
||||||
|
import type { BelongsTo } from '@adonisjs/lucid/types/relations';
|
||||||
|
import db from '@adonisjs/lucid/services/db';
|
||||||
|
import hash from '@adonisjs/core/services/hash';
|
||||||
|
|
||||||
|
export default class BackupCode extends BaseModel {
|
||||||
|
public static table = 'backupcodes';
|
||||||
|
public static namingStrategy = new SnakeCaseNamingStrategy();
|
||||||
|
|
||||||
|
@column({
|
||||||
|
isPrimary: true,
|
||||||
|
})
|
||||||
|
public id: number;
|
||||||
|
|
||||||
|
@column({})
|
||||||
|
public user_id: number;
|
||||||
|
|
||||||
|
@column({
|
||||||
|
// serializeAs: null,
|
||||||
|
// consume: (value: string) => (value ? JSON.parse(encryption.decrypt(value) ?? '{}') : null),
|
||||||
|
// prepare: (value: string) => encryption.encrypt(JSON.stringify(value)),
|
||||||
|
})
|
||||||
|
public code: string;
|
||||||
|
|
||||||
|
@column({})
|
||||||
|
public used: boolean;
|
||||||
|
|
||||||
|
@belongsTo(() => User, {
|
||||||
|
foreignKey: 'user_id',
|
||||||
|
})
|
||||||
|
public user: BelongsTo<typeof User>;
|
||||||
|
|
||||||
|
// public static async getBackupCodes(user: User): Promise<BackupCode[]> {
|
||||||
|
// return await db.from(this.table).select('id', 'user_id', 'code', 'used').where('user_id', user.id);
|
||||||
|
// }
|
||||||
|
|
||||||
|
public static async deleteCodes(user: User): Promise<void> {
|
||||||
|
await db.from(this.table).where('user_id', user.id).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async deleteCodesByUserId(uid: string): Promise<void> {
|
||||||
|
await db.from(this.table).where('user_id', uid).delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to verify password
|
||||||
|
public async verifyCode(plainCode: string) {
|
||||||
|
return await hash.verify(this.code, plainCode);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
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 } from '@adonisjs/lucid/orm';
|
import { column, manyToMany, hasMany } 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';
|
||||||
|
@ -13,6 +13,7 @@ import { TotpState } from '#contracts/enums';
|
||||||
import type { ManyToMany } from '@adonisjs/lucid/types/relations';
|
import type { ManyToMany } from '@adonisjs/lucid/types/relations';
|
||||||
import type { HasMany } from '@adonisjs/lucid/types/relations';
|
import type { HasMany } from '@adonisjs/lucid/types/relations';
|
||||||
import { compose } from '@adonisjs/core/helpers';
|
import { compose } from '@adonisjs/core/helpers';
|
||||||
|
import BackupCode from './backup_code.js';
|
||||||
|
|
||||||
const AuthFinder = withAuthFinder(() => hash.use('laravel'), {
|
const AuthFinder = withAuthFinder(() => hash.use('laravel'), {
|
||||||
uids: ['email'],
|
uids: ['email'],
|
||||||
|
@ -107,6 +108,19 @@ export default class User extends compose(BaseModel, AuthFinder) {
|
||||||
})
|
})
|
||||||
public datasets: HasMany<typeof Dataset>;
|
public datasets: HasMany<typeof Dataset>;
|
||||||
|
|
||||||
|
@hasMany(() => BackupCode, {
|
||||||
|
foreignKey: 'user_id',
|
||||||
|
})
|
||||||
|
public backupcodes: HasMany<typeof BackupCode>;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public async getBackupCodes(this: User): Promise<BackupCode[]> {
|
||||||
|
const test = await this.related('backupcodes').query();
|
||||||
|
// return test.map((role) => role.code);
|
||||||
|
return test;
|
||||||
|
}
|
||||||
|
|
||||||
// https://github.com/adonisjs/core/discussions/1872#discussioncomment-132289
|
// https://github.com/adonisjs/core/discussions/1872#discussioncomment-132289
|
||||||
public async getRoles(this: User): Promise<string[]> {
|
public async getRoles(this: User): Promise<string[]> {
|
||||||
const test = await this.related('roles').query();
|
const test = await this.related('roles').query();
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
// import Config from '@ioc:Adonis/Core/Config';
|
// import Config from '@ioc:Adonis/Core/Config';
|
||||||
import config from '@adonisjs/core/services/config'
|
// import config from '@adonisjs/core/services/config'
|
||||||
|
import env from '#start/env';
|
||||||
import User from '#models/user';
|
import User from '#models/user';
|
||||||
import { generateSecret, verifyToken } from 'node-2fa/dist/index.js';
|
import { generateSecret, verifyToken } from 'node-2fa/dist/index.js';
|
||||||
// import cryptoRandomString from 'crypto-random-string';
|
// import cryptoRandomString from 'crypto-random-string';
|
||||||
|
@ -14,7 +15,7 @@ import { TotpState } from '#contracts/enums';
|
||||||
// npm i --save-dev @types/qrcode
|
// npm i --save-dev @types/qrcode
|
||||||
|
|
||||||
class TwoFactorAuthProvider {
|
class TwoFactorAuthProvider {
|
||||||
private issuer: string = config.get('twoFactorAuthConfig.app.name') || 'TethysCloud';
|
private issuer: string = env.get('APP_NAME') || 'TethysCloud';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* generateSecret will generate a user-specific 32-character secret.
|
* generateSecret will generate a user-specific 32-character secret.
|
||||||
|
@ -41,7 +42,7 @@ class TwoFactorAuthProvider {
|
||||||
* Return recovery codes
|
* Return recovery codes
|
||||||
* @return {string[]}
|
* @return {string[]}
|
||||||
*/
|
*/
|
||||||
public generateRecoveryCodes() {
|
public generateRecoveryCodes(): string[] {
|
||||||
const recoveryCodeLimit: number = 8;
|
const recoveryCodeLimit: number = 8;
|
||||||
const codes: string[] = [];
|
const codes: string[] = [];
|
||||||
for (let i = 0; i < recoveryCodeLimit; i++) {
|
for (let i = 0; i < recoveryCodeLimit; i++) {
|
||||||
|
|
136
app/services/backup_code_storage.ts
Normal file
136
app/services/backup_code_storage.ts
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
import User from '#models/user';
|
||||||
|
import BackupCode from '#models/backup_code';
|
||||||
|
import hash from '@adonisjs/core/services/hash';
|
||||||
|
|
||||||
|
export interface ISecureRandom {
|
||||||
|
CHAR_UPPER: string;
|
||||||
|
CHAR_LOWER: string;
|
||||||
|
CHAR_DIGITS: string;
|
||||||
|
CHAR_SYMBOLS: string;
|
||||||
|
CHAR_ALPHANUMERIC: string;
|
||||||
|
CHAR_HUMAN_READABLE: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random string of specified length.
|
||||||
|
* @param int $length The length of the generated string
|
||||||
|
* @param string $characters An optional list of characters to use if no character list is
|
||||||
|
* specified all valid base64 characters are used.
|
||||||
|
* @return string
|
||||||
|
* @since 8.0.0
|
||||||
|
*/
|
||||||
|
generate(length: number, characters?: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SecureRandom implements ISecureRandom {
|
||||||
|
CHAR_UPPER: string = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
CHAR_LOWER: string = 'abcdefghijklmnopqrstuvwxyz';
|
||||||
|
CHAR_DIGITS: string = '0123456789';
|
||||||
|
CHAR_SYMBOLS: string = '!"#$%&\\\'()*+,-./:;<=>?@[]^_`{|}~';
|
||||||
|
CHAR_ALPHANUMERIC: string = this.CHAR_UPPER + this.CHAR_LOWER + this.CHAR_DIGITS;
|
||||||
|
CHAR_HUMAN_READABLE: string = 'abcdefgijkmnopqrstwxyzABCDEFGHJKLMNPQRSTWXYZ23456789';
|
||||||
|
|
||||||
|
public generate(length: number, characters: string = this.CHAR_ALPHANUMERIC): string {
|
||||||
|
if (length <= 0) {
|
||||||
|
throw new Error('Invalid length specified: ' + length + ' must be bigger than 0');
|
||||||
|
}
|
||||||
|
const maxCharIndex: number = characters.length - 1;
|
||||||
|
let randomString: string = '';
|
||||||
|
while (length > 0) {
|
||||||
|
const randomNumber: number = Math.floor(Math.random() * (maxCharIndex + 1));
|
||||||
|
randomString += characters[randomNumber];
|
||||||
|
length--;
|
||||||
|
}
|
||||||
|
return randomString;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BackupCodeStorage {
|
||||||
|
private static CODE_LENGTH: number = 16;
|
||||||
|
// private mapper: BackupCodeMapper;
|
||||||
|
// private hasher: IHasher;
|
||||||
|
private random: ISecureRandom;
|
||||||
|
// private eventDispatcher: IEventDispatcher;
|
||||||
|
|
||||||
|
// constructor(mapper: BackupCodeMapper, random: ISecureRandom, hasher: IHasher, eventDispatcher: IEventDispatcher) {
|
||||||
|
// this.mapper = mapper;
|
||||||
|
// this.hasher = hasher;
|
||||||
|
// this.random = random;
|
||||||
|
// this.eventDispatcher = eventDispatcher;
|
||||||
|
// }
|
||||||
|
constructor(random: ISecureRandom) {
|
||||||
|
// this.mapper = mapper;
|
||||||
|
// this.hasher = hasher;
|
||||||
|
this.random = random;
|
||||||
|
// this.eventDispatcher = eventDispatcher;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async createCodes(user: User, number: number = 10): Promise<string[]> {
|
||||||
|
let results: string[] = [];
|
||||||
|
// this.mapper.deleteCodes(user);
|
||||||
|
await BackupCode.deleteCodes(user);
|
||||||
|
// user.twoFactorRecoveryCodes = [""];
|
||||||
|
|
||||||
|
// const uid = user.getUID();
|
||||||
|
for (let i = 1; i <= Math.min(number, 20); i++) {
|
||||||
|
const code = this.random.generate(BackupCodeStorage.CODE_LENGTH, this.random.CHAR_HUMAN_READABLE);
|
||||||
|
// const code = crypto
|
||||||
|
// .randomBytes(Math.ceil(BackupCodeStorage.CODE_LENGTH / 2))
|
||||||
|
// .toString('hex')
|
||||||
|
// .slice(0, BackupCodeStorage.CODE_LENGTH);
|
||||||
|
const dbCode = new BackupCode();
|
||||||
|
// dbCode.setUserId(uid);
|
||||||
|
|
||||||
|
// dbCode.setCode(this.hasher.hash(code));
|
||||||
|
dbCode.code = await hash.make(code);
|
||||||
|
// dbCode.setUsed(0);
|
||||||
|
dbCode.used = false;
|
||||||
|
// this.mapper.insert(dbCode);
|
||||||
|
// await dbCode.save();
|
||||||
|
await dbCode.related('user').associate(user); // speichert schon ab
|
||||||
|
results.push(code);
|
||||||
|
}
|
||||||
|
// this.eventDispatcher.dispatchTyped(new CodesGenerated(user));
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async hasBackupCodes(user: User): Promise<boolean> {
|
||||||
|
const codes = await user.getBackupCodes();
|
||||||
|
return codes.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getBackupCodesState(user: User) {
|
||||||
|
// const codes = this.mapper.getBackupCodes(user);
|
||||||
|
// const codes = await user.related('backupcodes').query().exec();
|
||||||
|
const codes: BackupCode[] = await user.getBackupCodes();
|
||||||
|
const total = codes.length;
|
||||||
|
let used: number = 0;
|
||||||
|
codes.forEach((code) => {
|
||||||
|
if (code.used === true) {
|
||||||
|
used++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return {
|
||||||
|
enabled: total > 0,
|
||||||
|
total: total,
|
||||||
|
used: used,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// public validateCode(user: User, code: string): boolean {
|
||||||
|
// const dbCodes = await user.getBackupCodes();
|
||||||
|
// for (const dbCode of dbCodes) {
|
||||||
|
// if (parseInt(dbCode.getUsed()) === 0 && this.hasher.verify(code, dbCode.getCode())) {
|
||||||
|
// dbCode.setUsed(1);
|
||||||
|
// this.mapper.update(dbCode);
|
||||||
|
// return true;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// return false;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// public deleteCodes(user: User): void {
|
||||||
|
// this.mapper.deleteCodes(user);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
export default BackupCodeStorage;
|
|
@ -4,9 +4,11 @@ import dayjs from 'dayjs';
|
||||||
import MimeType from '#models/mime_type';
|
import MimeType from '#models/mime_type';
|
||||||
|
|
||||||
const enabledExtensions = await MimeType.query().select('file_extension').where('enabled', true).exec();
|
const enabledExtensions = await MimeType.query().select('file_extension').where('enabled', true).exec();
|
||||||
const extensions = enabledExtensions.map((extension)=> {
|
const extensions = enabledExtensions
|
||||||
return extension.file_extension.split('|')
|
.map((extension) => {
|
||||||
}).flat();
|
return extension.file_extension.split('|');
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the dataset's creation action
|
* Validates the dataset's creation action
|
||||||
|
@ -55,7 +57,13 @@ export const createDatasetValidator = vine.compile(
|
||||||
authors: vine
|
authors: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
email: vine.string().trim().maxLength(255).email().normalizeEmail().isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
|
email: vine
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.maxLength(255)
|
||||||
|
.email()
|
||||||
|
.normalizeEmail()
|
||||||
|
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
|
||||||
first_name: vine.string().trim().minLength(3).maxLength(255),
|
first_name: vine.string().trim().minLength(3).maxLength(255),
|
||||||
last_name: vine.string().trim().minLength(3).maxLength(255),
|
last_name: vine.string().trim().minLength(3).maxLength(255),
|
||||||
}),
|
}),
|
||||||
|
@ -65,7 +73,13 @@ export const createDatasetValidator = vine.compile(
|
||||||
contributors: vine
|
contributors: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
email: vine.string().trim().maxLength(255).email().normalizeEmail().isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
|
email: vine
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.maxLength(255)
|
||||||
|
.email()
|
||||||
|
.normalizeEmail()
|
||||||
|
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
|
||||||
first_name: vine.string().trim().minLength(3).maxLength(255),
|
first_name: vine.string().trim().minLength(3).maxLength(255),
|
||||||
last_name: vine.string().trim().minLength(3).maxLength(255),
|
last_name: vine.string().trim().minLength(3).maxLength(255),
|
||||||
pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)),
|
pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)),
|
||||||
|
@ -89,13 +103,13 @@ export const createDatasetValidator = vine.compile(
|
||||||
x_max: vine.number(),
|
x_max: vine.number(),
|
||||||
y_min: vine.number(),
|
y_min: vine.number(),
|
||||||
y_max: vine.number(),
|
y_max: vine.number(),
|
||||||
elevation_absolut: vine.number().optional(),
|
elevation_absolut: vine.number().positive().optional(),
|
||||||
elevation_min: vine.number().optional().requiredIfExists('elevation_max'),
|
elevation_min: vine.number().positive().optional().requiredIfExists('elevation_max'),
|
||||||
elevation_max: vine.number().optional().requiredIfExists('elevation_min'),
|
elevation_max: vine.number().positive().optional().requiredIfExists('elevation_min'),
|
||||||
// type: vine.enum(Object.values(DescriptionTypes)),
|
// type: vine.enum(Object.values(DescriptionTypes)),
|
||||||
depth_absolut: vine.number().optional(),
|
depth_absolut: vine.number().negative().optional(),
|
||||||
depth_min: vine.number().optional().requiredIfExists('depth_max'),
|
depth_min: vine.number().negative().optional().requiredIfExists('depth_max'),
|
||||||
depth_max: vine.number().optional().requiredIfExists('depth_min'),
|
depth_max: vine.number().negative().optional().requiredIfExists('depth_min'),
|
||||||
}),
|
}),
|
||||||
references: vine
|
references: vine
|
||||||
.array(
|
.array(
|
||||||
|
@ -120,10 +134,13 @@ export const createDatasetValidator = vine.compile(
|
||||||
// last step
|
// last step
|
||||||
files: vine
|
files: vine
|
||||||
.array(
|
.array(
|
||||||
vine.myfile({
|
vine
|
||||||
size: '512mb',
|
.myfile({
|
||||||
extnames: extensions,
|
size: '512mb',
|
||||||
}).filenameLength({ clientNameSizeLimit : 100 }),
|
extnames: extensions,
|
||||||
|
})
|
||||||
|
.filenameLength({ clientNameSizeLimit: 100 })
|
||||||
|
.fileScan({ removeInfected: true }),
|
||||||
)
|
)
|
||||||
.minLength(1),
|
.minLength(1),
|
||||||
}),
|
}),
|
||||||
|
@ -175,7 +192,13 @@ export const updateDatasetValidator = vine.compile(
|
||||||
authors: vine
|
authors: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
email: vine.string().trim().maxLength(255).email().normalizeEmail().isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
|
email: vine
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.maxLength(255)
|
||||||
|
.email()
|
||||||
|
.normalizeEmail()
|
||||||
|
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
|
||||||
first_name: vine.string().trim().minLength(3).maxLength(255),
|
first_name: vine.string().trim().minLength(3).maxLength(255),
|
||||||
last_name: vine.string().trim().minLength(3).maxLength(255),
|
last_name: vine.string().trim().minLength(3).maxLength(255),
|
||||||
}),
|
}),
|
||||||
|
@ -185,7 +208,13 @@ export const updateDatasetValidator = vine.compile(
|
||||||
contributors: vine
|
contributors: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
email: vine.string().trim().maxLength(255).email().normalizeEmail().isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
|
email: vine
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.maxLength(255)
|
||||||
|
.email()
|
||||||
|
.normalizeEmail()
|
||||||
|
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
|
||||||
first_name: vine.string().trim().minLength(3).maxLength(255),
|
first_name: vine.string().trim().minLength(3).maxLength(255),
|
||||||
last_name: vine.string().trim().minLength(3).maxLength(255),
|
last_name: vine.string().trim().minLength(3).maxLength(255),
|
||||||
pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)),
|
pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)),
|
||||||
|
@ -209,13 +238,13 @@ export const updateDatasetValidator = vine.compile(
|
||||||
x_max: vine.number(),
|
x_max: vine.number(),
|
||||||
y_min: vine.number(),
|
y_min: vine.number(),
|
||||||
y_max: vine.number(),
|
y_max: vine.number(),
|
||||||
elevation_absolut: vine.number().optional(),
|
elevation_absolut: vine.number().positive().optional(),
|
||||||
elevation_min: vine.number().optional().requiredIfExists('elevation_max'),
|
elevation_min: vine.number().positive().optional().requiredIfExists('elevation_max'),
|
||||||
elevation_max: vine.number().optional().requiredIfExists('elevation_min'),
|
elevation_max: vine.number().positive().optional().requiredIfExists('elevation_min'),
|
||||||
// type: vine.enum(Object.values(DescriptionTypes)),
|
// type: vine.enum(Object.values(DescriptionTypes)),
|
||||||
depth_absolut: vine.number().optional(),
|
depth_absolut: vine.number().negative().optional(),
|
||||||
depth_min: vine.number().optional().requiredIfExists('depth_max'),
|
depth_min: vine.number().negative().optional().requiredIfExists('depth_max'),
|
||||||
depth_max: vine.number().optional().requiredIfExists('depth_min'),
|
depth_max: vine.number().negative().optional().requiredIfExists('depth_min'),
|
||||||
}),
|
}),
|
||||||
references: vine
|
references: vine
|
||||||
.array(
|
.array(
|
||||||
|
@ -238,14 +267,13 @@ export const updateDatasetValidator = vine.compile(
|
||||||
.minLength(3)
|
.minLength(3)
|
||||||
.distinct('value'),
|
.distinct('value'),
|
||||||
// last step
|
// last step
|
||||||
files: vine
|
files: vine.array(
|
||||||
.array(
|
vine.myfile({
|
||||||
vine.myfile({
|
size: '512mb',
|
||||||
size: '512mb',
|
extnames: extensions,
|
||||||
extnames: extensions,
|
}),
|
||||||
}),
|
),
|
||||||
),
|
// .minLength(1),
|
||||||
// .minLength(1),
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -74,7 +74,7 @@ export default class IndexDatasets extends BaseCommand {
|
||||||
});
|
});
|
||||||
this.logger.info(`dataset with publish_id ${dataset.publish_id} successfully indexed`);
|
this.logger.info(`dataset with publish_id ${dataset.publish_id} successfully indexed`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.logger.error(`An error occurred while indexing datsaet with publish_id ${dataset.publish_id}.`);
|
this.logger.error(`An error occurred while indexing dataset with publish_id ${dataset.publish_id}.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
32
database/migrations/test.ts
Normal file
32
database/migrations/test.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
// CREATE SEQUENCE IF NOT EXISTS gba.backupcodes_id_seq
|
||||||
|
// INCREMENT 1
|
||||||
|
// START 1
|
||||||
|
// MINVALUE 1
|
||||||
|
// MAXVALUE 2147483647
|
||||||
|
// CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
|
// ALTER SEQUENCE gba.backupcodes_id_seq
|
||||||
|
// OWNER TO tethys_admin;
|
||||||
|
|
||||||
|
// GRANT ALL ON SEQUENCE gba.backupcodes_id_seq TO tethys_admin;
|
||||||
|
|
||||||
|
// GRANT USAGE ON SEQUENCE gba.backupcodes_id_seq TO tethys_app;
|
||||||
|
|
||||||
|
// CREATE TABLE IF NOT EXISTS gba.backupcodes
|
||||||
|
// (
|
||||||
|
// id integer NOT NULL DEFAULT nextval('gba.backupcodes_id_seq'::regclass),
|
||||||
|
// user_id integer,
|
||||||
|
// code character varying(64) NOT NULL,
|
||||||
|
// used boolean NOT NULL DEFAULT false,
|
||||||
|
// CONSTRAINT backupcodes_user_id_foreign FOREIGN KEY (user_id)
|
||||||
|
// REFERENCES gba.accounts (id) MATCH SIMPLE
|
||||||
|
// ON UPDATE CASCADE
|
||||||
|
// ON DELETE CASCADE,
|
||||||
|
// CONSTRAINT backupcodes_pkey PRIMARY KEY (id)
|
||||||
|
// )
|
||||||
|
|
||||||
|
|
||||||
|
// CREATE INDEX IF NOT EXISTS backupcodes_uid
|
||||||
|
// ON gba.backupcodes USING btree
|
||||||
|
// (user_id ASC);
|
841
package-lock.json
generated
841
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
|
@ -79,8 +79,8 @@
|
||||||
"@adonisjs/drive": "^2.3.0",
|
"@adonisjs/drive": "^2.3.0",
|
||||||
"@adonisjs/encore": "^1.0.0",
|
"@adonisjs/encore": "^1.0.0",
|
||||||
"@adonisjs/inertia": "^1.0.0-7",
|
"@adonisjs/inertia": "^1.0.0-7",
|
||||||
"@adonisjs/lucid": "^20.2.0",
|
"@adonisjs/lucid": "^21.1.0",
|
||||||
"@adonisjs/redis": "^8.0.1",
|
"@adonisjs/redis": "^9.1.0",
|
||||||
"@adonisjs/session": "^7.1.1",
|
"@adonisjs/session": "^7.1.1",
|
||||||
"@adonisjs/shield": "^8.1.1",
|
"@adonisjs/shield": "^8.1.1",
|
||||||
"@adonisjs/static": "^1.1.1",
|
"@adonisjs/static": "^1.1.1",
|
||||||
|
@ -112,6 +112,7 @@
|
||||||
"reflect-metadata": "^0.2.1",
|
"reflect-metadata": "^0.2.1",
|
||||||
"saxon-js": "^2.5.0",
|
"saxon-js": "^2.5.0",
|
||||||
"toastify-js": "^1.12.0",
|
"toastify-js": "^1.12.0",
|
||||||
|
"vue-i18n": "^9.13.1",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
"xmlbuilder2": "^3.1.1"
|
"xmlbuilder2": "^3.1.1"
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,6 +10,8 @@ import type { Validation, FieldContext, FieldOptions } from '@vinejs/vine/types'
|
||||||
import type { MultipartFile } from '@adonisjs/core/bodyparser';
|
import type { MultipartFile } from '@adonisjs/core/bodyparser';
|
||||||
import type { FileValidationOptions } from '@adonisjs/core/types/bodyparser';
|
import type { FileValidationOptions } from '@adonisjs/core/types/bodyparser';
|
||||||
import { Request, RequestValidator } from '@adonisjs/core/http';
|
import { Request, RequestValidator } from '@adonisjs/core/http';
|
||||||
|
import MimeType from '#models/mime_type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validation options accepted by the "file" rule
|
* Validation options accepted by the "file" rule
|
||||||
*/
|
*/
|
||||||
|
@ -37,11 +39,21 @@ declare module '@adonisjs/core/http' {
|
||||||
export function isBodyParserFile(file: MultipartFile | unknown): boolean {
|
export function isBodyParserFile(file: MultipartFile | unknown): boolean {
|
||||||
return !!(file && typeof file === 'object' && 'isMultipartFile' in file);
|
return !!(file && typeof file === 'object' && 'isMultipartFile' in file);
|
||||||
}
|
}
|
||||||
|
async function getEnabledExtensions() {
|
||||||
|
const enabledExtensions = await MimeType.query().select('file_extension').where('enabled', true).exec();
|
||||||
|
const extensions = enabledExtensions
|
||||||
|
.map((extension) => {
|
||||||
|
return extension.file_extension.split('|');
|
||||||
|
})
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
return extensions;
|
||||||
|
};
|
||||||
/**
|
/**
|
||||||
* VineJS validation rule that validates the file to be an
|
* VineJS validation rule that validates the file to be an
|
||||||
* instance of BodyParser MultipartFile class.
|
* instance of BodyParser MultipartFile class.
|
||||||
*/
|
*/
|
||||||
const isMultipartFile = vine.createRule((file: MultipartFile | unknown, options: FileRuleValidationOptions, field: FieldContext) => {
|
const isMultipartFile = vine.createRule(async (file: MultipartFile | unknown, options: FileRuleValidationOptions, field: FieldContext) => {
|
||||||
/**
|
/**
|
||||||
* Report error when value is not a field multipart
|
* Report error when value is not a field multipart
|
||||||
* file object
|
* file object
|
||||||
|
@ -64,8 +76,11 @@ const isMultipartFile = vine.createRule((file: MultipartFile | unknown, options:
|
||||||
* Set extensions when it's defined in the options and missing
|
* Set extensions when it's defined in the options and missing
|
||||||
* on the file instance
|
* on the file instance
|
||||||
*/
|
*/
|
||||||
|
// if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) {
|
||||||
|
// validatedFile.allowedExtensions = validationOptions.extnames;
|
||||||
|
// }
|
||||||
if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) {
|
if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) {
|
||||||
validatedFile.allowedExtensions = validationOptions.extnames;
|
validatedFile.allowedExtensions = await getEnabledExtensions();
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* wieder löschen
|
* wieder löschen
|
||||||
|
|
|
@ -17,8 +17,8 @@
|
||||||
"assets/resources_js_Pages_Admin_User_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Index_vue.js",
|
"assets/resources_js_Pages_Admin_User_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Index_vue.js",
|
||||||
"assets/resources_js_Pages_Admin_User_Show_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Show_vue.js",
|
"assets/resources_js_Pages_Admin_User_Show_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Show_vue.js",
|
||||||
"assets/resources_js_Pages_App_vue.js": "http://localhost:8080/assets/resources_js_Pages_App_vue.js",
|
"assets/resources_js_Pages_App_vue.js": "http://localhost:8080/assets/resources_js_Pages_App_vue.js",
|
||||||
"assets/resources_js_Pages_Auth_AccountInfo_vue.css": "http://localhost:8080/assets/resources_js_Pages_Auth_AccountInfo_vue.css",
|
"assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css.css": "http://localhost:8080/assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css.css",
|
||||||
"assets/resources_js_Pages_Auth_AccountInfo_vue.js": "http://localhost:8080/assets/resources_js_Pages_Auth_AccountInfo_vue.js",
|
"assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css.js": "http://localhost:8080/assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css.js",
|
||||||
"assets/resources_js_Pages_Auth_Login_vue.js": "http://localhost:8080/assets/resources_js_Pages_Auth_Login_vue.js",
|
"assets/resources_js_Pages_Auth_Login_vue.js": "http://localhost:8080/assets/resources_js_Pages_Auth_Login_vue.js",
|
||||||
"assets/resources_js_Pages_Auth_Register_vue.js": "http://localhost:8080/assets/resources_js_Pages_Auth_Register_vue.js",
|
"assets/resources_js_Pages_Auth_Register_vue.js": "http://localhost:8080/assets/resources_js_Pages_Auth_Register_vue.js",
|
||||||
"assets/resources_js_Pages_Dashboard_vue.js": "http://localhost:8080/assets/resources_js_Pages_Dashboard_vue.js",
|
"assets/resources_js_Pages_Dashboard_vue.js": "http://localhost:8080/assets/resources_js_Pages_Dashboard_vue.js",
|
||||||
|
@ -36,11 +36,11 @@
|
||||||
"assets/resources_js_Pages_Reviewer_Dataset_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Reviewer_Dataset_Index_vue.js",
|
"assets/resources_js_Pages_Reviewer_Dataset_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Reviewer_Dataset_Index_vue.js",
|
||||||
"assets/resources_js_Pages_Reviewer_Dataset_Reject_vue.js": "http://localhost:8080/assets/resources_js_Pages_Reviewer_Dataset_Reject_vue.js",
|
"assets/resources_js_Pages_Reviewer_Dataset_Reject_vue.js": "http://localhost:8080/assets/resources_js_Pages_Reviewer_Dataset_Reject_vue.js",
|
||||||
"assets/resources_js_Pages_Reviewer_Dataset_Review_vue.js": "http://localhost:8080/assets/resources_js_Pages_Reviewer_Dataset_Review_vue.js",
|
"assets/resources_js_Pages_Reviewer_Dataset_Review_vue.js": "http://localhost:8080/assets/resources_js_Pages_Reviewer_Dataset_Review_vue.js",
|
||||||
"assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_Components_FileUpload_vue-resour-910d0c.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_Components_FileUpload_vue-resour-910d0c.css",
|
"assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_utils_toast_css-resources_js_Com-0c4b04.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_utils_toast_css-resources_js_Com-0c4b04.css",
|
||||||
"assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_Components_FileUpload_vue-resour-910d0c.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_Components_FileUpload_vue-resour-910d0c.js",
|
"assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_utils_toast_css-resources_js_Com-0c4b04.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_utils_toast_css-resources_js_Com-0c4b04.js",
|
||||||
"assets/resources_js_Pages_Submitter_Dataset_Delete_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Delete_vue.js",
|
"assets/resources_js_Pages_Submitter_Dataset_Delete_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Delete_vue.js",
|
||||||
"assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_Components_FileUpload_vue-resource-b447ba.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_Components_FileUpload_vue-resource-b447ba.css",
|
"assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-b6a1eb.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-b6a1eb.css",
|
||||||
"assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_Components_FileUpload_vue-resource-b447ba.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_Components_FileUpload_vue-resource-b447ba.js",
|
"assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-b6a1eb.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-b6a1eb.js",
|
||||||
"assets/resources_js_Pages_Submitter_Dataset_Index_vue.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Index_vue.css",
|
"assets/resources_js_Pages_Submitter_Dataset_Index_vue.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Index_vue.css",
|
||||||
"assets/resources_js_Pages_Submitter_Dataset_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Index_vue.js",
|
"assets/resources_js_Pages_Submitter_Dataset_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Index_vue.js",
|
||||||
"assets/resources_js_Pages_Submitter_Dataset_Release_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Release_vue.js",
|
"assets/resources_js_Pages_Submitter_Dataset_Release_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Release_vue.js",
|
||||||
|
@ -49,11 +49,10 @@
|
||||||
"assets/vendors-node_modules_mdi_js_mdi_js-node_modules_vue-loader_dist_exportHelper_js.js": "http://localhost:8080/assets/vendors-node_modules_mdi_js_mdi_js-node_modules_vue-loader_dist_exportHelper_js.js",
|
"assets/vendors-node_modules_mdi_js_mdi_js-node_modules_vue-loader_dist_exportHelper_js.js": "http://localhost:8080/assets/vendors-node_modules_mdi_js_mdi_js-node_modules_vue-loader_dist_exportHelper_js.js",
|
||||||
"assets/vendors-node_modules_focus-trap_dist_focus-trap_esm_js-node_modules_notiwind_dist_index_esm_js.js": "http://localhost:8080/assets/vendors-node_modules_focus-trap_dist_focus-trap_esm_js-node_modules_notiwind_dist_index_esm_js.js",
|
"assets/vendors-node_modules_focus-trap_dist_focus-trap_esm_js-node_modules_notiwind_dist_index_esm_js.js": "http://localhost:8080/assets/vendors-node_modules_focus-trap_dist_focus-trap_esm_js-node_modules_notiwind_dist_index_esm_js.js",
|
||||||
"assets/vendors-node_modules_vue-facing-decorator_dist_esm_utils_js.js": "http://localhost:8080/assets/vendors-node_modules_vue-facing-decorator_dist_esm_utils_js.js",
|
"assets/vendors-node_modules_vue-facing-decorator_dist_esm_utils_js.js": "http://localhost:8080/assets/vendors-node_modules_vue-facing-decorator_dist_esm_utils_js.js",
|
||||||
"assets/vendors-node_modules_leaflet_src_control_Control_Attribution_js-node_modules_leaflet_src_laye-fbc1b4.js": "http://localhost:8080/assets/vendors-node_modules_leaflet_src_control_Control_Attribution_js-node_modules_leaflet_src_laye-fbc1b4.js",
|
"assets/vendors-node_modules_leaflet_dist_leaflet-src_js-node_modules_leaflet_src_control_Control_Att-4ee9a6.js": "http://localhost:8080/assets/vendors-node_modules_leaflet_dist_leaflet-src_js-node_modules_leaflet_src_control_Control_Att-4ee9a6.js",
|
||||||
|
"assets/vendors-node_modules_toastify-js_src_toastify_js.js": "http://localhost:8080/assets/vendors-node_modules_toastify-js_src_toastify_js.js",
|
||||||
"assets/vendors-node_modules_buffer_index_js-node_modules_leaflet_src_layer_tile_TileLayer_WMS_js-nod-e7bc71.js": "http://localhost:8080/assets/vendors-node_modules_buffer_index_js-node_modules_leaflet_src_layer_tile_TileLayer_WMS_js-nod-e7bc71.js",
|
"assets/vendors-node_modules_buffer_index_js-node_modules_leaflet_src_layer_tile_TileLayer_WMS_js-nod-e7bc71.js": "http://localhost:8080/assets/vendors-node_modules_buffer_index_js-node_modules_leaflet_src_layer_tile_TileLayer_WMS_js-nod-e7bc71.js",
|
||||||
"assets/vendors-node_modules_numeral_numeral_js-node_modules_chart_js_dist_chart_js.js": "http://localhost:8080/assets/vendors-node_modules_numeral_numeral_js-node_modules_chart_js_dist_chart_js.js",
|
"assets/vendors-node_modules_numeral_numeral_js-node_modules_chart_js_dist_chart_js.js": "http://localhost:8080/assets/vendors-node_modules_numeral_numeral_js-node_modules_chart_js_dist_chart_js.js",
|
||||||
"assets/vendors-node_modules_leaflet_dist_leaflet-src_js-node_modules_leaflet_src_layer_LayerGroup_js.js": "http://localhost:8080/assets/vendors-node_modules_leaflet_dist_leaflet-src_js-node_modules_leaflet_src_layer_LayerGroup_js.js",
|
|
||||||
"assets/vendors-node_modules_toastify-js_src_toastify_js.js": "http://localhost:8080/assets/vendors-node_modules_toastify-js_src_toastify_js.js",
|
|
||||||
"assets/resources_js_Components_BaseButton_vue.js": "http://localhost:8080/assets/resources_js_Components_BaseButton_vue.js",
|
"assets/resources_js_Components_BaseButton_vue.js": "http://localhost:8080/assets/resources_js_Components_BaseButton_vue.js",
|
||||||
"assets/resources_js_Stores_main_ts-resources_js_Components_BaseDivider_vue-resources_js_Components_C-b45805.js": "http://localhost:8080/assets/resources_js_Stores_main_ts-resources_js_Components_BaseDivider_vue-resources_js_Components_C-b45805.js",
|
"assets/resources_js_Stores_main_ts-resources_js_Components_BaseDivider_vue-resources_js_Components_C-b45805.js": "http://localhost:8080/assets/resources_js_Stores_main_ts-resources_js_Components_BaseDivider_vue-resources_js_Components_C-b45805.js",
|
||||||
"assets/resources_js_Components_SectionMain_vue-resources_js_Layouts_LayoutAuthenticated_vue.css": "http://localhost:8080/assets/resources_js_Components_SectionMain_vue-resources_js_Layouts_LayoutAuthenticated_vue.css",
|
"assets/resources_js_Components_SectionMain_vue-resources_js_Layouts_LayoutAuthenticated_vue.css": "http://localhost:8080/assets/resources_js_Components_SectionMain_vue-resources_js_Layouts_LayoutAuthenticated_vue.css",
|
||||||
|
@ -61,9 +60,10 @@
|
||||||
"assets/resources_js_Components_BaseButtons_vue-resources_js_Components_FormControl_vue-resources_js_-d830d6.js": "http://localhost:8080/assets/resources_js_Components_BaseButtons_vue-resources_js_Components_FormControl_vue-resources_js_-d830d6.js",
|
"assets/resources_js_Components_BaseButtons_vue-resources_js_Components_FormControl_vue-resources_js_-d830d6.js": "http://localhost:8080/assets/resources_js_Components_BaseButtons_vue-resources_js_Components_FormControl_vue-resources_js_-d830d6.js",
|
||||||
"assets/resources_js_Components_Admin_Pagination_vue-resources_js_Components_BaseButtons_vue-resource-8134e7.js": "http://localhost:8080/assets/resources_js_Components_Admin_Pagination_vue-resources_js_Components_BaseButtons_vue-resource-8134e7.js",
|
"assets/resources_js_Components_Admin_Pagination_vue-resources_js_Components_BaseButtons_vue-resource-8134e7.js": "http://localhost:8080/assets/resources_js_Components_Admin_Pagination_vue-resources_js_Components_BaseButtons_vue-resource-8134e7.js",
|
||||||
"assets/resources_js_Components_Map_draw_component_vue-resources_js_Components_Map_zoom_component_vue-196747.js": "http://localhost:8080/assets/resources_js_Components_Map_draw_component_vue-resources_js_Components_Map_zoom_component_vue-196747.js",
|
"assets/resources_js_Components_Map_draw_component_vue-resources_js_Components_Map_zoom_component_vue-196747.js": "http://localhost:8080/assets/resources_js_Components_Map_draw_component_vue-resources_js_Components_Map_zoom_component_vue-196747.js",
|
||||||
|
"assets/resources_js_utils_toast_ts-resources_js_Components_FormValidationErrors_vue-resources_js_Com-a6b99f.js": "http://localhost:8080/assets/resources_js_utils_toast_ts-resources_js_Components_FormValidationErrors_vue-resources_js_Com-a6b99f.js",
|
||||||
"assets/resources_js_Components_Admin_Sort_vue-resources_js_Components_SectionTitleLineWithButton_vue.js": "http://localhost:8080/assets/resources_js_Components_Admin_Sort_vue-resources_js_Components_SectionTitleLineWithButton_vue.js",
|
"assets/resources_js_Components_Admin_Sort_vue-resources_js_Components_SectionTitleLineWithButton_vue.js": "http://localhost:8080/assets/resources_js_Components_Admin_Sort_vue-resources_js_Components_SectionTitleLineWithButton_vue.js",
|
||||||
"assets/resources_js_Components_CardBoxModal_vue.js": "http://localhost:8080/assets/resources_js_Components_CardBoxModal_vue.js",
|
"assets/resources_js_Components_CardBoxModal_vue.js": "http://localhost:8080/assets/resources_js_Components_CardBoxModal_vue.js",
|
||||||
"assets/resources_js_Components_FileUpload_vue-resources_js_Components_FormCheckRadioGroup_vue-resour-d5e5fc.js": "http://localhost:8080/assets/resources_js_Components_FileUpload_vue-resources_js_Components_FormCheckRadioGroup_vue-resour-d5e5fc.js",
|
"assets/resources_js_Components_FileUpload_vue-resources_js_Components_FormCheckRadioGroup_vue-resour-bdf2f9.js": "http://localhost:8080/assets/resources_js_Components_FileUpload_vue-resources_js_Components_FormCheckRadioGroup_vue-resour-bdf2f9.js",
|
||||||
"assets/fonts/inter-latin-ext-400-normal.woff": "http://localhost:8080/assets/fonts/inter-latin-ext-400-normal.40b3b0d5.woff",
|
"assets/fonts/inter-latin-ext-400-normal.woff": "http://localhost:8080/assets/fonts/inter-latin-ext-400-normal.40b3b0d5.woff",
|
||||||
"assets/fonts/inter-latin-ext-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-latin-ext-400-normal.0f9e8d4e.woff2",
|
"assets/fonts/inter-latin-ext-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-latin-ext-400-normal.0f9e8d4e.woff2",
|
||||||
"assets/fonts/inter-latin-400-normal.woff": "http://localhost:8080/assets/fonts/inter-latin-400-normal.08a02fd2.woff",
|
"assets/fonts/inter-latin-400-normal.woff": "http://localhost:8080/assets/fonts/inter-latin-400-normal.08a02fd2.woff",
|
||||||
|
|
|
@ -4,13 +4,10 @@
|
||||||
<fa-icon [icon]="faSearchLocation"></fa-icon>
|
<fa-icon [icon]="faSearchLocation"></fa-icon>
|
||||||
</button> -->
|
</button> -->
|
||||||
<!-- -->
|
<!-- -->
|
||||||
<button
|
<button ref="inputDraw"
|
||||||
ref="inputDraw"
|
|
||||||
class="inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 text-black border-teal-50 hover:bg-gray-200 text-sm p-1"
|
class="inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 text-black border-teal-50 hover:bg-gray-200 text-sm p-1"
|
||||||
type="button"
|
type="button" :class="[_enabled ? 'cursor-not-allowed bg-cyan-200' : 'bg-teal-50 is-active']"
|
||||||
:class="[_enabled ? 'cursor-not-allowed bg-cyan-200' : 'bg-teal-50 is-active']"
|
@click.prevent="toggleDraw">
|
||||||
@click.prevent="toggleDraw"
|
|
||||||
>
|
|
||||||
<BaseIcon v-if="mdiDrawPen" :path="mdiDrawPen" />
|
<BaseIcon v-if="mdiDrawPen" :path="mdiDrawPen" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -28,6 +25,8 @@ import { Map } from 'leaflet/src/map/index';
|
||||||
import { on, off, preventDefault } from 'leaflet/src/dom/DomEvent';
|
import { on, off, preventDefault } from 'leaflet/src/dom/DomEvent';
|
||||||
import { Rectangle } from 'leaflet/src/layer/vector/Rectangle';
|
import { Rectangle } from 'leaflet/src/layer/vector/Rectangle';
|
||||||
import { LatLngBounds } from 'leaflet/src/geo/LatLngBounds';
|
import { LatLngBounds } from 'leaflet/src/geo/LatLngBounds';
|
||||||
|
import { LatLng } from 'leaflet';
|
||||||
|
import { LeafletMouseEvent } from 'leaflet';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
name: 'draw-control',
|
name: 'draw-control',
|
||||||
|
@ -58,19 +57,19 @@ export default class DrawControlComponent extends Vue {
|
||||||
|
|
||||||
@Prop() public mapId: string;
|
@Prop() public mapId: string;
|
||||||
// @Prop() public map: Map;
|
// @Prop() public map: Map;
|
||||||
@Prop public southWest: LatLngBounds;
|
@Prop public southWest: LatLng;
|
||||||
@Prop public northEast: LatLngBounds;
|
@Prop public northEast: LatLng;
|
||||||
@Prop({
|
@Prop({
|
||||||
default: true,
|
default: true,
|
||||||
})
|
})
|
||||||
public preserve: boolean;
|
public preserve: boolean;
|
||||||
|
|
||||||
mapService = MapService();
|
mapService = MapService();
|
||||||
public _enabled;
|
public _enabled: boolean;
|
||||||
private _map: Map;
|
private _map: Map;
|
||||||
private _isDrawing: boolean = false;
|
private _isDrawing: boolean = false;
|
||||||
private _startLatLng;
|
private _startLatLng: LatLng;
|
||||||
private _mapDraggable;
|
private _mapDraggable: boolean;
|
||||||
private _shape: Rectangle | undefined;
|
private _shape: Rectangle | undefined;
|
||||||
|
|
||||||
enable() {
|
enable() {
|
||||||
|
@ -80,6 +79,7 @@ export default class DrawControlComponent extends Vue {
|
||||||
|
|
||||||
this._enabled = true;
|
this._enabled = true;
|
||||||
this.addHooks();
|
this.addHooks();
|
||||||
|
this._map.control = this;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -111,6 +111,7 @@ export default class DrawControlComponent extends Vue {
|
||||||
// this._map.domElement.style.cursor = 'crosshair';
|
// this._map.domElement.style.cursor = 'crosshair';
|
||||||
this._map._container.style.cursor = 'crosshair';
|
this._map._container.style.cursor = 'crosshair';
|
||||||
// this._tooltip.updateContent({text: this._initialLabelText});
|
// this._tooltip.updateContent({text: this._initialLabelText});
|
||||||
|
|
||||||
this._map
|
this._map
|
||||||
.on('mousedown', this._onMouseDown, this)
|
.on('mousedown', this._onMouseDown, this)
|
||||||
.on('mousemove', this._onMouseMove, this)
|
.on('mousemove', this._onMouseMove, this)
|
||||||
|
@ -157,7 +158,7 @@ export default class DrawControlComponent extends Vue {
|
||||||
this._isDrawing = false;
|
this._isDrawing = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onMouseDown(e) {
|
private _onMouseDown(e: LeafletMouseEvent) {
|
||||||
this._isDrawing = true;
|
this._isDrawing = true;
|
||||||
this._startLatLng = e.latlng;
|
this._startLatLng = e.latlng;
|
||||||
|
|
||||||
|
@ -169,7 +170,7 @@ export default class DrawControlComponent extends Vue {
|
||||||
preventDefault(e.originalEvent);
|
preventDefault(e.originalEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _onMouseMove(e) {
|
private _onMouseMove(e: LeafletMouseEvent) {
|
||||||
var latlng = e.latlng;
|
var latlng = e.latlng;
|
||||||
|
|
||||||
// this._tooltip.updatePosition(latlng);
|
// this._tooltip.updatePosition(latlng);
|
||||||
|
@ -191,13 +192,21 @@ export default class DrawControlComponent extends Vue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private _fireCreatedEvent(shape) {
|
private _fireCreatedEvent(shape: Rectangle) {
|
||||||
var rectangle = new Rectangle(shape.getBounds(), this.options.shapeOptions);
|
var rectangle = new Rectangle(shape.getBounds(), this.options.shapeOptions);
|
||||||
// L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, rectangle);
|
// L.Draw.SimpleShape.prototype._fireCreatedEvent.call(this, rectangle);
|
||||||
this._map.fire('Draw.Event.CREATED', { layer: rectangle, type: this.TYPE });
|
this._map.fire('Draw.Event.CREATED', { layer: rectangle, type: this.TYPE });
|
||||||
}
|
}
|
||||||
|
|
||||||
public drawShape(southWest, northEast) {
|
public removeShape() {
|
||||||
|
if (this._shape) {
|
||||||
|
this._map.removeLayer(this._shape);
|
||||||
|
// delete this._shape;
|
||||||
|
this._shape = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public drawShape(southWest: LatLng, northEast: LatLng) {
|
||||||
if (!this._shape) {
|
if (!this._shape) {
|
||||||
const bounds = new LatLngBounds(southWest, northEast);
|
const bounds = new LatLngBounds(southWest, northEast);
|
||||||
this._shape = new Rectangle(bounds, this.options.shapeOptions);
|
this._shape = new Rectangle(bounds, this.options.shapeOptions);
|
||||||
|
@ -210,7 +219,7 @@ export default class DrawControlComponent extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
// from Draw Rectangle
|
// from Draw Rectangle
|
||||||
private _drawShape(latlng) {
|
private _drawShape(latlng: LatLng) {
|
||||||
if (!this._shape) {
|
if (!this._shape) {
|
||||||
const bounds = new LatLngBounds(this._startLatLng, latlng);
|
const bounds = new LatLngBounds(this._startLatLng, latlng);
|
||||||
this._shape = new Rectangle(bounds, this.options.shapeOptions);
|
this._shape = new Rectangle(bounds, this.options.shapeOptions);
|
||||||
|
|
|
@ -7,22 +7,32 @@
|
||||||
<DrawControlComponent ref="draw" :mapId="mapId" :southWest="southWest" :northEast="northEast" />
|
<DrawControlComponent ref="draw" :mapId="mapId" :southWest="southWest" :northEast="northEast" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="gba-control-validate btn-group-vertical">
|
||||||
|
<button
|
||||||
|
class="min-w-27 inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 text-black text-sm p-1"
|
||||||
|
type="button"
|
||||||
|
@click.stop.prevent="validateBoundingBox"
|
||||||
|
:class="[validBoundingBox ? 'cursor-not-allowed bg-green-500 is-active' : 'bg-red-500 ']"
|
||||||
|
>
|
||||||
|
<!-- <BaseIcon v-if="mdiMapCheckOutline" :path="mdiMapCheckOutline" /> -->
|
||||||
|
{{ label }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { EventEmitter } from './EventEmitter';
|
import { EventEmitter } from './EventEmitter';
|
||||||
import { Component, Vue, Prop, Ref } from 'vue-facing-decorator';
|
import { Component, Vue, Prop, Ref } from 'vue-facing-decorator';
|
||||||
// import type { Coverage } from '@/Dataset';
|
|
||||||
// import { Map, Control, MapOptions, LatLngBoundsExpression, tileLayer, latLng, latLngBounds, FeatureGroup } from 'leaflet';
|
|
||||||
import { Map } from 'leaflet/src/map/index';
|
import { Map } from 'leaflet/src/map/index';
|
||||||
import { Control } from 'leaflet/src/control/Control';
|
import { Control } from 'leaflet/src/control/Control';
|
||||||
import { LatLngBoundsExpression, toLatLngBounds } from 'leaflet/src/geo/LatLngBounds';
|
import { LatLngBoundsExpression, LatLngBounds } from 'leaflet/src/geo/LatLngBounds';
|
||||||
import { toLatLng } from 'leaflet/src/geo/LatLng';
|
// import { toLatLng } from 'leaflet/src/geo/LatLng';
|
||||||
|
import { LatLng } from 'leaflet'; //'leaflet/src/geo/LatLng';
|
||||||
import { tileLayerWMS } from 'leaflet/src/layer/tile/TileLayer.WMS';
|
import { tileLayerWMS } from 'leaflet/src/layer/tile/TileLayer.WMS';
|
||||||
import { Attribution } from 'leaflet/src/control/Control.Attribution';
|
import { Attribution } from 'leaflet/src/control/Control.Attribution';
|
||||||
// import { Attribution } from 'leaflet';
|
import { mdiMapCheckOutline } from '@mdi/js';
|
||||||
// import { FeatureGroup } from 'leaflet/src/layer/FeatureGroup';
|
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
|
|
||||||
import { MapOptions } from './MapOptions';
|
import { MapOptions } from './MapOptions';
|
||||||
import { LayerOptions, LayerMap } from './LayerOptions';
|
import { LayerOptions, LayerMap } from './LayerOptions';
|
||||||
|
@ -32,6 +42,7 @@ import DrawControlComponent from './draw.component.vue';
|
||||||
import { Coverage } from '@/Dataset';
|
import { Coverage } from '@/Dataset';
|
||||||
import { canvas } from 'leaflet/src/layer/vector/Canvas';
|
import { canvas } from 'leaflet/src/layer/vector/Canvas';
|
||||||
import { svg } from 'leaflet/src/layer/vector/SVG';
|
import { svg } from 'leaflet/src/layer/vector/SVG';
|
||||||
|
import Notification from '@/utils/toast';
|
||||||
|
|
||||||
Map.include({
|
Map.include({
|
||||||
// @namespace Map; @method getRenderer(layer: Path): Renderer
|
// @namespace Map; @method getRenderer(layer: Path): Renderer
|
||||||
|
@ -84,6 +95,7 @@ const DEFAULT_BASE_LAYER_ATTRIBUTION = '© <a target="_blank" href="http://o
|
||||||
components: {
|
components: {
|
||||||
ZoomControlComponent,
|
ZoomControlComponent,
|
||||||
DrawControlComponent,
|
DrawControlComponent,
|
||||||
|
BaseIcon,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class MapComponent extends Vue {
|
export default class MapComponent extends Vue {
|
||||||
|
@ -119,14 +131,40 @@ export default class MapComponent extends Vue {
|
||||||
@Prop()
|
@Prop()
|
||||||
public baseMaps: LayerMap;
|
public baseMaps: LayerMap;
|
||||||
|
|
||||||
|
get label(): string {
|
||||||
|
return this.validBoundingBox ? ' valid' : 'invalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
get validBoundingBox(): boolean {
|
||||||
|
let isValidNumber =
|
||||||
|
(typeof this.coverage.x_min === 'number' || !isNaN(Number(this.coverage.x_min))) &&
|
||||||
|
(typeof this.coverage.y_min === 'number' || !isNaN(Number(this.coverage.y_min))) &&
|
||||||
|
(typeof this.coverage.x_max === 'number' || !isNaN(Number(this.coverage.x_max))) &&
|
||||||
|
(typeof this.coverage.y_max === 'number' || !isNaN(Number(this.coverage.y_max)));
|
||||||
|
|
||||||
|
let isBoundValid = true;
|
||||||
|
if (isValidNumber) {
|
||||||
|
let _southWest: LatLng = new LatLng(this.coverage.y_min, this.coverage.x_min);
|
||||||
|
let _northEast: LatLng = new LatLng(this.coverage.y_max, this.coverage.x_max);
|
||||||
|
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
||||||
|
if (!bounds.isValid() || !(_southWest.lat < _northEast.lat && _southWest.lng < _northEast.lng)) {
|
||||||
|
// this.draw.removeShape();
|
||||||
|
// Notification.showTemporary('Bounds are not valid.');
|
||||||
|
isBoundValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return isValidNumber && isBoundValid;
|
||||||
|
}
|
||||||
|
|
||||||
@Ref('zoom') private zoom: ZoomControlComponent;
|
@Ref('zoom') private zoom: ZoomControlComponent;
|
||||||
@Ref('draw') private draw: DrawControlComponent;
|
@Ref('draw') private draw: DrawControlComponent;
|
||||||
|
|
||||||
// services:
|
// services:
|
||||||
mapService = MapService();
|
mapService = MapService();
|
||||||
|
|
||||||
southWest;
|
mdiMapCheckOutline = mdiMapCheckOutline;
|
||||||
northEast;
|
southWest: LatLng;
|
||||||
|
northEast: LatLng;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Informs when initialization is done with map id.
|
* Informs when initialization is done with map id.
|
||||||
|
@ -136,8 +174,65 @@ export default class MapComponent extends Vue {
|
||||||
public map!: Map;
|
public map!: Map;
|
||||||
// protected drawnItems!: FeatureGroup<any>;
|
// protected drawnItems!: FeatureGroup<any>;
|
||||||
|
|
||||||
// @Prop({ type: Object })
|
validateBoundingBox() {
|
||||||
// geolocation: Coverage;
|
if (this.validBoundingBox == false) {
|
||||||
|
this.draw.removeShape();
|
||||||
|
Notification.showError('Bounds are not valid.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.map.control && this.map.control.disable();
|
||||||
|
var _this = this;
|
||||||
|
// // _this.locationErrors.length = 0;
|
||||||
|
// this.drawnItems.clearLayers();
|
||||||
|
// //var xmin = document.getElementById("xmin").value;
|
||||||
|
// var xmin = (<HTMLInputElement>document.getElementById("xmin")).value;
|
||||||
|
// // var ymin = document.getElementById("ymin").value;
|
||||||
|
// var ymin = (<HTMLInputElement>document.getElementById("ymin")).value;
|
||||||
|
// //var xmax = document.getElementById("xmax").value;
|
||||||
|
// var xmax = (<HTMLInputElement>document.getElementById("xmax")).value;
|
||||||
|
// //var ymax = document.getElementById("ymax").value;
|
||||||
|
// var ymax = (<HTMLInputElement>document.getElementById("ymax")).value;
|
||||||
|
// var bounds = [[ymin, xmin], [ymax, xmax]];
|
||||||
|
|
||||||
|
// let _southWest: LatLng;
|
||||||
|
// let _northEast: LatLng;
|
||||||
|
// if (this.coverage.x_min && this.coverage.y_min) {
|
||||||
|
let _southWest: LatLng = new LatLng(this.coverage.y_min, this.coverage.x_min);
|
||||||
|
// }
|
||||||
|
// if (this.coverage.x_max && this.coverage.y_max) {
|
||||||
|
let _northEast: LatLng = new LatLng(this.coverage.y_max, this.coverage.x_max);
|
||||||
|
// }
|
||||||
|
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
||||||
|
if (!bounds.isValid() || !(_southWest.lat < _northEast.lat && _southWest.lng < _northEast.lng)) {
|
||||||
|
this.draw.removeShape();
|
||||||
|
Notification.showTemporary('Bounds are not valid.');
|
||||||
|
} else {
|
||||||
|
// this.draw.drawShape(_southWest, _northEast);
|
||||||
|
try {
|
||||||
|
this.draw.drawShape(_southWest, _northEast);
|
||||||
|
_this.map.fitBounds(bounds);
|
||||||
|
|
||||||
|
// var boundingBox = L.rectangle(bounds, { color: "#005F6A", weight: 1 });
|
||||||
|
// // this.geolocation.xmin = xmin;
|
||||||
|
// // this.geolocation.ymin = ymin;
|
||||||
|
// // this.geolocation.xmax = xmax;
|
||||||
|
// // this.geolocation.ymax = ymax;
|
||||||
|
|
||||||
|
// _this.drawnItems.addLayer(boundingBox);
|
||||||
|
// _this.map.fitBounds(bounds);
|
||||||
|
// this.options.message = "valid bounding box";
|
||||||
|
// this.$toast.success("valid bounding box", this.options);
|
||||||
|
Notification.showSuccess('valid bounding box');
|
||||||
|
} catch (err) {
|
||||||
|
// this.options.message = e.message;
|
||||||
|
// // _this.errors.push(e);
|
||||||
|
// this.$toast.error(e.message, this.options);
|
||||||
|
Notification.showTemporary('An error occurred while drawing bounding box');
|
||||||
|
// generatingCodes.value = false;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
mounted(): void {
|
mounted(): void {
|
||||||
this.initMap();
|
this.initMap();
|
||||||
|
@ -195,26 +290,26 @@ export default class MapComponent extends Vue {
|
||||||
// this.map.fitBounds(this.fitBounds);
|
// this.map.fitBounds(this.fitBounds);
|
||||||
// }
|
// }
|
||||||
if (this.coverage.x_min && this.coverage.y_min) {
|
if (this.coverage.x_min && this.coverage.y_min) {
|
||||||
this.southWest = toLatLng(this.coverage.y_min, this.coverage.x_min);
|
this.southWest = new LatLng(this.coverage.y_min, this.coverage.x_min);
|
||||||
} else {
|
} else {
|
||||||
this.southWest = toLatLng(46.5, 9.9);
|
this.southWest = new LatLng(46.5, 9.9);
|
||||||
}
|
}
|
||||||
if (this.coverage.x_max && this.coverage.y_max) {
|
if (this.coverage.x_max && this.coverage.y_max) {
|
||||||
this.northEast = toLatLng(this.coverage.y_max, this.coverage.x_max);
|
this.northEast = new LatLng(this.coverage.y_max, this.coverage.x_max);
|
||||||
} else {
|
} else {
|
||||||
this.northEast = toLatLng(48.9, 16.9);
|
this.northEast = new LatLng(48.9, 16.9);
|
||||||
} // this.northEast = toLatLng(48.9, 16.9);
|
} // this.northEast = toLatLng(48.9, 16.9);
|
||||||
const bounds = toLatLngBounds(this.southWest, this.northEast);
|
const bounds = new LatLngBounds(this.southWest, this.northEast);
|
||||||
map.fitBounds(bounds);
|
map.fitBounds(bounds);
|
||||||
|
|
||||||
if (this.coverage.x_min && this.coverage.x_max && this.coverage.y_min && this.coverage.y_max) {
|
if (this.coverage.x_min && this.coverage.x_max && this.coverage.y_min && this.coverage.y_max) {
|
||||||
let _southWest;
|
let _southWest: LatLng;
|
||||||
let _northEast;
|
let _northEast: LatLng;
|
||||||
if (this.coverage.x_min && this.coverage.y_min) {
|
if (this.coverage.x_min && this.coverage.y_min) {
|
||||||
_southWest = toLatLng(this.coverage.y_min, this.coverage.x_min);
|
_southWest = new LatLng(this.coverage.y_min, this.coverage.x_min);
|
||||||
}
|
}
|
||||||
if (this.coverage.x_max && this.coverage.y_max) {
|
if (this.coverage.x_max && this.coverage.y_max) {
|
||||||
_northEast = toLatLng(this.coverage.y_max, this.coverage.x_max);
|
_northEast = new LatLng(this.coverage.y_max, this.coverage.x_max);
|
||||||
}
|
}
|
||||||
this.draw.drawShape(_southWest, _northEast);
|
this.draw.drawShape(_southWest, _northEast);
|
||||||
}
|
}
|
||||||
|
@ -257,6 +352,26 @@ export default class MapComponent extends Vue {
|
||||||
background: none;
|
background: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gba-control-validate {
|
||||||
|
-webkit-user-select: none;
|
||||||
|
-moz-user-select: none;
|
||||||
|
-ms-user-select: none;
|
||||||
|
user-select: none;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: 4px;
|
||||||
|
position: absolute;
|
||||||
|
left: 10px;
|
||||||
|
top: 150px;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-group-vertical button {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
/* .leaflet-pane {
|
/* .leaflet-pane {
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
} */
|
} */
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
ref="inputPlus"
|
ref="inputPlus"
|
||||||
class="disabled:bg-gray-200 inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 bg-teal-50 text-black border-teal-50 text-sm p-1"
|
class="disabled:bg-gray-200 inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 bg-teal-50 text-black border-teal-50 text-sm p-1"
|
||||||
type="button"
|
type="button"
|
||||||
@click.prevent="zoomIn"
|
@click.stop.prevent="zoomIn"
|
||||||
>
|
>
|
||||||
<BaseIcon v-if="mdiPlus" :path="mdiPlus" />
|
<BaseIcon v-if="mdiPlus" :path="mdiPlus" />
|
||||||
</button>
|
</button>
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
ref="inputMinus"
|
ref="inputMinus"
|
||||||
class="disabled:bg-gray-200 inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 bg-teal-50 text-black border-teal-50 text-sm p-1"
|
class="disabled:bg-gray-200 inline-flex cursor-pointer justify-center items-center whitespace-nowrap focus:outline-none transition-colors duration-150 border rounded ring-blue-700 bg-teal-50 text-black border-teal-50 text-sm p-1"
|
||||||
type="button"
|
type="button"
|
||||||
@click.prevent="zoomOut"
|
@click.stop.prevent="zoomOut"
|
||||||
>
|
>
|
||||||
<BaseIcon v-if="mdiMinus" :path="mdiMinus" />
|
<BaseIcon v-if="mdiMinus" :path="mdiMinus" />
|
||||||
</button>
|
</button>
|
||||||
|
|
152
resources/js/Components/PersonalSettings.vue
Normal file
152
resources/js/Components/PersonalSettings.vue
Normal file
|
@ -0,0 +1,152 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<BaseButton v-if="!enabled" id="generate-backup-codes" class="mx-2" :icon="mdiContentSaveCheck" type="button"
|
||||||
|
color="info" :class="{ 'icon-loading-small': generatingCodes }" :disabled="generatingCodes"
|
||||||
|
label=" Generate backup codes" @click="generateBackupCodes" />
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
|
||||||
|
<template v-if="!haveCodes">
|
||||||
|
{{ `Backup codes have been generated. ${used} of ${total} codes have been used.` }}
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div>
|
||||||
|
These are your backup codes. Please save and/or print them as you will not be able to read the codes
|
||||||
|
again later
|
||||||
|
<ul>
|
||||||
|
<li v-for="code in codes" :key="code" class="backup-code">
|
||||||
|
{{ code }}
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<BaseButton :href="downloadUrl" class="mt-2 mb-2" :download="downloadFilename" rounded-full small
|
||||||
|
:icon="mdiContentSave" :label="'Save backup codes'">
|
||||||
|
</BaseButton>
|
||||||
|
<BaseButton @click="printCodes" rounded-full small :icon="mdiContentSave"
|
||||||
|
:label="'Print backup codes'">
|
||||||
|
</BaseButton>
|
||||||
|
<!-- <button class="button" @click="printCodes">
|
||||||
|
{{ t('twofactor_backupcodes', 'Print backup codes') }}
|
||||||
|
</button> -->
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="mt-4 max-w-xl text-sm text-gray-600">
|
||||||
|
<BaseButton class="mt-2 mb-2" :icon="mdiContentSaveCheck" type="button" color="info"
|
||||||
|
:disabled="generatingCodes" label="Regenerate backup codes" @click="generateBackupCodes" />
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
<em> 'twofactor_backupcodes', `If you regenerate backup codes, you automatically invalidate old codes.`
|
||||||
|
</em>
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, Ref } from 'vue';
|
||||||
|
import { ComputedRef } from 'vue';
|
||||||
|
import { MainService } from '@/Stores/main';
|
||||||
|
import Notification from '@/utils/toast';
|
||||||
|
import { mdiContentSaveCheck, mdiContentSave } from '@mdi/js';
|
||||||
|
import BaseButton from '@/Components/BaseButton.vue';
|
||||||
|
// import { useI18n } from 'vue-i18n';
|
||||||
|
// import { confirmPassword } from '@nextcloud/password-confirmation'
|
||||||
|
// import '@nextcloud/password-confirmation/dist/style.css'
|
||||||
|
// import { print } from '../service/PrintService.js'
|
||||||
|
// const { t } = useI18n();
|
||||||
|
|
||||||
|
const generatingCodes: Ref<boolean> = ref(false);
|
||||||
|
const mainService = MainService();
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
backupState: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (props.backupState.enabled) {
|
||||||
|
mainService.backupcodesEnabled = true;
|
||||||
|
mainService.total = props.backupState.total;
|
||||||
|
mainService.used = props.backupState.used;
|
||||||
|
}
|
||||||
|
|
||||||
|
const enabled = computed(() => mainService.backupcodesEnabled);
|
||||||
|
const total = computed(() => mainService.total);
|
||||||
|
const used = computed(() => mainService.used);
|
||||||
|
const codes: ComputedRef<string[]> = computed(() => mainService.codes);
|
||||||
|
const haveCodes = computed(() => {
|
||||||
|
return codes && codes.value.length > 0;
|
||||||
|
});
|
||||||
|
const downloadFilename = computed(() => {
|
||||||
|
return 'tethys-backup-codes.txt';
|
||||||
|
});
|
||||||
|
const downloadUrl = computed(() => {
|
||||||
|
if (!codes) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
'data:text/plain,' +
|
||||||
|
encodeURIComponent(
|
||||||
|
codes.value.reduce((prev, code) => {
|
||||||
|
return prev + code + '\r\n';
|
||||||
|
}, ''),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const print = (data: any) => {
|
||||||
|
const name = 'Tethys';
|
||||||
|
// const newTab = window.open('', `${name} backup codes`)
|
||||||
|
const newTab = window.open('about:blank', `${name} backup codes`);
|
||||||
|
if (newTab) {
|
||||||
|
newTab.document.write('<h1>' + `${name} backup codes` + '</h1>');
|
||||||
|
newTab.document.write('<pre>' + data + '</pre>');
|
||||||
|
newTab.print();
|
||||||
|
newTab.close();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const getPrintData = (codes: string[]) => {
|
||||||
|
if (!codes) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return codes.reduce((prev, code) => {
|
||||||
|
return prev + code + '<br>';
|
||||||
|
}, '');
|
||||||
|
};
|
||||||
|
|
||||||
|
const printCodes = () => {
|
||||||
|
const data = getPrintData(codes.value);
|
||||||
|
print(data);
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateBackupCodes = async () => {
|
||||||
|
// Hide old codes
|
||||||
|
generatingCodes.value = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mainService.generate();
|
||||||
|
generatingCodes.value = false;
|
||||||
|
} catch (err) {
|
||||||
|
Notification.showTemporary('An error occurred while generating your backup codes');
|
||||||
|
generatingCodes.value = false;
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
// this.$store.dispatch('generate').then(data => {
|
||||||
|
// this.generatingCodes = false
|
||||||
|
// }).catch(err => {
|
||||||
|
// OC.Notification.showTemporary(t('twofactor_backupcodes', 'An error occurred while generating your backup codes'))
|
||||||
|
// this.generatingCodes = false
|
||||||
|
// throw err
|
||||||
|
// })
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.backup-code {
|
||||||
|
font-family: monospace;
|
||||||
|
letter-spacing: 0.02em;
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -44,6 +44,8 @@
|
||||||
:loading="loadingConfirmation" v-model:confirmation="confirmationCode" @confirm="enableTOTP" />
|
:loading="loadingConfirmation" v-model:confirmation="confirmationCode" @confirm="enableTOTP" />
|
||||||
|
|
||||||
|
|
||||||
|
<BaseDivider></BaseDivider>
|
||||||
|
<PersonalSettings :backupState="props.backupState"/>
|
||||||
|
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</template>
|
</template>
|
||||||
|
@ -56,6 +58,8 @@ import SetupConfirmation from '@/Components/SetupConfirmation.vue';
|
||||||
|
|
||||||
import Notification from '@/utils/toast';
|
import Notification from '@/utils/toast';
|
||||||
import { mdiTwoFactorAuthentication } from '@mdi/js';
|
import { mdiTwoFactorAuthentication } from '@mdi/js';
|
||||||
|
import PersonalSettings from '@/Components/PersonalSettings.vue';
|
||||||
|
import BaseDivider from './BaseDivider.vue';
|
||||||
|
|
||||||
const mainService = MainService();
|
const mainService = MainService();
|
||||||
// const emit = defineEmits(['confirm', 'update:confirmation']);
|
// const emit = defineEmits(['confirm', 'update:confirmation']);
|
||||||
|
@ -70,6 +74,10 @@ const props = defineProps({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
backupState: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
// // code: {
|
// // code: {
|
||||||
// // type: Object,
|
// // type: Object,
|
||||||
// // },
|
// // },
|
||||||
|
|
|
@ -29,6 +29,7 @@ import { computed, Ref } from 'vue';
|
||||||
import { usePage } from '@inertiajs/vue3';
|
import { usePage } from '@inertiajs/vue3';
|
||||||
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
|
import FormValidationErrors from '@/Components/FormValidationErrors.vue';
|
||||||
import PersonalTotpSettings from '@/Components/PersonalTotpSettings.vue';
|
import PersonalTotpSettings from '@/Components/PersonalTotpSettings.vue';
|
||||||
|
// import PersonalSettings from '@/Components/PersonalSettings.vue';
|
||||||
// import { MainService } from '@/Stores/main';
|
// import { MainService } from '@/Stores/main';
|
||||||
// const mainService = MainService();
|
// const mainService = MainService();
|
||||||
|
|
||||||
|
@ -47,9 +48,9 @@ defineProps({
|
||||||
code: {
|
code: {
|
||||||
type: Object,
|
type: Object,
|
||||||
},
|
},
|
||||||
recoveryCodes: {
|
backupState: {
|
||||||
type: Array<string>,
|
type: Object,
|
||||||
default: () => [],
|
default: () => ({}),
|
||||||
},
|
},
|
||||||
errors: {
|
errors: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -211,7 +212,10 @@ const flash: Ref<any> = computed(() => {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<PersonalTotpSettings :twoFactorEnabled="twoFactorEnabled"/>
|
<PersonalTotpSettings :twoFactorEnabled="twoFactorEnabled" :backupState="backupState">
|
||||||
|
</PersonalTotpSettings>
|
||||||
|
<!-- <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()">
|
||||||
<div class="text-lg font-medium text-gray-900">
|
<div class="text-lg font-medium text-gray-900">
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<LayoutGuest>
|
<LayoutGuest>
|
||||||
|
|
||||||
|
@ -11,8 +10,7 @@
|
||||||
<!-- <span class="self-center text-2xl font-bold whitespace-nowrap">Tethys</span> -->
|
<!-- <span class="self-center text-2xl font-bold whitespace-nowrap">Tethys</span> -->
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<CardBox v-if="isTwoFactorAuthNeeded == false" :class="cardClass" form
|
<CardBox v-if="isTwoFactorAuthNeeded == false" :class="cardClass" form @submit.prevent="submit">
|
||||||
@submit.prevent="submit">
|
|
||||||
<FormValidationErrors v-bind:errors="errors" />
|
<FormValidationErrors v-bind:errors="errors" />
|
||||||
|
|
||||||
<NotificationBarInCard v-if="status" color="info">
|
<NotificationBarInCard v-if="status" color="info">
|
||||||
|
@ -47,8 +45,8 @@
|
||||||
<!-- buttons -->
|
<!-- buttons -->
|
||||||
|
|
||||||
<BaseButtons>
|
<BaseButtons>
|
||||||
<BaseButton type="submit" color="info" label=" Login to your account" :class="{ 'opacity-25': form.processing }"
|
<BaseButton type="submit" color="info" label=" Login to your account"
|
||||||
v-bind:disabled="form.processing" />
|
:class="{ 'opacity-25': form.processing }" v-bind:disabled="form.processing" />
|
||||||
<!-- <button type="submit" v-bind:disabled="form.processing" :class="{ 'opacity-25': form.processing }"
|
<!-- <button type="submit" v-bind:disabled="form.processing" :class="{ 'opacity-25': form.processing }"
|
||||||
class="text-white bg-cyan-600 hover:bg-cyan-700 focus:ring-4 focus:ring-cyan-200 font-medium rounded-lg text-base px-5 py-3 w-full sm:w-auto text-center">
|
class="text-white bg-cyan-600 hover:bg-cyan-700 focus:ring-4 focus:ring-cyan-200 font-medium rounded-lg text-base px-5 py-3 w-full sm:w-auto text-center">
|
||||||
Login to your account
|
Login to your account
|
||||||
|
@ -66,10 +64,29 @@
|
||||||
|
|
||||||
<CardBox v-else-if="isTwoFactorAuthNeeded" :icon="mdiTwoFactorAuthentication" :class="cardClass" form
|
<CardBox v-else-if="isTwoFactorAuthNeeded" :icon="mdiTwoFactorAuthentication" :class="cardClass" form
|
||||||
@submit.prevent="submitFa2Form">
|
@submit.prevent="submitFa2Form">
|
||||||
<FormField label="2FA Code" label-for="code" help="Please enter 2factor code">
|
|
||||||
|
<!-- authentication method menu -->
|
||||||
|
<div class="lex flex-col md:flex-row mb-3">
|
||||||
|
<label for="authmethod-option-one" class="pure-radio">
|
||||||
|
<input id="authmethod-option-one" type="radio" v-model="authMethod" value="code" />
|
||||||
|
use 2fa app
|
||||||
|
</label>
|
||||||
|
<label for="authmethod-option-two" class="pure-radio">
|
||||||
|
<input id="authmethod-option-two" type="radio" v-model="authMethod" value="backupCode" />
|
||||||
|
use backup code
|
||||||
|
</label>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField v-if="authMethod === 'code'" label="2FA Code" label-for="code" help="Please enter 2factor code">
|
||||||
<FormControl v-model="fa2Form.code" :icon="mdiAccount" id="code" type="tel" required />
|
<FormControl v-model="fa2Form.code" :icon="mdiAccount" id="code" type="tel" required />
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
<FormField v-if="authMethod === 'backupCode'" label="Backup Code" label-for="backupCode" help="Please enter backup code">
|
||||||
|
<FormControl v-model="fa2Form.backup_code" :icon="mdiAccount" id="backupCode" type="tel" required />
|
||||||
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
<div v-if="flash && flash.message" class="flex flex-col mt-6 animate-fade-in">
|
<div v-if="flash && flash.message" class="flex flex-col mt-6 animate-fade-in">
|
||||||
<div class="bg-yellow-500 border-l-4 border-orange-400 text-white p-4" role="alert">
|
<div class="bg-yellow-500 border-l-4 border-orange-400 text-white p-4" role="alert">
|
||||||
<p class="font-bold">Be Warned</p>
|
<p class="font-bold">Be Warned</p>
|
||||||
|
@ -85,8 +102,8 @@
|
||||||
class="text-white bg-cyan-600 hover:bg-cyan-700 focus:ring-4 focus:ring-cyan-200 font-medium rounded-lg text-base px-5 py-3 w-full sm:w-auto text-center">
|
class="text-white bg-cyan-600 hover:bg-cyan-700 focus:ring-4 focus:ring-cyan-200 font-medium rounded-lg text-base px-5 py-3 w-full sm:w-auto text-center">
|
||||||
Verify
|
Verify
|
||||||
</button> -->
|
</button> -->
|
||||||
<BaseButton type="submit" :icon="mdiContentSaveCheck" color="info" label=" Login to your account" :class="{ 'opacity-25': fa2Form.processing }"
|
<BaseButton type="submit" :icon="mdiContentSaveCheck" color="info" label=" Login to your account"
|
||||||
v-bind:disabled="fa2Form.processing" />
|
:class="{ 'opacity-25': fa2Form.processing }" v-bind:disabled="fa2Form.processing" />
|
||||||
</BaseButtons>
|
</BaseButtons>
|
||||||
<!-- <Link :href="stardust.route('app.register.show')"> Register </Link> -->
|
<!-- <Link :href="stardust.route('app.register.show')"> Register </Link> -->
|
||||||
|
|
||||||
|
@ -136,6 +153,15 @@ const user_id: Ref<any> = computed(() => {
|
||||||
|
|
||||||
const isTwoFactorAuthNeeded = ref(false);
|
const isTwoFactorAuthNeeded = ref(false);
|
||||||
|
|
||||||
|
const authMethod = ref('code');
|
||||||
|
watch(authMethod, (currentValue) => {
|
||||||
|
if (currentValue == 'code') {
|
||||||
|
fa2Form.backup_code = undefined;
|
||||||
|
} else if (currentValue == 'backupCode') {
|
||||||
|
fa2Form.code = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// const user_id: ComputedRef<number> = computed(() => {
|
// const user_id: ComputedRef<number> = computed(() => {
|
||||||
// return usePage().props.flash.user_id as number;
|
// return usePage().props.flash.user_id as number;
|
||||||
// });
|
// });
|
||||||
|
@ -230,7 +256,7 @@ const submit = async () => {
|
||||||
remember: form.remember && form.remember.length ? 'on' : '',
|
remember: form.remember && form.remember.length ? 'on' : '',
|
||||||
}))
|
}))
|
||||||
.post('/app/login', {
|
.post('/app/login', {
|
||||||
// .post(stardust.route('login.store'), {
|
// .post(stardust.route('login.store'), {
|
||||||
onFinish: () => {
|
onFinish: () => {
|
||||||
form.reset('password');
|
form.reset('password');
|
||||||
},
|
},
|
||||||
|
@ -240,14 +266,15 @@ const submit = async () => {
|
||||||
|
|
||||||
const fa2Form: InertiaForm = useForm(() => ({
|
const fa2Form: InertiaForm = useForm(() => ({
|
||||||
code: '',
|
code: '',
|
||||||
remember: [],
|
backup_code: '',
|
||||||
|
// remember: [],
|
||||||
login_id: ''
|
login_id: ''
|
||||||
}));
|
}));
|
||||||
const submitFa2Form = async () => {
|
const submitFa2Form = async () => {
|
||||||
await fa2Form
|
await fa2Form
|
||||||
.transform((data: InertiaForm) => ({
|
.transform((data: InertiaForm) => ({
|
||||||
...data,
|
...data,
|
||||||
remember: fa2Form.remember && fa2Form.remember.length ? 'on' : '',
|
// remember: fa2Form.remember && fa2Form.remember.length ? 'on' : '',
|
||||||
login_id: user_id.value
|
login_id: user_id.value
|
||||||
}))
|
}))
|
||||||
.post(stardust.route('login.twoFactorChallenge'), {
|
.post(stardust.route('login.twoFactorChallenge'), {
|
||||||
|
|
|
@ -306,8 +306,6 @@ const prevStep = () => {
|
||||||
const submit = async () => {
|
const submit = async () => {
|
||||||
let route = stardust.route('dataset.submit');
|
let route = stardust.route('dataset.submit');
|
||||||
|
|
||||||
// this.currentStatus = STATUS_SAVING;
|
|
||||||
// serrors = [];
|
|
||||||
const files = form.files.map((obj) => {
|
const files = form.files.map((obj) => {
|
||||||
return new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified });
|
return new File([obj.blob], obj.label, { type: obj.type, lastModified: obj.lastModified });
|
||||||
});
|
});
|
||||||
|
@ -808,7 +806,7 @@ Removes a selected keyword
|
||||||
<FormField label="Coverage X Min"
|
<FormField label="Coverage X Min"
|
||||||
:class="{ 'text-red-400': form.errors['coverage.x_min'] }"
|
:class="{ 'text-red-400': form.errors['coverage.x_min'] }"
|
||||||
class="w-full mx-2 flex-1">
|
class="w-full mx-2 flex-1">
|
||||||
<FormControl required v-model="form.coverage.x_min" type="text"
|
<FormControl required v-model="form.coverage.x_min" type="text" inputmode="numeric" pattern="\d*"
|
||||||
placeholder="[enter x_min]">
|
placeholder="[enter x_min]">
|
||||||
<div class="text-red-400 text-sm"
|
<div class="text-red-400 text-sm"
|
||||||
v-if="form.errors['coverage.x_min'] && Array.isArray(form.errors['coverage.x_min'])">
|
v-if="form.errors['coverage.x_min'] && Array.isArray(form.errors['coverage.x_min'])">
|
||||||
|
|
|
@ -31,7 +31,7 @@ const errors: Ref<any> = computed(() => {
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
preferred_reviewer: '',
|
preferred_reviewer: '',
|
||||||
preferred_reviewer_email: '',
|
preferred_reviewer_email: '',
|
||||||
preferation: 'yes_preferation',
|
preferation: 'no_preferation',
|
||||||
|
|
||||||
// preferation: '',
|
// preferation: '',
|
||||||
// isPreferationRequired: false,
|
// isPreferationRequired: false,
|
||||||
|
|
|
@ -35,6 +35,14 @@ export const saveState = async (data) => {
|
||||||
return resp.data;
|
return resp.data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generateCodes = async () => {
|
||||||
|
const url = '/api/twofactor_backupcodes/settings/create';
|
||||||
|
|
||||||
|
// return Axios.post(url, {}).then((resp) => resp.data);
|
||||||
|
const resp = await axios.post(url, {});
|
||||||
|
return resp.data;
|
||||||
|
};
|
||||||
|
|
||||||
// Anfrage staet : 1
|
// Anfrage staet : 1
|
||||||
|
|
||||||
// ANtwort json:
|
// ANtwort json:
|
||||||
|
@ -68,11 +76,16 @@ export const MainService = defineStore('main', {
|
||||||
|
|
||||||
totpState: 0,
|
totpState: 0,
|
||||||
|
|
||||||
|
backupcodesEnabled: false,
|
||||||
|
total: 0,
|
||||||
|
used: 0,
|
||||||
|
codes: [],
|
||||||
|
|
||||||
graphData: {},
|
graphData: {},
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
// payload = authenticated user
|
// payload = authenticated user
|
||||||
setUser(payload) {
|
setUser(payload: any) {
|
||||||
if (payload.name) {
|
if (payload.name) {
|
||||||
this.userName = payload.name;
|
this.userName = payload.name;
|
||||||
}
|
}
|
||||||
|
@ -84,7 +97,7 @@ export const MainService = defineStore('main', {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
setDataset(payload) {
|
setDataset(payload: any) {
|
||||||
this.dataset = payload;
|
this.dataset = payload;
|
||||||
|
|
||||||
// this.dataset = {
|
// this.dataset = {
|
||||||
|
@ -109,7 +122,7 @@ export const MainService = defineStore('main', {
|
||||||
this.dataset = null;
|
this.dataset = null;
|
||||||
},
|
},
|
||||||
|
|
||||||
fetch(sampleDataKey) {
|
fetch(sampleDataKey: any) {
|
||||||
// sampleDataKey= clients or history
|
// sampleDataKey= clients or history
|
||||||
axios
|
axios
|
||||||
.get(`/data-sources/${sampleDataKey}.json`)
|
.get(`/data-sources/${sampleDataKey}.json`)
|
||||||
|
@ -155,8 +168,12 @@ export const MainService = defineStore('main', {
|
||||||
},
|
},
|
||||||
|
|
||||||
async disable() {
|
async disable() {
|
||||||
const { state } = await saveState({ state: State.STATE_DISABLED });
|
const { state, backupState } = await saveState({ state: State.STATE_DISABLED });
|
||||||
this.totpState = state;
|
this.totpState = state;
|
||||||
|
|
||||||
|
this.backupcodesEnabled = backupState.enabled;
|
||||||
|
this.total = backupState.total;
|
||||||
|
this.used = backupState.used;
|
||||||
},
|
},
|
||||||
|
|
||||||
async confirm(code: string) {
|
async confirm(code: string) {
|
||||||
|
@ -181,6 +198,15 @@ export const MainService = defineStore('main', {
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
|
async generate(): Promise<void> {
|
||||||
|
const { codes, backupState } = await generateCodes();
|
||||||
|
// this.totpState = state;
|
||||||
|
this.codes = codes;
|
||||||
|
this.backupcodesEnabled = backupState.enabled;
|
||||||
|
this.total = backupState.total;
|
||||||
|
this.used = backupState.used;
|
||||||
|
},
|
||||||
|
|
||||||
// fetchfiles(id) {
|
// fetchfiles(id) {
|
||||||
// // sampleDataKey= authors or datasets
|
// // sampleDataKey= authors or datasets
|
||||||
// axios
|
// axios
|
||||||
|
|
|
@ -11,6 +11,7 @@ import { darkModeKey, styleKey } from '@/config';
|
||||||
// import type { DefineComponent } from 'vue';
|
// import type { DefineComponent } from 'vue';
|
||||||
// import { resolvePageComponent } from '@adonisjs/inertia/helpers';
|
// import { resolvePageComponent } from '@adonisjs/inertia/helpers';
|
||||||
const pinia = createPinia();
|
const pinia = createPinia();
|
||||||
|
import i18n from './i18n';
|
||||||
import { EmitterPlugin } from '@/EmitterDirective';
|
import { EmitterPlugin } from '@/EmitterDirective';
|
||||||
|
|
||||||
import { initRoutes } from '@eidellev/adonis-stardust/client/index.js';
|
import { initRoutes } from '@eidellev/adonis-stardust/client/index.js';
|
||||||
|
@ -62,6 +63,7 @@ createInertiaApp({
|
||||||
createApp({ render: () => h(App, props) })
|
createApp({ render: () => h(App, props) })
|
||||||
.use(plugin)
|
.use(plugin)
|
||||||
.use(pinia)
|
.use(pinia)
|
||||||
|
.use(i18n)
|
||||||
.use(EmitterPlugin)
|
.use(EmitterPlugin)
|
||||||
// .component('inertia-link', Link)
|
// .component('inertia-link', Link)
|
||||||
.mount(el);
|
.mount(el);
|
||||||
|
|
17
resources/js/i18n/index.ts
Normal file
17
resources/js/i18n/index.ts
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
import { createI18n } from 'vue-i18n';
|
||||||
|
const i18n = createI18n({
|
||||||
|
// default locale
|
||||||
|
locale: 'en',
|
||||||
|
// translations
|
||||||
|
messages: {
|
||||||
|
en: {
|
||||||
|
appTitle: 'Mushahed',
|
||||||
|
twofactor_backupcodes: '',
|
||||||
|
},
|
||||||
|
de: {
|
||||||
|
appTitle: 'مشاهد',
|
||||||
|
twofactor_backupcodes: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export default i18n;
|
|
@ -28,6 +28,7 @@ router.group(() => {
|
||||||
|
|
||||||
|
|
||||||
router.post('/twofactor_totp/settings/enable/:state/:code?', [UserController, 'enable']).as('apps.twofactor_totp.enable') .use(middleware.auth());
|
router.post('/twofactor_totp/settings/enable/:state/:code?', [UserController, 'enable']).as('apps.twofactor_totp.enable') .use(middleware.auth());
|
||||||
|
router.post('/twofactor_backupcodes/settings/create', [UserController, 'createCodes']).as('apps.twofactor_backupcodes.create') .use(middleware.auth());
|
||||||
|
|
||||||
})
|
})
|
||||||
// .namespace('App/Controllers/Http/Api')
|
// .namespace('App/Controllers/Http/Api')
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Preloaded File - node ace make:preload rules/translatedLanguage
|
| Preloaded File - node ace make:preload rules/fileLength
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|*/
|
|*/
|
||||||
|
|
||||||
|
|
104
start/rules/file_scan.ts
Normal file
104
start/rules/file_scan.ts
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Preloaded File - node ace make:preload rules/fileScan
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|*/
|
||||||
|
|
||||||
|
import { FieldContext } from '@vinejs/vine/types';
|
||||||
|
import vine, { errors } from '@vinejs/vine';
|
||||||
|
|
||||||
|
// import { VineString } from '@vinejs/vine';
|
||||||
|
import { VineMultipartFile, isBodyParserFile } from '#providers/vinejs_provider';
|
||||||
|
import type { MultipartFile } from '@adonisjs/core/bodyparser';
|
||||||
|
import ClamScan from 'clamscan';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Options accepted by the unique rule
|
||||||
|
*/
|
||||||
|
// type Options = {
|
||||||
|
// mainLanguageField: string;
|
||||||
|
// typeField: string;
|
||||||
|
// };
|
||||||
|
type Options = {
|
||||||
|
// size: string | number;
|
||||||
|
// extnames: string[];
|
||||||
|
removeInfected: boolean;
|
||||||
|
// debugMode?: boolean;
|
||||||
|
// scanRecursively?: boolean;
|
||||||
|
host?: string;
|
||||||
|
port?: number;
|
||||||
|
// clamdscan: {
|
||||||
|
// active: boolean;
|
||||||
|
// host: string;
|
||||||
|
// port: number;
|
||||||
|
// multiscan: boolean;
|
||||||
|
// };
|
||||||
|
// preference: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function fileScan(file: VineMultipartFile | unknown, options: Options, field: FieldContext) {
|
||||||
|
// if (typeof value !== 'string' && typeof value != 'number') {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
if (!isBodyParserFile(file)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const validatedFile = file as MultipartFile;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await scanFileForViruses(validatedFile.tmpPath, options.host, options.port); //, 'gitea.lan', 3310);
|
||||||
|
// await this.scanFileForViruses("/tmp/testfile.txt");
|
||||||
|
} catch (error) {
|
||||||
|
// If the file is infected or there's an error scanning the file, throw a validation exception
|
||||||
|
// throw error;
|
||||||
|
field.report(`Upload error. Code: ${error.code} message: ${error.messages.uploadError}`, 'fileScan', field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function scanFileForViruses(filePath: string | undefined, host?: string, port?: number): Promise<void> {
|
||||||
|
// const clamscan = await (new ClamScan().init());
|
||||||
|
const opts: ClamScan.Options = {
|
||||||
|
removeInfected: true, // If true, removes infected files
|
||||||
|
debugMode: false, // Whether or not to log info/debug/error msgs to the console
|
||||||
|
scanRecursively: true, // If true, deep scan folders recursively
|
||||||
|
clamdscan: {
|
||||||
|
active: true, // If true, this module will consider using the clamdscan binary
|
||||||
|
host,
|
||||||
|
port,
|
||||||
|
multiscan: true, // Scan using all available cores! Yay!
|
||||||
|
},
|
||||||
|
preference: 'clamdscan', // If clamdscan is found and active, it will be used by default
|
||||||
|
};
|
||||||
|
|
||||||
|
return new Promise(async (resolve, reject) => {
|
||||||
|
try {
|
||||||
|
const clamscan = await new ClamScan().init(opts);
|
||||||
|
// You can re-use the `clamscan` object as many times as you want
|
||||||
|
// const version = await clamscan.getVersion();
|
||||||
|
// console.log(`ClamAV Version: ${version}`);
|
||||||
|
const { file, isInfected, viruses } = await clamscan.isInfected(filePath);
|
||||||
|
if (isInfected) {
|
||||||
|
console.log(`${file} is infected with ${viruses}!`);
|
||||||
|
// reject(new ValidationException(true, { 'upload error': `File ${file} is infected!` }));
|
||||||
|
reject(new errors.E_VALIDATION_ERROR({ uploadError: `File ${file} is infected!` }));
|
||||||
|
} else {
|
||||||
|
resolve();
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// If there's an error scanning the file, throw a validation exception
|
||||||
|
// reject(new ValidationException(true, { 'upload error': `${error.message}` }));
|
||||||
|
reject(new errors.E_VALIDATION_ERROR({ uploadError: `${error.message}!` }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const fileScanRule = vine.createRule(fileScan);
|
||||||
|
|
||||||
|
declare module '#providers/vinejs_provider' {
|
||||||
|
interface VineMultipartFile {
|
||||||
|
fileScan(options: Options): this;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
VineMultipartFile.macro('fileScan', function (this: VineMultipartFile, options: Options) {
|
||||||
|
return this.use(fileScanRule(options));
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user