Compare commits
30 commits
Author | SHA1 | Date | |
---|---|---|---|
be6b38d0a3 | |||
c245c8e97d | |||
c3ae4327b7 | |||
2cb33a779c | |||
dbd2bf2e9d | |||
106f8d5f27 | |||
f04c1f6327 | |||
10d159a57a | |||
8fbda9fc64 | |||
7bb4bd06cf | |||
f89b119b18 | |||
09f65359f9 | |||
9823364670 | |||
b93e46207f | |||
a25f8bf6f7 | |||
70f016422c | |||
9f5d35f7ba | |||
a934626721 | |||
0d259b6464 | |||
c350e9c373 | |||
51a5673a3d | |||
b540547e4c | |||
36cd7a757b | |||
a41b091214 | |||
a3031169ca | |||
4c5a8f5a42 | |||
8d47a58d29 | |||
a5e0a36327 | |||
c0496be51b | |||
2c4f51be68 |
|
@ -18,3 +18,5 @@ REDIS_PASSWORD=
|
||||||
SMTP_HOST=
|
SMTP_HOST=
|
||||||
SMTP_PORT=
|
SMTP_PORT=
|
||||||
RESEND_API_KEY=
|
RESEND_API_KEY=
|
||||||
|
OPENSEARCH_HOST=http://localhost
|
||||||
|
OPENSEARCH_CORE=tethys-records
|
78
.gitea/workflows/checkReferenceType.yaml
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
# This is a Gitea Actions workflow configuration file for running CI tests on the `feat/checkReferenceType` branch.
|
||||||
|
# The workflow is named "CI" and runs on the latest Ubuntu environment using a Node.js 20 Docker container.
|
||||||
|
# It sets up a PostgreSQL service with specified environment variables and health checks.
|
||||||
|
# The workflow includes the following steps:
|
||||||
|
# 1. Checkout the repository using the actions/checkout@v3 action.
|
||||||
|
# 2. Install Node.js dependencies using `npm ci`.
|
||||||
|
# 3. Create a `.env.test` file by copying from `.env.example`.
|
||||||
|
# 4. Set up environment variables in the `.env.test` file, including database connection details and other app-specific settings.
|
||||||
|
# 5. Run functional tests using the `node ace test functional --groups "ReferenceValidation"` command.
|
||||||
|
name: CI
|
||||||
|
run-name: Running tests for checkReferenceType branch
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- feat/checkReferenceType
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
container-job:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# Docker Hub image that `container-job` executes in
|
||||||
|
container: node:20-bullseye
|
||||||
|
|
||||||
|
services:
|
||||||
|
# Label used to access the service container
|
||||||
|
postgres:
|
||||||
|
image: postgres:latest
|
||||||
|
env:
|
||||||
|
POSTGRES_USER: alice
|
||||||
|
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
||||||
|
POSTGRES_DB: tethys_dev
|
||||||
|
# ports:
|
||||||
|
# - 5432:5432
|
||||||
|
options: |
|
||||||
|
--health-cmd pg_isready
|
||||||
|
--health-interval 10s
|
||||||
|
--health-timeout 5s
|
||||||
|
--health-retries 5
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
# - name: Set up Node.js
|
||||||
|
# uses: actions/setup-node@v2
|
||||||
|
# with:
|
||||||
|
# node-version: '20'
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Create .env.test file
|
||||||
|
run: cp .env.example .env.test
|
||||||
|
|
||||||
|
- name: Set up environment variables
|
||||||
|
run: |
|
||||||
|
echo "DB_CONNECTION=pg" >> .env.test
|
||||||
|
echo "PG_HOST=postgres" >> .env.test
|
||||||
|
echo "PG_PORT=5432" >> .env.test
|
||||||
|
echo "PG_USER=alice" >> .env.test
|
||||||
|
echo "PG_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}" >> .env.test
|
||||||
|
echo "PG_DB_NAME=tethys_dev" >> .env.test
|
||||||
|
echo "NODE_ENV=test" >> .env.test
|
||||||
|
echo "ASSETS_DRIVER=fake" >> .env.test
|
||||||
|
echo "SESSION_DRIVER=memory" >> .env.test
|
||||||
|
echo "HASH_DRIVER=bcrypt" >> .env.test
|
||||||
|
echo "HOST=127.0.0.1" >> .env.test
|
||||||
|
echo "PORT=3333" >> .env.test
|
||||||
|
echo "APP_NAME=TethysCloud" >> .env.test
|
||||||
|
echo "APP_URL=http://${HOST}:${PORT}" >> .env.test
|
||||||
|
echo "CACHE_VIEWS=false" >> .env.test
|
||||||
|
echo "APP_KEY=pfi5N2ACN4tMJ5d8d8BPHfh3FEuvleej" >> .env.test
|
||||||
|
echo "DRIVE_DISK=local" >> .env.test
|
||||||
|
echo "OAI_LIST_SIZE=200" >> .env.test
|
||||||
|
echo "OPENSEARCH_HOST=${{ secrets.OPENSEARCH_HOST }}" >> .env.test
|
||||||
|
echo "OPENSEARCH_CORE=tethys-records" >> .env.test
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: node ace test functional --groups "ReferenceValidation"
|
|
@ -4,7 +4,13 @@
|
||||||
name: CI Pipeline
|
name: CI Pipeline
|
||||||
run-name: ${{ github.actor }} is running CI pipeline
|
run-name: ${{ github.actor }} is running CI pipeline
|
||||||
# trigger build when pushing, or when creating a pull request
|
# trigger build when pushing, or when creating a pull request
|
||||||
on: [push, pull_request]
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
pull_request:
|
||||||
|
branches:
|
||||||
|
- master
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Label of the container job
|
# Label of the container job
|
||||||
|
@ -12,7 +18,7 @@ jobs:
|
||||||
# run build on latest ubuntu
|
# run build on latest ubuntu
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
container: node:18-bullseye
|
container: node:20-bullseye
|
||||||
|
|
||||||
services:
|
services:
|
||||||
mydb:
|
mydb:
|
||||||
|
@ -70,6 +76,7 @@ jobs:
|
||||||
&& echo "CACHE_VIEWS=false" >> .env.test
|
&& echo "CACHE_VIEWS=false" >> .env.test
|
||||||
&& echo "APP_KEY=pfi5N2ACN4tMJ5d8d8BPHfh3FEuvleej" >> .env.test
|
&& echo "APP_KEY=pfi5N2ACN4tMJ5d8d8BPHfh3FEuvleej" >> .env.test
|
||||||
&& echo "DRIVE_DISK=local" >> .env.test
|
&& echo "DRIVE_DISK=local" >> .env.test
|
||||||
|
&& echo "OAI_LIST_SIZE=200" >> .env.test
|
||||||
|
|
||||||
# finally run the tests
|
# finally run the tests
|
||||||
# - run: npm test
|
# - run: npm test
|
||||||
|
@ -95,3 +102,4 @@ jobs:
|
||||||
# uses: coverallsapp/github-action@master
|
# uses: coverallsapp/github-action@master
|
||||||
# with:
|
# with:
|
||||||
# github-token: ${{ secrets.GITHUB_TOKEN }}
|
# github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
|
1
.gitignore
vendored
|
@ -7,3 +7,4 @@ coverage
|
||||||
tmp
|
tmp
|
||||||
docker-compose.yml
|
docker-compose.yml
|
||||||
.env.test
|
.env.test
|
||||||
|
public/assets
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
################## First Stage - Creating base #########################
|
################## First Stage - Creating base #########################
|
||||||
|
|
||||||
# Created a variable to hold our node base image
|
# Created a variable to hold our node base image
|
||||||
ARG NODE_IMAGE=node:20-bookworm-slim
|
ARG NODE_IMAGE=node:22-bookworm-slim
|
||||||
|
|
||||||
FROM $NODE_IMAGE AS base
|
FROM $NODE_IMAGE AS base
|
||||||
# Install dumb-init and ClamAV, and perform ClamAV database update
|
# Install dumb-init and ClamAV, and perform ClamAV database update
|
||||||
|
|
7
ace.js
|
@ -15,10 +15,11 @@
|
||||||
/**
|
/**
|
||||||
* Register hook to process TypeScript files using ts-node
|
* Register hook to process TypeScript files using ts-node
|
||||||
*/
|
*/
|
||||||
import { register } from 'node:module'
|
// import { register } from 'node:module';
|
||||||
register('ts-node/esm', import.meta.url)
|
// register('ts-node/esm', import.meta.url);
|
||||||
|
import 'ts-node-maintained/register/esm';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Import ace console entrypoint
|
* Import ace console entrypoint
|
||||||
*/
|
*/
|
||||||
await import('./bin/console.js')
|
await import('./bin/console.js');
|
||||||
|
|
169
adonisrc.ts
|
@ -1,7 +1,7 @@
|
||||||
import { defineConfig } from '@adonisjs/core/app'
|
import { defineConfig } from '@adonisjs/core/app';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Commands
|
| Commands
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
@ -10,12 +10,11 @@ export default defineConfig({
|
||||||
| will be scanned automatically from the "./commands" directory.
|
| will be scanned automatically from the "./commands" directory.
|
||||||
|
|
||||||
*/
|
*/
|
||||||
commands: [
|
commands: [
|
||||||
() => import('@adonisjs/core/commands'),
|
() => import('@adonisjs/core/commands'),
|
||||||
() => import('@adonisjs/lucid/commands'),
|
() => import('@adonisjs/lucid/commands'),
|
||||||
() => import('@adonisjs/mail/commands')
|
() => import('@adonisjs/mail/commands')],
|
||||||
],
|
/*
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Preloads
|
| Preloads
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
@ -23,19 +22,22 @@ export default defineConfig({
|
||||||
| List of modules to import before starting the application.
|
| List of modules to import before starting the application.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
preloads: [
|
preloads: [
|
||||||
() => import('./start/routes.js'),
|
() => import('./start/routes.js'),
|
||||||
() => import('./start/kernel.js'),
|
() => import('./start/kernel.js'),
|
||||||
() => import('#start/validator'),
|
() => import('#start/validator'),
|
||||||
() => 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'),
|
() => import('#start/rules/file_scan'),
|
||||||
() => import('#start/rules/allowed_extensions_mimetypes'),
|
() => import('#start/rules/allowed_extensions_mimetypes'),
|
||||||
() => import('#start/rules/dependent_array_min_length')
|
() => import('#start/rules/dependent_array_min_length'),
|
||||||
],
|
() => import('#start/rules/referenceValidation'),
|
||||||
/*
|
() => import('#start/rules/valid_mimetype'),
|
||||||
|
() => import('#start/rules/array_contains_types'),
|
||||||
|
],
|
||||||
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Service providers
|
| Service providers
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
@ -44,48 +46,49 @@ export default defineConfig({
|
||||||
| application
|
| application
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
providers: [
|
providers: [
|
||||||
// () => import('./providers/AppProvider.js'),
|
// () => import('./providers/AppProvider.js'),
|
||||||
() => import('@adonisjs/core/providers/app_provider'),
|
() => import('@adonisjs/core/providers/app_provider'),
|
||||||
() => import('@adonisjs/core/providers/hash_provider'),
|
() => import('@adonisjs/core/providers/hash_provider'),
|
||||||
{
|
{
|
||||||
file: () => import('@adonisjs/core/providers/repl_provider'),
|
file: () => import('@adonisjs/core/providers/repl_provider'),
|
||||||
environment: ['repl', 'test'],
|
environment: ['repl', 'test'],
|
||||||
},
|
},
|
||||||
() => import('@adonisjs/session/session_provider'),
|
() => import('@adonisjs/session/session_provider'),
|
||||||
() => import('@adonisjs/core/providers/edge_provider'),
|
() => import('@adonisjs/core/providers/edge_provider'),
|
||||||
() => import('@adonisjs/shield/shield_provider'),
|
() => import('@adonisjs/shield/shield_provider'),
|
||||||
// () => import('@eidellev/inertia-adonisjs'),
|
// () => import('@eidellev/inertia-adonisjs'),
|
||||||
// () => import('@adonisjs/inertia/inertia_provider'),
|
// () => import('@adonisjs/inertia/inertia_provider'),
|
||||||
() => import('#providers/app_provider'),
|
() => import('#providers/app_provider'),
|
||||||
() => import('#providers/inertia_provider'),
|
() => import('#providers/inertia_provider'),
|
||||||
() => import('@adonisjs/lucid/database_provider'),
|
() => import('@adonisjs/lucid/database_provider'),
|
||||||
() => import('@adonisjs/auth/auth_provider'),
|
() => import('@adonisjs/auth/auth_provider'),
|
||||||
// () => import('@eidellev/adonis-stardust'),
|
// () => import('@eidellev/adonis-stardust'),
|
||||||
() => import('@adonisjs/redis/redis_provider'),
|
() => import('@adonisjs/redis/redis_provider'),
|
||||||
() => import('@adonisjs/encore/encore_provider'),
|
// () => import('@adonisjs/encore/encore_provider'),
|
||||||
() => import('@adonisjs/static/static_provider'),
|
() => import('@adonisjs/static/static_provider'),
|
||||||
() => import('#providers/stardust_provider'),
|
() => import('#providers/stardust_provider'),
|
||||||
() => import('#providers/query_builder_provider'),
|
() => import('#providers/query_builder_provider'),
|
||||||
() => import('#providers/token_worker_provider'),
|
() => import('#providers/token_worker_provider'),
|
||||||
// () => import('#providers/validator_provider'),
|
// () => import('#providers/validator_provider'),
|
||||||
() => import('#providers/drive/provider/drive_provider'),
|
// () => import('#providers/drive/provider/drive_provider'),
|
||||||
// () => import('@adonisjs/core/providers/vinejs_provider'),
|
() => import('@adonisjs/drive/drive_provider'),
|
||||||
() => import('#providers/vinejs_provider'),
|
// () => import('@adonisjs/core/providers/vinejs_provider'),
|
||||||
() => import('@adonisjs/mail/mail_provider')
|
() => import('#providers/vinejs_provider'),
|
||||||
// () => import('#providers/mail_provider'),
|
() => import('@adonisjs/mail/mail_provider'),
|
||||||
],
|
() => import('@adonisjs/vite/vite_provider'),
|
||||||
metaFiles: [
|
],
|
||||||
{
|
metaFiles: [
|
||||||
pattern: 'public/**',
|
{
|
||||||
reloadServer: false,
|
pattern: 'public/**',
|
||||||
},
|
reloadServer: false,
|
||||||
{
|
},
|
||||||
pattern: 'resources/views/**/*.edge',
|
{
|
||||||
reloadServer: false,
|
pattern: 'resources/views/**/*.edge',
|
||||||
},
|
reloadServer: false,
|
||||||
],
|
},
|
||||||
/*
|
],
|
||||||
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
| Tests
|
| Tests
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
@ -94,22 +97,24 @@ export default defineConfig({
|
||||||
| and add additional suites.
|
| and add additional suites.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
tests: {
|
tests: {
|
||||||
suites: [
|
suites: [
|
||||||
{
|
{
|
||||||
files: ['tests/unit/**/*.spec(.ts|.js)'],
|
files: ['tests/unit/**/*.spec(.ts|.js)'],
|
||||||
name: 'unit',
|
name: 'unit',
|
||||||
timeout: 2000,
|
timeout: 2000,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
files: ['tests/functional/**/*.spec(.ts|.js)'],
|
files: ['tests/functional/**/*.spec(.ts|.js)'],
|
||||||
name: 'functional',
|
name: 'functional',
|
||||||
timeout: 30000,
|
timeout: 30000,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
forceExit: false,
|
forceExit: false,
|
||||||
},
|
},
|
||||||
|
assetsBundler: false,
|
||||||
|
hooks: {
|
||||||
|
onBuildStarting: [() => import('@adonisjs/vite/build_hook')],
|
||||||
})
|
},
|
||||||
|
// assetsBundler: false
|
||||||
|
});
|
||||||
|
|
|
@ -85,7 +85,9 @@ export default class AdminuserController {
|
||||||
// return response.badRequest(error.messages);
|
// return response.badRequest(error.messages);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const input = request.only(['login', 'email', 'password', 'first_name', 'last_name']);
|
|
||||||
|
const input: Record<string, any> = request.only(['login', 'email','first_name', 'last_name']);
|
||||||
|
input.password = request.input('new_password');
|
||||||
const user = await User.create(input);
|
const user = await User.create(input);
|
||||||
if (request.input('roles')) {
|
if (request.input('roles')) {
|
||||||
const roles: Array<number> = request.input('roles');
|
const roles: Array<number> = request.input('roles');
|
||||||
|
@ -95,7 +97,6 @@ export default class AdminuserController {
|
||||||
session.flash('message', 'User has been created successfully');
|
session.flash('message', 'User has been created successfully');
|
||||||
return response.redirect().toRoute('settings.user.index');
|
return response.redirect().toRoute('settings.user.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async show({ request, inertia }: HttpContext) {
|
public async show({ request, inertia }: HttpContext) {
|
||||||
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();
|
||||||
|
@ -139,9 +140,11 @@ export default class AdminuserController {
|
||||||
});
|
});
|
||||||
|
|
||||||
// password is optional
|
// password is optional
|
||||||
let input;
|
let input: Record<string, any>;
|
||||||
if (request.input('password')) {
|
|
||||||
input = request.only(['login', 'email', 'password', 'first_name', 'last_name']);
|
if (request.input('new_password')) {
|
||||||
|
input = request.only(['login', 'email', 'first_name', 'last_name']);
|
||||||
|
input.password = request.input('new_password');
|
||||||
} else {
|
} else {
|
||||||
input = request.only(['login', 'email', 'first_name', 'last_name']);
|
input = request.only(['login', 'email', 'first_name', 'last_name']);
|
||||||
}
|
}
|
||||||
|
@ -156,7 +159,6 @@ export default class AdminuserController {
|
||||||
session.flash('message', 'User has been updated successfully');
|
session.flash('message', 'User has been updated successfully');
|
||||||
return response.redirect().toRoute('settings.user.index');
|
return response.redirect().toRoute('settings.user.index');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async destroy({ request, response, session }: HttpContext) {
|
public async destroy({ request, response, session }: HttpContext) {
|
||||||
const id = request.param('id');
|
const id = request.param('id');
|
||||||
const user = await User.findOrFail(id);
|
const user = await User.findOrFail(id);
|
||||||
|
|
|
@ -25,6 +25,7 @@ export default class MimetypeController {
|
||||||
const newDatasetSchema = vine.object({
|
const newDatasetSchema = vine.object({
|
||||||
name: vine.string().trim().isUnique({ table: 'mime_types', column: 'name' }),
|
name: vine.string().trim().isUnique({ table: 'mime_types', column: 'name' }),
|
||||||
file_extension: vine.array(vine.string()).minLength(1), // define at least one extension for the new mimetype
|
file_extension: vine.array(vine.string()).minLength(1), // define at least one extension for the new mimetype
|
||||||
|
alternate_mimetype: vine.array(vine.string().isValidMimetype()).distinct().optional(), // define alias mimetypes
|
||||||
enabled: vine.boolean(),
|
enabled: vine.boolean(),
|
||||||
});
|
});
|
||||||
// await request.validate({ schema: newDatasetSchema, messages: this.messages });
|
// await request.validate({ schema: newDatasetSchema, messages: this.messages });
|
||||||
|
@ -33,17 +34,21 @@ export default class MimetypeController {
|
||||||
// await request.validate({ schema: newDatasetSchema, messages: this.messages });
|
// await request.validate({ schema: newDatasetSchema, messages: this.messages });
|
||||||
const validator = vine.compile(newDatasetSchema);
|
const validator = vine.compile(newDatasetSchema);
|
||||||
validator.messagesProvider = new SimpleMessagesProvider(this.messages);
|
validator.messagesProvider = new SimpleMessagesProvider(this.messages);
|
||||||
await request.validateUsing(validator);
|
await request.validateUsing(validator, { messagesProvider: new SimpleMessagesProvider(this.messages) });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Step 3 - Handle errors
|
// Step 3 - Handle errors
|
||||||
// return response.badRequest(error.messages);
|
// return response.badRequest(error.messages);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
const input = request.only(['name', 'enabled', 'file_extension']);
|
const input = request.only(['name', 'enabled', 'file_extension', 'alternate_mimetype']);
|
||||||
// Concatenate the file_extensions array into a string with '|' as the separator
|
// Concatenate the file_extensions array into a string with '|' as the separator
|
||||||
if (Array.isArray(input.file_extension)) {
|
if (Array.isArray(input.file_extension)) {
|
||||||
input.file_extension = input.file_extension.join('|');
|
input.file_extension = input.file_extension.join('|');
|
||||||
}
|
}
|
||||||
|
// Concatenate the alias_mimetype array into a string with '|' as the separator
|
||||||
|
if (Array.isArray(input.alternate_mimetype)) {
|
||||||
|
input.alternate_mimetype = input.alternate_mimetype.join('|');
|
||||||
|
}
|
||||||
await MimeType.create(input);
|
await MimeType.create(input);
|
||||||
// if (request.input('roles')) {
|
// if (request.input('roles')) {
|
||||||
// const roles: Array<number> = request.input('roles');
|
// const roles: Array<number> = request.input('roles');
|
||||||
|
|
|
@ -9,12 +9,15 @@ export default class AuthorsController {
|
||||||
// where exists (select * from gba.documents inner join gba.link_documents_persons on "documents"."id" = "link_documents_persons"."document_id"
|
// where exists (select * from gba.documents inner join gba.link_documents_persons on "documents"."id" = "link_documents_persons"."document_id"
|
||||||
// where ("link_documents_persons"."role" = 'author') and ("persons"."id" = "link_documents_persons"."person_id"));
|
// where ("link_documents_persons"."role" = 'author') and ("persons"."id" = "link_documents_persons"."person_id"));
|
||||||
const authors = await Person.query()
|
const authors = await Person.query()
|
||||||
|
.preload('datasets')
|
||||||
|
.where('name_type', 'Personal')
|
||||||
.whereHas('datasets', (dQuery) => {
|
.whereHas('datasets', (dQuery) => {
|
||||||
dQuery.wherePivot('role', 'author');
|
dQuery.wherePivot('role', 'author');
|
||||||
})
|
})
|
||||||
.withCount('datasets', (query) => {
|
.withCount('datasets', (query) => {
|
||||||
query.as('datasets_count');
|
query.as('datasets_count');
|
||||||
});
|
})
|
||||||
|
.orderBy('datasets_count', 'desc');
|
||||||
|
|
||||||
return authors;
|
return authors;
|
||||||
}
|
}
|
||||||
|
@ -25,7 +28,10 @@ export default class AuthorsController {
|
||||||
if (request.input('filter')) {
|
if (request.input('filter')) {
|
||||||
// users = users.whereRaw('name like %?%', [request.input('search')])
|
// users = users.whereRaw('name like %?%', [request.input('search')])
|
||||||
const searchTerm = request.input('filter');
|
const searchTerm = request.input('filter');
|
||||||
authors.whereILike('first_name', `%${searchTerm}%`).orWhereILike('last_name', `%${searchTerm}%`);
|
authors.andWhere((query) => {
|
||||||
|
query.whereILike('first_name', `%${searchTerm}%`)
|
||||||
|
.orWhereILike('last_name', `%${searchTerm}%`);
|
||||||
|
});
|
||||||
// .orWhere('email', 'like', `%${searchTerm}%`);
|
// .orWhere('email', 'like', `%${searchTerm}%`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,65 +1,135 @@
|
||||||
import type { HttpContext } from '@adonisjs/core/http';
|
import type { HttpContext } from '@adonisjs/core/http';
|
||||||
import { StatusCodes } from 'http-status-codes';
|
import { StatusCodes } from 'http-status-codes';
|
||||||
// import * as fs from 'fs';
|
import redis from '@adonisjs/redis/services/main';
|
||||||
// import * as path from 'path';
|
|
||||||
|
|
||||||
const prefixes = ['von', 'van'];
|
const PREFIXES = ['von', 'van'];
|
||||||
|
const DEFAULT_SIZE = 50;
|
||||||
|
const FONT_SIZE_RATIO = 0.4;
|
||||||
|
const COLOR_LIGHTENING_PERCENT = 60;
|
||||||
|
const COLOR_DARKENING_FACTOR = 0.6;
|
||||||
|
|
||||||
// node ace make:controller Author
|
|
||||||
export default class AvatarController {
|
export default class AvatarController {
|
||||||
public async generateAvatar({ request, response }: HttpContext) {
|
public async generateAvatar({ request, response }: HttpContext) {
|
||||||
try {
|
try {
|
||||||
const { name, background, textColor, size } = request.only(['name', 'background', 'textColor', 'size']);
|
const { name, size = DEFAULT_SIZE } = request.only(['name', 'size']);
|
||||||
|
if (!name) {
|
||||||
|
return response.status(StatusCodes.BAD_REQUEST).json({ error: 'Name is required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a unique cache key for the given name and size
|
||||||
|
const cacheKey = `avatar:${name.trim().toLowerCase()}-${size}`;
|
||||||
|
const cachedSvg = await redis.get(cacheKey);
|
||||||
|
if (cachedSvg) {
|
||||||
|
this.setResponseHeaders(response);
|
||||||
|
return response.send(cachedSvg);
|
||||||
|
}
|
||||||
|
|
||||||
// Generate initials
|
|
||||||
// const initials = name
|
|
||||||
// .split(' ')
|
|
||||||
// .map((part) => part.charAt(0).toUpperCase())
|
|
||||||
// .join('');
|
|
||||||
const initials = this.getInitials(name);
|
const initials = this.getInitials(name);
|
||||||
|
const colors = this.generateColors(name);
|
||||||
|
const svgContent = this.createSvg(size, colors, initials);
|
||||||
|
|
||||||
// Define SVG content with dynamic values for initials, background color, text color, and size
|
// // Cache the generated avatar for future use, e.g. 1 hour expiry
|
||||||
const svgContent = `
|
await redis.setex(cacheKey, 3600, svgContent);
|
||||||
<svg width="${size || 50}" height="${size || 50}" xmlns="http://www.w3.org/2000/svg">
|
|
||||||
<rect width="100%" height="100%" fill="#${background || '7F9CF5'}"/>
|
|
||||||
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-weight="bold" font-family="Arial, sans-serif" font-size="${
|
|
||||||
(size / 100) * 40 || 25
|
|
||||||
}" fill="#${textColor || 'ffffff'}">${initials}</text>
|
|
||||||
</svg>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Set response headers for SVG content
|
|
||||||
response.header('Content-type', 'image/svg+xml');
|
|
||||||
response.header('Cache-Control', 'no-cache');
|
|
||||||
response.header('Pragma', 'no-cache');
|
|
||||||
response.header('Expires', '0');
|
|
||||||
|
|
||||||
|
this.setResponseHeaders(response);
|
||||||
return response.send(svgContent);
|
return response.send(svgContent);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return response.status(StatusCodes.OK).json({ error: error.message });
|
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ error: error.message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private getInitials(name: string) {
|
private getInitials(name: string): string {
|
||||||
const parts = name.split(' ');
|
const parts = name
|
||||||
let initials = '';
|
.trim()
|
||||||
|
.split(' ')
|
||||||
|
.filter((part) => part.length > 0);
|
||||||
|
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return 'NA';
|
||||||
|
}
|
||||||
|
|
||||||
if (parts.length >= 2) {
|
if (parts.length >= 2) {
|
||||||
const firstName = parts[0];
|
return this.getMultiWordInitials(parts);
|
||||||
const lastName = parts[parts.length - 1];
|
}
|
||||||
|
return parts[0].substring(0, 2).toUpperCase();
|
||||||
|
}
|
||||||
|
|
||||||
const firstInitial = firstName.charAt(0).toUpperCase();
|
private getMultiWordInitials(parts: string[]): string {
|
||||||
const lastInitial = lastName.charAt(0).toUpperCase();
|
const firstName = parts[0];
|
||||||
|
const lastName = parts[parts.length - 1];
|
||||||
|
const firstInitial = firstName.charAt(0).toUpperCase();
|
||||||
|
const lastInitial = lastName.charAt(0).toUpperCase();
|
||||||
|
|
||||||
if (prefixes.includes(lastName.toLowerCase()) && lastName === lastName.toUpperCase()) {
|
if (PREFIXES.includes(lastName.toLowerCase()) && lastName === lastName.toUpperCase()) {
|
||||||
initials = firstInitial + lastName.charAt(1).toUpperCase();
|
return firstInitial + lastName.charAt(1).toUpperCase();
|
||||||
} else {
|
}
|
||||||
initials = firstInitial + lastInitial;
|
return firstInitial + lastInitial;
|
||||||
}
|
}
|
||||||
} else if (parts.length === 1) {
|
|
||||||
initials = parts[0].substring(0, 2).toUpperCase();
|
private generateColors(name: string): { background: string; text: string } {
|
||||||
|
const baseColor = this.getColorFromName(name);
|
||||||
|
return {
|
||||||
|
background: this.lightenColor(baseColor, COLOR_LIGHTENING_PERCENT),
|
||||||
|
text: this.darkenColor(baseColor),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private createSvg(size: number, colors: { background: string; text: string }, initials: string): string {
|
||||||
|
const fontSize = size * FONT_SIZE_RATIO;
|
||||||
|
return `
|
||||||
|
<svg width="${size}" height="${size}" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<rect width="100%" height="100%" fill="#${colors.background}"/>
|
||||||
|
<text x="50%" y="50%" dominant-baseline="middle" text-anchor="middle" font-weight="bold" font-family="Arial, sans-serif" font-size="${fontSize}" fill="#${colors.text}">${initials}</text>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private setResponseHeaders(response: HttpContext['response']): void {
|
||||||
|
response.header('Content-type', 'image/svg+xml');
|
||||||
|
response.header('Cache-Control', 'no-cache');
|
||||||
|
response.header('Pragma', 'no-cache');
|
||||||
|
response.header('Expires', '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
private getColorFromName(name: string): string {
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < name.length; i++) {
|
||||||
|
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||||
}
|
}
|
||||||
|
|
||||||
return initials;
|
const colorParts = [];
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const value = (hash >> (i * 8)) & 0xff;
|
||||||
|
colorParts.push(value.toString(16).padStart(2, '0'));
|
||||||
|
}
|
||||||
|
return colorParts.join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
private lightenColor(hexColor: string, percent: number): string {
|
||||||
|
const r = parseInt(hexColor.substring(0, 2), 16);
|
||||||
|
const g = parseInt(hexColor.substring(2, 4), 16);
|
||||||
|
const b = parseInt(hexColor.substring(4, 6), 16);
|
||||||
|
|
||||||
|
const lightenValue = (value: number) => Math.min(255, Math.floor((value * (100 + percent)) / 100));
|
||||||
|
|
||||||
|
const newR = lightenValue(r);
|
||||||
|
const newG = lightenValue(g);
|
||||||
|
const newB = lightenValue(b);
|
||||||
|
|
||||||
|
return ((newR << 16) | (newG << 8) | newB).toString(16).padStart(6, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
private darkenColor(hexColor: string): string {
|
||||||
|
const r = parseInt(hexColor.slice(0, 2), 16);
|
||||||
|
const g = parseInt(hexColor.slice(2, 4), 16);
|
||||||
|
const b = parseInt(hexColor.slice(4, 6), 16);
|
||||||
|
|
||||||
|
const darkenValue = (value: number) => Math.round(value * COLOR_DARKENING_FACTOR);
|
||||||
|
|
||||||
|
const darkerR = darkenValue(r);
|
||||||
|
const darkerG = darkenValue(g);
|
||||||
|
const darkerB = darkenValue(b);
|
||||||
|
|
||||||
|
return ((darkerR << 16) + (darkerG << 8) + darkerB).toString(16).padStart(6, '0');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,15 @@ import { StatusCodes } from 'http-status-codes';
|
||||||
// node ace make:controller Author
|
// node ace make:controller Author
|
||||||
export default class DatasetController {
|
export default class DatasetController {
|
||||||
public async index({}: HttpContext) {
|
public async index({}: HttpContext) {
|
||||||
// select * from gba.persons
|
// Select datasets with server_state 'published' or 'deleted' and sort by the last published date
|
||||||
// where exists (select * from gba.documents inner join gba.link_documents_persons on "documents"."id" = "link_documents_persons"."document_id"
|
const datasets = await Dataset.query()
|
||||||
// where ("link_documents_persons"."role" = 'author') and ("persons"."id" = "link_documents_persons"."person_id"));
|
.where(function (query) {
|
||||||
const datasets = await Dataset.query().where('server_state', 'published').orWhere('server_state', 'deleted');
|
query.where('server_state', 'published')
|
||||||
|
.orWhere('server_state', 'deleted');
|
||||||
|
})
|
||||||
|
.preload('titles')
|
||||||
|
.preload('identifier')
|
||||||
|
.orderBy('server_date_published', 'desc');
|
||||||
|
|
||||||
return datasets;
|
return datasets;
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ export default class FileController {
|
||||||
// where: { id: id },
|
// where: { id: id },
|
||||||
// });
|
// });
|
||||||
if (file) {
|
if (file) {
|
||||||
const filePath = '/storage/app/public/' + file.pathName;
|
const filePath = '/storage/app/data/' + file.pathName;
|
||||||
const ext = path.extname(filePath);
|
const ext = path.extname(filePath);
|
||||||
const fileName = file.label + ext;
|
const fileName = file.label + ext;
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -17,7 +17,8 @@ export default class HomeController {
|
||||||
// .preload('authors')
|
// .preload('authors')
|
||||||
// .orderBy('server_date_published');
|
// .orderBy('server_date_published');
|
||||||
|
|
||||||
const datasets = await db.from('documents as doc')
|
const datasets = await db
|
||||||
|
.from('documents as doc')
|
||||||
.select(['publish_id', 'server_date_published', db.raw(`date_part('year', server_date_published) as pub_year`)])
|
.select(['publish_id', 'server_date_published', db.raw(`date_part('year', server_date_published) as pub_year`)])
|
||||||
.where('server_state', serverState)
|
.where('server_state', serverState)
|
||||||
.innerJoin('link_documents_persons as ba', 'doc.id', 'ba.document_id')
|
.innerJoin('link_documents_persons as ba', 'doc.id', 'ba.document_id')
|
||||||
|
@ -59,7 +60,6 @@ export default class HomeController {
|
||||||
// const year = params.year;
|
// const year = params.year;
|
||||||
// const from = parseInt(year);
|
// const from = parseInt(year);
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// const datasets = await Database.from('documents as doc')
|
// const datasets = await Database.from('documents as doc')
|
||||||
// .select([Database.raw(`date_part('month', server_date_published) as pub_month`), Database.raw('COUNT(*) as count')])
|
// .select([Database.raw(`date_part('month', server_date_published) as pub_month`), Database.raw('COUNT(*) as count')])
|
||||||
// .where('server_state', serverState)
|
// .where('server_state', serverState)
|
||||||
|
@ -68,9 +68,12 @@ export default class HomeController {
|
||||||
// .groupBy('pub_month');
|
// .groupBy('pub_month');
|
||||||
// // .orderBy('server_date_published');
|
// // .orderBy('server_date_published');
|
||||||
|
|
||||||
const years = [2021, 2022, 2023]; // Add the second year
|
// Calculate the last 4 years including the current year
|
||||||
|
const currentYear = new Date().getFullYear();
|
||||||
|
const years = Array.from({ length: 4 }, (_, i) => currentYear - (i + 1)).reverse();
|
||||||
|
|
||||||
const result = await db.from('documents as doc')
|
const result = await db
|
||||||
|
.from('documents as doc')
|
||||||
.select([
|
.select([
|
||||||
db.raw(`date_part('year', server_date_published) as pub_year`),
|
db.raw(`date_part('year', server_date_published) as pub_year`),
|
||||||
db.raw(`date_part('month', server_date_published) as pub_month`),
|
db.raw(`date_part('month', server_date_published) as pub_month`),
|
||||||
|
@ -100,15 +103,15 @@ export default class HomeController {
|
||||||
|
|
||||||
acc[pub_year].data[pub_month - 1] = parseInt(count);
|
acc[pub_year].data[pub_month - 1] = parseInt(count);
|
||||||
|
|
||||||
return acc ;
|
return acc;
|
||||||
}, {});
|
}, {});
|
||||||
|
|
||||||
const outputDatasets = Object.entries(inputDatasets).map(([year, data]) => ({
|
const outputDatasets = Object.entries(inputDatasets).map(([year, data]) => ({
|
||||||
data: data.data,
|
data: data.data,
|
||||||
label: year,
|
label: year,
|
||||||
borderColor: data.borderColor,
|
borderColor: data.borderColor,
|
||||||
fill: data.fill
|
fill: data.fill,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const data = {
|
const data = {
|
||||||
labels: labels,
|
labels: labels,
|
||||||
|
@ -139,5 +142,4 @@ interface ChartDataset {
|
||||||
label: string;
|
label: string;
|
||||||
borderColor: string;
|
borderColor: string;
|
||||||
fill: boolean;
|
fill: boolean;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,24 @@ 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 {
|
||||||
|
public async getSubmitters({ response }: HttpContext) {
|
||||||
|
try {
|
||||||
|
const submitters = await User.query()
|
||||||
|
.preload('roles', (query) => {
|
||||||
|
query.where('name', 'submitter')
|
||||||
|
})
|
||||||
|
.whereHas('roles', (query) => {
|
||||||
|
query.where('name', 'submitter')
|
||||||
|
})
|
||||||
|
.exec();
|
||||||
|
return submitters;
|
||||||
|
} catch (error) {
|
||||||
|
return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({
|
||||||
|
message: 'Invalid TOTP state',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async enable({ auth, response, request }: HttpContext) {
|
public async enable({ auth, response, request }: HttpContext) {
|
||||||
const user = (await User.find(auth.user?.id)) as User;
|
const user = (await User.find(auth.user?.id)) as User;
|
||||||
// await user.load('totp_secret');
|
// await user.load('totp_secret');
|
||||||
|
|
36
app/Controllers/Http/Api/collections_controller.ts
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import type { HttpContext } from '@adonisjs/core/http';
|
||||||
|
import Collection from '#models/collection';
|
||||||
|
|
||||||
|
export default class CollectionsController {
|
||||||
|
public async show({ params, response }: HttpContext) {
|
||||||
|
// Get the collection id from route parameters
|
||||||
|
const collectionId = params.id;
|
||||||
|
|
||||||
|
// Find the selected collection by id
|
||||||
|
const collection = await Collection.find(collectionId);
|
||||||
|
if (!collection) {
|
||||||
|
return response.status(404).json({ message: 'Collection not found' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query for narrower concepts: collections whose parent_id equals the selected collection's id
|
||||||
|
const narrowerCollections = await Collection.query().where('parent_id', collection.id) || [];
|
||||||
|
|
||||||
|
// For broader concept, if the selected collection has a parent_id fetch that record (otherwise null)
|
||||||
|
const broaderCollection: Collection[] | never[] | null = await (async () => {
|
||||||
|
if (collection.parent_id) {
|
||||||
|
// Try to fetch the parent...
|
||||||
|
const parent = await Collection.find(collection.parent_id)
|
||||||
|
// If found, return it wrapped in an array; if not found, return null (or empty array if you prefer)
|
||||||
|
return parent ? [parent] : null
|
||||||
|
}
|
||||||
|
return []
|
||||||
|
})()
|
||||||
|
|
||||||
|
// Return the selected collection along with its narrower and broader concepts in JSON format
|
||||||
|
return response.json({
|
||||||
|
selectedCollection: collection,
|
||||||
|
narrowerCollections,
|
||||||
|
broaderCollection,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,7 +5,7 @@ import BackupCode from '#models/backup_code';
|
||||||
// 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 hash from '@adonisjs/core/services/hash';
|
||||||
|
import db from '@adonisjs/lucid/services/db';
|
||||||
import TwoFactorAuthProvider from '#app/services/TwoFactorAuthProvider';
|
import TwoFactorAuthProvider from '#app/services/TwoFactorAuthProvider';
|
||||||
// import { Authenticator } from '@adonisjs/auth';
|
// import { Authenticator } from '@adonisjs/auth';
|
||||||
// import { LoginState } from 'Contracts/enums';
|
// import { LoginState } from 'Contracts/enums';
|
||||||
|
@ -29,6 +29,10 @@ export default class AuthController {
|
||||||
const { email, password } = request.only(['email', 'password']);
|
const { email, password } = request.only(['email', 'password']);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
|
await db.connection().rawQuery('SELECT 1')
|
||||||
|
|
||||||
|
|
||||||
// // attempt to verify credential and login user
|
// // attempt to verify credential and login user
|
||||||
// await auth.use('web').attempt(email, plainPassword);
|
// await auth.use('web').attempt(email, plainPassword);
|
||||||
|
|
||||||
|
@ -51,6 +55,9 @@ export default class AuthController {
|
||||||
|
|
||||||
await auth.use('web').login(user);
|
await auth.use('web').login(user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
if (error.code === 'ECONNREFUSED') {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
// if login fails, return vague form message and redirect back
|
// if login fails, return vague form message and redirect back
|
||||||
session.flash('message', 'Your username, email, or password is incorrect');
|
session.flash('message', 'Your username, email, or password is incorrect');
|
||||||
return response.redirect().back();
|
return response.redirect().back();
|
||||||
|
|
|
@ -6,6 +6,11 @@ import hash from '@adonisjs/core/services/hash';
|
||||||
// import { schema, rules } from '@adonisjs/validator';
|
// import { schema, rules } from '@adonisjs/validator';
|
||||||
import vine from '@vinejs/vine';
|
import vine from '@vinejs/vine';
|
||||||
import BackupCodeStorage, { SecureRandom } from '#services/backup_code_storage';
|
import BackupCodeStorage, { SecureRandom } from '#services/backup_code_storage';
|
||||||
|
import path from 'path';
|
||||||
|
import crypto from 'crypto';
|
||||||
|
// import drive from '#services/drive';
|
||||||
|
import drive from '@adonisjs/drive/services/main';
|
||||||
|
import logger from '@adonisjs/core/services/logger';
|
||||||
|
|
||||||
// Here we are generating secret and recovery codes for the user that’s enabling 2FA and storing them to our database.
|
// Here we are generating secret and recovery codes for the user that’s enabling 2FA and storing them to our database.
|
||||||
export default class UserController {
|
export default class UserController {
|
||||||
|
@ -28,7 +33,7 @@ export default class UserController {
|
||||||
user: user,
|
user: user,
|
||||||
twoFactorEnabled: user.isTwoFactorEnabled,
|
twoFactorEnabled: user.isTwoFactorEnabled,
|
||||||
// code: await TwoFactorAuthProvider.generateQrCode(user),
|
// code: await TwoFactorAuthProvider.generateQrCode(user),
|
||||||
backupState: backupState,
|
backupState: backupState,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,10 +45,8 @@ export default class UserController {
|
||||||
// });
|
// });
|
||||||
const passwordSchema = vine.object({
|
const passwordSchema = vine.object({
|
||||||
// first step
|
// first step
|
||||||
old_password: vine
|
old_password: vine.string().trim(),
|
||||||
.string()
|
// .regex(/^[a-zA-Z0-9]+$/),
|
||||||
.trim()
|
|
||||||
.regex(/^[a-zA-Z0-9]+$/),
|
|
||||||
new_password: vine.string().confirmed({ confirmationField: 'confirm_password' }).trim().minLength(8).maxLength(255),
|
new_password: vine.string().confirmed({ confirmationField: 'confirm_password' }).trim().minLength(8).maxLength(255),
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
|
@ -56,7 +59,7 @@ export default class UserController {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const user = await auth.user as User;
|
const user = (await auth.user) as User;
|
||||||
const { old_password, new_password } = request.only(['old_password', 'new_password']);
|
const { old_password, new_password } = request.only(['old_password', 'new_password']);
|
||||||
|
|
||||||
// if (!(old_password && new_password && confirm_password)) {
|
// if (!(old_password && new_password && confirm_password)) {
|
||||||
|
@ -82,6 +85,171 @@ export default class UserController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async profile({ inertia, auth }: HttpContext) {
|
||||||
|
const user = await User.find(auth.user?.id);
|
||||||
|
// let test = await drive.use().getUrl(user?.avatar);
|
||||||
|
// user?.preload('roles');
|
||||||
|
const avatarFullPathUrl = user?.avatar ? await drive.use('public').getUrl(user.avatar) : null;
|
||||||
|
return inertia.render('profile/show', {
|
||||||
|
user: user,
|
||||||
|
defaultUrl: avatarFullPathUrl,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the user's profile information.
|
||||||
|
*
|
||||||
|
* @param {HttpContext} ctx - The HTTP context object.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
public async profileUpdate({ auth, request, response, session }: HttpContext) {
|
||||||
|
if (!auth.user) {
|
||||||
|
session.flash('error', 'You must be logged in to update your profile.');
|
||||||
|
return response.redirect().toRoute('login');
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateProfileValidator = vine.withMetaData<{ userId: number }>().compile(
|
||||||
|
vine.object({
|
||||||
|
first_name: vine.string().trim().minLength(4).maxLength(255),
|
||||||
|
last_name: vine.string().trim().minLength(4).maxLength(255),
|
||||||
|
login: vine.string().trim().minLength(4).maxLength(255),
|
||||||
|
email: vine
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.maxLength(255)
|
||||||
|
.email()
|
||||||
|
.normalizeEmail()
|
||||||
|
.isUnique({ table: 'accounts', column: 'email', whereNot: (field) => field.meta.userId }),
|
||||||
|
avatar: vine
|
||||||
|
.myfile({
|
||||||
|
size: '2mb',
|
||||||
|
extnames: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'],
|
||||||
|
})
|
||||||
|
// .allowedMimetypeExtensions({
|
||||||
|
// allowedExtensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'],
|
||||||
|
// })
|
||||||
|
.optional(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const user = await User.find(auth.user.id);
|
||||||
|
if (!user) {
|
||||||
|
session.flash('error', 'User not found.');
|
||||||
|
return response.redirect().toRoute('login');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// validate update form
|
||||||
|
await request.validateUsing(updateProfileValidator, {
|
||||||
|
meta: {
|
||||||
|
userId: user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const { login, email, first_name, last_name } = request.only(['login', 'email', 'first_name', 'last_name']);
|
||||||
|
const sanitizedData: { [key: string]: any } = {
|
||||||
|
login: login?.trim(),
|
||||||
|
email: email?.toLowerCase().trim(),
|
||||||
|
first_name: first_name?.trim(),
|
||||||
|
last_name: last_name?.trim(),
|
||||||
|
// avatar: "",
|
||||||
|
};
|
||||||
|
const toCamelCase = (str: string) => str.replace(/_([a-z])/g, (g) => g[1].toUpperCase());
|
||||||
|
const hasInputChanges = Object.keys(sanitizedData).some((key) => {
|
||||||
|
const camelKey = toCamelCase(key);
|
||||||
|
return sanitizedData[key] !== (user.$attributes as { [key: string]: any })[camelKey];
|
||||||
|
});
|
||||||
|
|
||||||
|
let hasAvatarChanged = false;
|
||||||
|
const avatar = request.file('avatar');
|
||||||
|
if (avatar) {
|
||||||
|
const fileHash = crypto
|
||||||
|
.createHash('sha256')
|
||||||
|
.update(avatar.clientName + avatar.size)
|
||||||
|
.digest('hex');
|
||||||
|
const fileName = `avatar-${fileHash}.${avatar.extname}`;
|
||||||
|
const avatarFullPath = path.join('/uploads', `${user.login}`, fileName);
|
||||||
|
|
||||||
|
if (user.avatar != avatarFullPath) {
|
||||||
|
if (user.avatar) {
|
||||||
|
await drive.use('public').delete(user.avatar);
|
||||||
|
}
|
||||||
|
hasAvatarChanged = user.avatar !== avatarFullPath;
|
||||||
|
await avatar.moveToDisk(avatarFullPath, 'public', {
|
||||||
|
name: fileName,
|
||||||
|
overwrite: true, // overwrite in case of conflict
|
||||||
|
disk: 'public',
|
||||||
|
});
|
||||||
|
sanitizedData.avatar = avatarFullPath;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasInputChanges && !hasAvatarChanged) {
|
||||||
|
session.flash('message', 'No changes were made.');
|
||||||
|
return response.redirect().back();
|
||||||
|
}
|
||||||
|
|
||||||
|
await user.merge(sanitizedData).save();
|
||||||
|
session.flash('message', 'User has been updated successfully');
|
||||||
|
return response.redirect().toRoute('settings.profile.edit');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Profile update failed:', error);
|
||||||
|
// session.flash('errors', 'Profile update failed. Please try again.');
|
||||||
|
// return response.redirect().back();
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async passwordUpdate({ auth, request, response, session }: HttpContext) {
|
||||||
|
// const passwordSchema = schema.create({
|
||||||
|
// old_password: schema.string({ trim: true }, [rules.required()]),
|
||||||
|
// new_password: schema.string({ trim: true }, [rules.minLength(8), rules.maxLength(255), rules.confirmed('confirm_password')]),
|
||||||
|
// confirm_password: schema.string({ trim: true }, [rules.required()]),
|
||||||
|
// });
|
||||||
|
const passwordSchema = vine.object({
|
||||||
|
// first step
|
||||||
|
old_password: vine.string().trim(),
|
||||||
|
// .regex(/^[a-zA-Z0-9]+$/),
|
||||||
|
new_password: vine.string().confirmed({ confirmationField: 'confirm_password' }).trim().minLength(8).maxLength(255),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
// await request.validate({ schema: passwordSchema });
|
||||||
|
const validator = vine.compile(passwordSchema);
|
||||||
|
await request.validateUsing(validator);
|
||||||
|
} catch (error) {
|
||||||
|
// return response.badRequest(error.messages);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const user = (await auth.user) as User;
|
||||||
|
const { old_password, new_password } = request.only(['old_password', 'new_password']);
|
||||||
|
|
||||||
|
// if (!(old_password && new_password && confirm_password)) {
|
||||||
|
// return response.status(400).send({ warning: 'Old password and new password are required.' });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Verify if the provided old password matches the user's current password
|
||||||
|
const isSame = await hash.verify(user.password, old_password);
|
||||||
|
if (!isSame) {
|
||||||
|
session.flash('warning', 'Old password is incorrect.');
|
||||||
|
return response.redirect().back();
|
||||||
|
// return response.flash('warning', 'Old password is incorrect.').redirect().back();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash the new password before updating the user's password
|
||||||
|
user.password = new_password;
|
||||||
|
await user.save();
|
||||||
|
|
||||||
|
// return response.status(200).send({ message: 'Password updated successfully.' });
|
||||||
|
session.flash({ message: 'Password updated successfully.' });
|
||||||
|
return response.redirect().toRoute('settings.profile.edit');
|
||||||
|
} catch (error) {
|
||||||
|
// return response.status(500).send({ message: 'Internal server error.' });
|
||||||
|
return response.flash('warning', `Invalid server state. Internal server error.`).redirect().back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async enableTwoFactorAuthentication({ auth, response, session }: HttpContext): Promise<void> {
|
public async enableTwoFactorAuthentication({ auth, response, session }: HttpContext): Promise<void> {
|
||||||
// const user: User | undefined = auth?.user;
|
// const user: User | undefined = auth?.user;
|
||||||
const user = (await User.find(auth.user?.id)) as User;
|
const user = (await User.find(auth.user?.id)) as User;
|
||||||
|
|
|
@ -18,9 +18,33 @@ import { HttpException } from 'node-exceptions';
|
||||||
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
|
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
|
||||||
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
|
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
|
||||||
import mail from '@adonisjs/mail/services/main';
|
import mail from '@adonisjs/mail/services/main';
|
||||||
// import { resolveMx } from 'dns/promises';
|
|
||||||
// import * as net from 'net';
|
|
||||||
import { validate } from 'deep-email-validator';
|
import { validate } from 'deep-email-validator';
|
||||||
|
import {
|
||||||
|
TitleTypes,
|
||||||
|
DescriptionTypes,
|
||||||
|
ContributorTypes,
|
||||||
|
PersonNameTypes,
|
||||||
|
ReferenceIdentifierTypes,
|
||||||
|
RelationTypes,
|
||||||
|
SubjectTypes,
|
||||||
|
DatasetTypes,
|
||||||
|
} from '#contracts/enums';
|
||||||
|
import { TransactionClientContract } from '@adonisjs/lucid/types/database';
|
||||||
|
import db from '@adonisjs/lucid/services/db';
|
||||||
|
import Project from '#models/project';
|
||||||
|
import License from '#models/license';
|
||||||
|
import Language from '#models/language';
|
||||||
|
import File from '#models/file';
|
||||||
|
import Coverage from '#models/coverage';
|
||||||
|
import Title from '#models/title';
|
||||||
|
import Description from '#models/description';
|
||||||
|
import Subject from '#models/subject';
|
||||||
|
import DatasetReference from '#models/dataset_reference';
|
||||||
|
import Collection from '#models/collection';
|
||||||
|
import CollectionRole from '#models/collection_role';
|
||||||
|
import { updateEditorDatasetValidator } from '#validators/dataset';
|
||||||
|
import { savePersons } from '#app/utils/utility-functions';
|
||||||
|
|
||||||
// Create a new instance of the client
|
// Create a new instance of the client
|
||||||
const client = new Client({ node: 'http://localhost:9200' }); // replace with your OpenSearch endpoint
|
const client = new Client({ node: 'http://localhost:9200' }); // replace with your OpenSearch endpoint
|
||||||
|
|
||||||
|
@ -63,8 +87,15 @@ export default class DatasetsController {
|
||||||
}
|
}
|
||||||
datasets.orderBy(attribute, sortOrder);
|
datasets.orderBy(attribute, sortOrder);
|
||||||
} else {
|
} else {
|
||||||
// users.orderBy('created_at', 'desc');
|
// datasets.orderBy('id', 'asc');
|
||||||
datasets.orderBy('id', 'asc');
|
// Custom ordering to prioritize rejected_editor state
|
||||||
|
datasets.orderByRaw(`
|
||||||
|
CASE
|
||||||
|
WHEN server_state = 'rejected_reviewer' THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END ASC,
|
||||||
|
id ASC
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// const users = await User.query().orderBy('login').paginate(page, limit);
|
// const users = await User.query().orderBy('login').paginate(page, limit);
|
||||||
|
@ -217,6 +248,10 @@ export default class DatasetsController {
|
||||||
if (dataset.reject_reviewer_note != null) {
|
if (dataset.reject_reviewer_note != null) {
|
||||||
dataset.reject_reviewer_note = null;
|
dataset.reject_reviewer_note = null;
|
||||||
}
|
}
|
||||||
|
if (dataset.reject_editor_note != null) {
|
||||||
|
dataset.reject_editor_note = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
//save main and additional titles
|
//save main and additional titles
|
||||||
const reviewer_id = request.input('reviewer_id', null);
|
const reviewer_id = request.input('reviewer_id', null);
|
||||||
|
@ -255,70 +290,7 @@ export default class DatasetsController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// private async checkEmailDomain(email: string): Promise<boolean> {
|
|
||||||
// const domain = email.split('@')[1];
|
|
||||||
|
|
||||||
// try {
|
|
||||||
// // Step 1: Check MX records for the domain
|
|
||||||
// const mxRecords = await resolveMx(domain);
|
|
||||||
// if (mxRecords.length === 0) {
|
|
||||||
// return false; // No MX records, can't send email
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // Sort MX records by priority
|
|
||||||
// mxRecords.sort((a, b) => a.priority - b.priority);
|
|
||||||
|
|
||||||
// // Step 2: Attempt SMTP connection to the first available mail server
|
|
||||||
// const smtpServer = mxRecords[0].exchange;
|
|
||||||
|
|
||||||
// return await this.checkMailboxExists(smtpServer, email);
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('Error during MX lookup or SMTP validation:', error);
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
//// Helper function to check if the mailbox exists using SMTP
|
|
||||||
// private async checkMailboxExists(smtpServer: string, email: string): Promise<boolean> {
|
|
||||||
// return new Promise((resolve, reject) => {
|
|
||||||
// const socket = net.createConnection(25, smtpServer);
|
|
||||||
|
|
||||||
// socket.on('connect', () => {
|
|
||||||
// socket.write(`HELO ${smtpServer}\r\n`);
|
|
||||||
// socket.write(`MAIL FROM: <test@example.com>\r\n`);
|
|
||||||
// socket.write(`RCPT TO: <${email}>\r\n`);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socket.on('data', (data) => {
|
|
||||||
// const response = data.toString();
|
|
||||||
// if (response.includes('250')) {
|
|
||||||
// // 250 is an SMTP success code
|
|
||||||
// socket.end();
|
|
||||||
// resolve(true); // Email exists
|
|
||||||
// } else if (response.includes('550')) {
|
|
||||||
// // 550 means the mailbox doesn't exist
|
|
||||||
// socket.end();
|
|
||||||
// resolve(false); // Email doesn't exist
|
|
||||||
// }
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socket.on('error', (error) => {
|
|
||||||
// console.error('SMTP connection error:', error);
|
|
||||||
// socket.end();
|
|
||||||
// resolve(false);
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socket.on('end', () => {
|
|
||||||
// // SMTP connection closed
|
|
||||||
// });
|
|
||||||
|
|
||||||
// socket.setTimeout(5000, () => {
|
|
||||||
// // Timeout after 5 seconds
|
|
||||||
// socket.end();
|
|
||||||
// resolve(false); // Assume email doesn't exist if no response
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
|
|
||||||
public async rejectUpdate({ request, response, auth }: HttpContext) {
|
public async rejectUpdate({ request, response, auth }: HttpContext) {
|
||||||
const authUser = auth.user!;
|
const authUser = auth.user!;
|
||||||
|
@ -353,7 +325,7 @@ export default class DatasetsController {
|
||||||
return response
|
return response
|
||||||
.flash(
|
.flash(
|
||||||
`Invalid server state. Dataset with id ${id} cannot be rejected. Datset has server state ${dataset.server_state}.`,
|
`Invalid server state. Dataset with id ${id} cannot be rejected. Datset has server state ${dataset.server_state}.`,
|
||||||
'warning'
|
'warning',
|
||||||
)
|
)
|
||||||
.redirect()
|
.redirect()
|
||||||
.toRoute('editor.dataset.list');
|
.toRoute('editor.dataset.list');
|
||||||
|
@ -388,7 +360,9 @@ export default class DatasetsController {
|
||||||
emailStatusMessage = ` A rejection email was successfully sent to ${dataset.user.email}.`;
|
emailStatusMessage = ` A rejection email was successfully sent to ${dataset.user.email}.`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
return response.flash('Dataset has not been rejected due to an email error: ' + error.message, 'error').toRoute('editor.dataset.list');
|
return response
|
||||||
|
.flash('Dataset has not been rejected due to an email error: ' + error.message, 'error')
|
||||||
|
.toRoute('editor.dataset.list');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
emailStatusMessage = ` However, the email could not be sent because the submitter's email address (${dataset.user.email}) is not valid.`;
|
emailStatusMessage = ` However, the email could not be sent because the submitter's email address (${dataset.user.email}) is not valid.`;
|
||||||
|
@ -404,7 +378,7 @@ export default class DatasetsController {
|
||||||
.toRoute('editor.dataset.list');
|
.toRoute('editor.dataset.list');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async publish({ request, inertia, response }: HttpContext) {
|
public async publish({ request, inertia, response, auth }: HttpContext) {
|
||||||
const id = request.param('id');
|
const id = request.param('id');
|
||||||
|
|
||||||
const dataset = await Dataset.query()
|
const dataset = await Dataset.query()
|
||||||
|
@ -428,8 +402,14 @@ export default class DatasetsController {
|
||||||
.back();
|
.back();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return inertia.render('Editor/Dataset/Publish', {
|
return inertia.render('Editor/Dataset/Publish', {
|
||||||
dataset,
|
dataset,
|
||||||
|
can: {
|
||||||
|
reject: await auth.user?.can(['dataset-editor-reject']),
|
||||||
|
publish: await auth.user?.can(['dataset-publish']),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -471,6 +451,119 @@ export default class DatasetsController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async rejectToReviewer({ request, inertia, response }: HttpContext) {
|
||||||
|
const id = request.param('id');
|
||||||
|
const dataset = await Dataset.query()
|
||||||
|
.where('id', id)
|
||||||
|
.preload('reviewer', (builder) => {
|
||||||
|
builder.select('id', 'login', 'email');
|
||||||
|
})
|
||||||
|
.firstOrFail();
|
||||||
|
|
||||||
|
const validStates = ['reviewed'];
|
||||||
|
if (!validStates.includes(dataset.server_state)) {
|
||||||
|
// session.flash('errors', 'Invalid server state!');
|
||||||
|
return response
|
||||||
|
.flash(
|
||||||
|
'warning',
|
||||||
|
`Invalid server state. Dataset with id ${id} cannot be rejected to the reviewer. Datset has server state ${dataset.server_state}.`,
|
||||||
|
)
|
||||||
|
.redirect()
|
||||||
|
.toRoute('editor.dataset.list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return inertia.render('Editor/Dataset/RejectToReviewer', {
|
||||||
|
dataset,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async rejectToReviewerUpdate({ request, response, auth }: HttpContext) {
|
||||||
|
const authUser = auth.user!;
|
||||||
|
|
||||||
|
const id = request.param('id');
|
||||||
|
const dataset = await Dataset.query()
|
||||||
|
.where('id', id)
|
||||||
|
.preload('reviewer', (builder) => {
|
||||||
|
builder.select('id', 'login', 'email');
|
||||||
|
})
|
||||||
|
.firstOrFail();
|
||||||
|
|
||||||
|
const newSchema = vine.object({
|
||||||
|
server_state: vine.string().trim(),
|
||||||
|
reject_editor_note: vine.string().trim().minLength(10).maxLength(500),
|
||||||
|
send_mail: vine.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
// await request.validate({ schema: newSchema });
|
||||||
|
const validator = vine.compile(newSchema);
|
||||||
|
await request.validateUsing(validator);
|
||||||
|
} catch (error) {
|
||||||
|
// return response.badRequest(error.messages);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const validStates = ['reviewed'];
|
||||||
|
if (!validStates.includes(dataset.server_state)) {
|
||||||
|
// throw new Error('Invalid server state!');
|
||||||
|
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
|
||||||
|
return response
|
||||||
|
.flash(
|
||||||
|
`Invalid server state. Dataset with id ${id} cannot be rejected to reviewer. Datset has server state ${dataset.server_state}.`,
|
||||||
|
'warning',
|
||||||
|
)
|
||||||
|
.redirect()
|
||||||
|
.toRoute('editor.dataset.list');
|
||||||
|
}
|
||||||
|
|
||||||
|
dataset.server_state = 'rejected_to_reviewer';
|
||||||
|
const rejectEditorNote = request.input('reject_editor_note', '');
|
||||||
|
dataset.reject_editor_note = rejectEditorNote;
|
||||||
|
|
||||||
|
// add logic for sending reject message
|
||||||
|
const sendMail = request.input('send_email', false);
|
||||||
|
// const validRecipientEmail = await this.checkEmailDomain('arno.kaimbacher@outlook.at');
|
||||||
|
const validationResult = await validate({
|
||||||
|
email: dataset.reviewer.email,
|
||||||
|
validateSMTP: false,
|
||||||
|
});
|
||||||
|
const validRecipientEmail: boolean = validationResult.valid;
|
||||||
|
|
||||||
|
await dataset.save();
|
||||||
|
|
||||||
|
let emailStatusMessage = '';
|
||||||
|
if (sendMail == true) {
|
||||||
|
if (dataset.reviewer.email && validRecipientEmail) {
|
||||||
|
try {
|
||||||
|
await mail.send((message) => {
|
||||||
|
message.to(dataset.reviewer.email).subject('Dataset Rejection Notification').html(`
|
||||||
|
<p>Dear ${dataset.reviewer.login},</p>
|
||||||
|
<p>Your dataset with ID ${dataset.id} has been rejected.</p>
|
||||||
|
<p>Reason for rejection: ${rejectEditorNote}</p>
|
||||||
|
<p>Best regards,<br>Your Tethys editor: ${authUser.login}</p>
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
emailStatusMessage = ` A rejection email was successfully sent to ${dataset.reviewer.email}.`;
|
||||||
|
} catch (error) {
|
||||||
|
logger.error(error);
|
||||||
|
return response
|
||||||
|
.flash('Dataset has not been rejected due to an email error: ' + error.message, 'error')
|
||||||
|
.toRoute('editor.dataset.list');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emailStatusMessage = ` However, the email could not be sent because the submitter's email address (${dataset.reviewer.email}) is not valid.`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return response
|
||||||
|
.flash(
|
||||||
|
`You have successfully rejected dataset ${dataset.id} reviewed by ${dataset.reviewer.login}.${emailStatusMessage}`,
|
||||||
|
'message',
|
||||||
|
)
|
||||||
|
.toRoute('editor.dataset.list');
|
||||||
|
}
|
||||||
|
|
||||||
public async doiCreate({ request, inertia }: HttpContext) {
|
public async doiCreate({ request, inertia }: HttpContext) {
|
||||||
const id = request.param('id');
|
const id = request.param('id');
|
||||||
const dataset = await Dataset.query()
|
const dataset = await Dataset.query()
|
||||||
|
@ -536,10 +629,375 @@ export default class DatasetsController {
|
||||||
|
|
||||||
public async show({}: HttpContext) {}
|
public async show({}: HttpContext) {}
|
||||||
|
|
||||||
public async edit({}: HttpContext) {}
|
public async edit({ request, inertia, response }: HttpContext) {
|
||||||
|
const id = request.param('id');
|
||||||
|
const datasetQuery = Dataset.query().where('id', id);
|
||||||
|
datasetQuery
|
||||||
|
.preload('titles', (query) => query.orderBy('id', 'asc'))
|
||||||
|
.preload('descriptions', (query) => query.orderBy('id', 'asc'))
|
||||||
|
.preload('coverage')
|
||||||
|
.preload('licenses')
|
||||||
|
.preload('authors', (query) => query.orderBy('pivot_sort_order', 'asc'))
|
||||||
|
.preload('contributors', (query) => query.orderBy('pivot_sort_order', 'asc'))
|
||||||
|
// .preload('subjects')
|
||||||
|
.preload('subjects', (builder) => {
|
||||||
|
builder.orderBy('id', 'asc').withCount('datasets');
|
||||||
|
})
|
||||||
|
.preload('references')
|
||||||
|
.preload('files', (query) => {
|
||||||
|
query.orderBy('sort_order', 'asc'); // Sort by sort_order column
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataset = await datasetQuery.firstOrFail();
|
||||||
|
const validStates = ['editor_accepted', 'rejected_reviewer'];
|
||||||
|
if (!validStates.includes(dataset.server_state)) {
|
||||||
|
// session.flash('errors', 'Invalid server state!');
|
||||||
|
return response
|
||||||
|
.flash(
|
||||||
|
`Invalid server state. Dataset with id ${id} cannot be edited. Datset has server state ${dataset.server_state}.`,
|
||||||
|
'warning',
|
||||||
|
)
|
||||||
|
.toRoute('editor.dataset.list');
|
||||||
|
}
|
||||||
|
|
||||||
|
const titleTypes = Object.entries(TitleTypes)
|
||||||
|
.filter(([value]) => value !== 'Main')
|
||||||
|
.map(([key, value]) => ({ value: key, label: value }));
|
||||||
|
|
||||||
|
const descriptionTypes = Object.entries(DescriptionTypes)
|
||||||
|
.filter(([value]) => value !== 'Abstract')
|
||||||
|
.map(([key, value]) => ({ value: key, label: value }));
|
||||||
|
|
||||||
|
const languages = await Language.query().where('active', true).pluck('part1', 'part1');
|
||||||
|
|
||||||
|
// const contributorTypes = Config.get('enums.contributor_types');
|
||||||
|
const contributorTypes = Object.entries(ContributorTypes).map(([key, value]) => ({ value: key, label: value }));
|
||||||
|
|
||||||
|
// const nameTypes = Config.get('enums.name_types');
|
||||||
|
const nameTypes = Object.entries(PersonNameTypes).map(([key, value]) => ({ value: key, label: value }));
|
||||||
|
|
||||||
|
// const messages = await Database.table('messages')
|
||||||
|
// .pluck('help_text', 'metadata_element');
|
||||||
|
|
||||||
|
const projects = await Project.query().pluck('label', 'id');
|
||||||
|
|
||||||
|
const currentDate = new Date();
|
||||||
|
const currentYear = currentDate.getFullYear();
|
||||||
|
const years = Array.from({ length: currentYear - 1990 + 1 }, (_, index) => 1990 + index);
|
||||||
|
|
||||||
|
const licenses = await License.query().select('id', 'name_long').where('active', 'true').pluck('name_long', 'id');
|
||||||
|
// const userHasRoles = user.roles;
|
||||||
|
// const datasetHasLicenses = await dataset.related('licenses').query().pluck('id');
|
||||||
|
// const checkeds = dataset.licenses.first().id;
|
||||||
|
|
||||||
|
// const doctypes = {
|
||||||
|
// analysisdata: { label: 'Analysis', value: 'analysisdata' },
|
||||||
|
// measurementdata: { label: 'Measurements', value: 'measurementdata' },
|
||||||
|
// monitoring: 'Monitoring',
|
||||||
|
// remotesensing: 'Remote Sensing',
|
||||||
|
// gis: 'GIS',
|
||||||
|
// models: 'Models',
|
||||||
|
// mixedtype: 'Mixed Type',
|
||||||
|
// };
|
||||||
|
|
||||||
|
return inertia.render('Editor/Dataset/Edit', {
|
||||||
|
dataset,
|
||||||
|
titletypes: titleTypes,
|
||||||
|
descriptiontypes: descriptionTypes,
|
||||||
|
contributorTypes,
|
||||||
|
nameTypes,
|
||||||
|
languages,
|
||||||
|
// messages,
|
||||||
|
projects,
|
||||||
|
licenses,
|
||||||
|
// datasetHasLicenses: Object.keys(datasetHasLicenses).map((key) => datasetHasLicenses[key]), //convert object to array with license ids
|
||||||
|
// checkeds,
|
||||||
|
years,
|
||||||
|
// languages,
|
||||||
|
subjectTypes: SubjectTypes,
|
||||||
|
referenceIdentifierTypes: Object.entries(ReferenceIdentifierTypes).map(([key, value]) => ({ value: key, label: value })),
|
||||||
|
relationTypes: Object.entries(RelationTypes).map(([key, value]) => ({ value: key, label: value })),
|
||||||
|
doctypes: DatasetTypes,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async update({ request, response, session }: HttpContext) {
|
||||||
|
// Get the dataset id from the route parameter
|
||||||
|
const datasetId = request.param('id');
|
||||||
|
// Retrieve the dataset and load its existing files
|
||||||
|
const dataset = await Dataset.findOrFail(datasetId);
|
||||||
|
await dataset.load('files');
|
||||||
|
|
||||||
|
let trx: TransactionClientContract | null = null;
|
||||||
|
try {
|
||||||
|
await request.validateUsing(updateEditorDatasetValidator);
|
||||||
|
trx = await db.transaction();
|
||||||
|
// const user = (await User.find(auth.user?.id)) as User;
|
||||||
|
// await this.createDatasetAndAssociations(user, request, trx);
|
||||||
|
const dataset = await Dataset.findOrFail(datasetId);
|
||||||
|
|
||||||
|
// save the licenses
|
||||||
|
const licenses: number[] = request.input('licenses', []);
|
||||||
|
// await dataset.useTransaction(trx).related('licenses').sync(licenses);
|
||||||
|
await dataset.useTransaction(trx).related('licenses').sync(licenses);
|
||||||
|
|
||||||
|
// save authors and contributors
|
||||||
|
await dataset.useTransaction(trx).related('authors').sync([]);
|
||||||
|
await dataset.useTransaction(trx).related('contributors').sync([]);
|
||||||
|
await savePersons(dataset, request.input('authors', []), 'author', trx);
|
||||||
|
await savePersons(dataset, request.input('contributors', []), 'contributor', trx);
|
||||||
|
|
||||||
|
//save the titles:
|
||||||
|
const titles = request.input('titles', []);
|
||||||
|
// const savedTitles:Array<Title> = [];
|
||||||
|
for (const titleData of titles) {
|
||||||
|
if (titleData.id) {
|
||||||
|
const title = await Title.findOrFail(titleData.id);
|
||||||
|
title.value = titleData.value;
|
||||||
|
title.language = titleData.language;
|
||||||
|
title.type = titleData.type;
|
||||||
|
if (title.$isDirty) {
|
||||||
|
await title.useTransaction(trx).save();
|
||||||
|
// await dataset.useTransaction(trx).related('titles').save(title);
|
||||||
|
// savedTitles.push(title);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const title = new Title();
|
||||||
|
title.fill(titleData);
|
||||||
|
// savedTitles.push(title);
|
||||||
|
await dataset.useTransaction(trx).related('titles').save(title);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// save the abstracts
|
||||||
|
const descriptions = request.input('descriptions', []);
|
||||||
|
// const savedTitles:Array<Title> = [];
|
||||||
|
for (const descriptionData of descriptions) {
|
||||||
|
if (descriptionData.id) {
|
||||||
|
const description = await Description.findOrFail(descriptionData.id);
|
||||||
|
description.value = descriptionData.value;
|
||||||
|
description.language = descriptionData.language;
|
||||||
|
description.type = descriptionData.type;
|
||||||
|
if (description.$isDirty) {
|
||||||
|
await description.useTransaction(trx).save();
|
||||||
|
// await dataset.useTransaction(trx).related('titles').save(title);
|
||||||
|
// savedTitles.push(title);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const description = new Description();
|
||||||
|
description.fill(descriptionData);
|
||||||
|
// savedTitles.push(title);
|
||||||
|
await dataset.useTransaction(trx).related('descriptions').save(description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process all subjects/keywords from the request
|
||||||
|
const subjects = request.input('subjects');
|
||||||
|
for (const subjectData of subjects) {
|
||||||
|
// Case 1: Subject already exists in the database (has an ID)
|
||||||
|
if (subjectData.id) {
|
||||||
|
// Retrieve the existing subject
|
||||||
|
const existingSubject = await Subject.findOrFail(subjectData.id);
|
||||||
|
|
||||||
|
// Update subject properties from the request data
|
||||||
|
existingSubject.value = subjectData.value;
|
||||||
|
existingSubject.type = subjectData.type;
|
||||||
|
existingSubject.external_key = subjectData.external_key;
|
||||||
|
|
||||||
|
// Only save if there are actual changes
|
||||||
|
if (existingSubject.$isDirty) {
|
||||||
|
await existingSubject.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The relationship between dataset and subject is already established,
|
||||||
|
// so we don't need to attach it again
|
||||||
|
}
|
||||||
|
// Case 2: New subject being added (no ID)
|
||||||
|
else {
|
||||||
|
// Check if a subject with the same value and type already exists in the database
|
||||||
|
const subject = await Subject.firstOrNew({ value: subjectData.value, type: subjectData.type }, subjectData);
|
||||||
|
|
||||||
|
if (subject.$isNew === true) {
|
||||||
|
// If it's a completely new subject, create and associate it with the dataset
|
||||||
|
await dataset.useTransaction(trx).related('subjects').save(subject);
|
||||||
|
} else {
|
||||||
|
// If the subject already exists, just create the relationship
|
||||||
|
await dataset.useTransaction(trx).related('subjects').attach([subject.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const subjectsToDelete = request.input('subjectsToDelete', []);
|
||||||
|
for (const subjectData of subjectsToDelete) {
|
||||||
|
if (subjectData.id) {
|
||||||
|
// const subject = await Subject.findOrFail(subjectData.id);
|
||||||
|
const subject = await Subject.query()
|
||||||
|
.where('id', subjectData.id)
|
||||||
|
.preload('datasets', (builder) => {
|
||||||
|
builder.orderBy('id', 'asc');
|
||||||
|
})
|
||||||
|
.withCount('datasets')
|
||||||
|
.firstOrFail();
|
||||||
|
|
||||||
|
// Check if the subject is used by multiple datasets
|
||||||
|
if (subject.$extras.datasets_count > 1) {
|
||||||
|
// If used by multiple datasets, just detach it from the current dataset
|
||||||
|
await dataset.useTransaction(trx).related('subjects').detach([subject.id]);
|
||||||
|
} else {
|
||||||
|
// If only used by this dataset, delete the subject completely
|
||||||
|
|
||||||
|
await dataset.useTransaction(trx).related('subjects').detach([subject.id]);
|
||||||
|
await subject.useTransaction(trx).delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process references
|
||||||
|
const references = request.input('references', []);
|
||||||
|
// First, get existing references to determine which ones to update vs. create
|
||||||
|
const existingReferences = await dataset.related('references').query();
|
||||||
|
const existingReferencesMap: Map<number, DatasetReference> = new Map(existingReferences.map((ref) => [ref.id, ref]));
|
||||||
|
|
||||||
|
for (const referenceData of references) {
|
||||||
|
if (existingReferencesMap.has(referenceData.id) && referenceData.id) {
|
||||||
|
// Update existing reference
|
||||||
|
const reference = existingReferencesMap.get(referenceData.id);
|
||||||
|
if (reference) {
|
||||||
|
reference.merge(referenceData);
|
||||||
|
if (reference.$isDirty) {
|
||||||
|
await reference.useTransaction(trx).save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Create new reference
|
||||||
|
const dataReference = new DatasetReference();
|
||||||
|
dataReference.fill(referenceData);
|
||||||
|
await dataset.useTransaction(trx).related('references').save(dataReference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle references to delete if provided
|
||||||
|
const referencesToDelete = request.input('referencesToDelete', []);
|
||||||
|
for (const referenceData of referencesToDelete) {
|
||||||
|
if (referenceData.id) {
|
||||||
|
const reference = await DatasetReference.findOrFail(referenceData.id);
|
||||||
|
await reference.useTransaction(trx).delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// save coverage
|
||||||
|
const coverageData = request.input('coverage');
|
||||||
|
if (coverageData) {
|
||||||
|
if (coverageData.id) {
|
||||||
|
const coverage = await Coverage.findOrFail(coverageData.id);
|
||||||
|
coverage.merge(coverageData);
|
||||||
|
if (coverage.$isDirty) {
|
||||||
|
await coverage.useTransaction(trx).save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const input = request.only(['project_id', 'embargo_date', 'language', 'type', 'creating_corporation']);
|
||||||
|
// dataset.type = request.input('type');
|
||||||
|
dataset.merge(input);
|
||||||
|
// let test: boolean = dataset.$isDirty;
|
||||||
|
await dataset.useTransaction(trx).save();
|
||||||
|
|
||||||
|
await trx.commit();
|
||||||
|
// console.log('Dataset has been updated successfully');
|
||||||
|
|
||||||
|
session.flash('message', 'Dataset has been updated successfully');
|
||||||
|
// return response.redirect().toRoute('user.index');
|
||||||
|
return response.redirect().toRoute('editor.dataset.edit', [dataset.id]);
|
||||||
|
} catch (error) {
|
||||||
|
if (trx !== null) {
|
||||||
|
await trx.rollback();
|
||||||
|
}
|
||||||
|
console.error('Failed to update dataset and related models:', error);
|
||||||
|
// throw new ValidationException(true, { 'upload error': `failed to create dataset and related models. ${error}` });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async categorize({ inertia, request, response }: HttpContext) {
|
||||||
|
const id = request.param('id');
|
||||||
|
// Preload dataset and its "collections" relation
|
||||||
|
const dataset = await Dataset.query().where('id', id).preload('collections').firstOrFail();
|
||||||
|
const validStates = ['editor_accepted', 'rejected_reviewer'];
|
||||||
|
if (!validStates.includes(dataset.server_state)) {
|
||||||
|
// session.flash('errors', 'Invalid server state!');
|
||||||
|
return response
|
||||||
|
.flash(
|
||||||
|
'warning',
|
||||||
|
`Invalid server state. Dataset with id ${id} cannot be edited. Datset has server state ${dataset.server_state}.`,
|
||||||
|
)
|
||||||
|
.redirect()
|
||||||
|
.toRoute('editor.dataset.list');
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionRoles = await CollectionRole.query()
|
||||||
|
.preload('collections', (coll: Collection) => {
|
||||||
|
// preloa only top level collection with noparent_id
|
||||||
|
coll.whereNull('parent_id').orderBy('number', 'asc');
|
||||||
|
})
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
return inertia.render('Editor/Dataset/Category', {
|
||||||
|
collectionRoles: collectionRoles,
|
||||||
|
dataset: dataset,
|
||||||
|
relatedCollections: dataset.collections,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async categorizeUpdate({ request, response, session }: HttpContext) {
|
||||||
|
// Get the dataset id from the route parameter
|
||||||
|
const id = request.param('id');
|
||||||
|
const dataset = await Dataset.query().preload('files').where('id', id).firstOrFail();
|
||||||
|
|
||||||
|
const validStates = ['editor_accepted', 'rejected_reviewer'];
|
||||||
|
if (!validStates.includes(dataset.server_state)) {
|
||||||
|
return response
|
||||||
|
.flash(
|
||||||
|
'warning',
|
||||||
|
`Invalid server state. Dataset with id ${id} cannot be categorized. Dataset has server state ${dataset.server_state}.`,
|
||||||
|
)
|
||||||
|
.redirect()
|
||||||
|
.toRoute('editor.dataset.list');
|
||||||
|
}
|
||||||
|
|
||||||
|
let trx: TransactionClientContract | null = null;
|
||||||
|
try {
|
||||||
|
trx = await db.transaction();
|
||||||
|
// const user = (await User.find(auth.user?.id)) as User;
|
||||||
|
// await this.createDatasetAndAssociations(user, request, trx);
|
||||||
|
|
||||||
|
// Retrieve the selected collections from the request.
|
||||||
|
// This should be an array of collection ids.
|
||||||
|
const collections: number[] = request.input('collections', []);
|
||||||
|
|
||||||
|
// Synchronize the dataset collections using the transaction.
|
||||||
|
await dataset.useTransaction(trx).related('collections').sync(collections);
|
||||||
|
|
||||||
|
// Commit the transaction.await trx.commit()
|
||||||
|
await trx.commit();
|
||||||
|
|
||||||
|
// Redirect with a success flash message.
|
||||||
|
// return response.flash('success', 'Dataset collections updated successfully!').redirect().toRoute('dataset.list');
|
||||||
|
|
||||||
|
session.flash('message', 'Dataset collections updated successfully!');
|
||||||
|
return response.redirect().toRoute('editor.dataset.list');
|
||||||
|
} catch (error) {
|
||||||
|
if (trx !== null) {
|
||||||
|
await trx.rollback();
|
||||||
|
}
|
||||||
|
console.error('Failed tocatgorize dataset collections:', error);
|
||||||
|
// throw new ValidationException(true, { 'upload error': `failed to create dataset and related models. ${error}` });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// public async update({}: HttpContextContract) {}
|
// public async update({}: HttpContextContract) {}
|
||||||
public async update({ response }: HttpContext) {
|
public async updateOpensearch({ response }: HttpContext) {
|
||||||
const id = 273; //request.param('id');
|
const id = 273; //request.param('id');
|
||||||
const dataset = await Dataset.query().preload('xmlCache').where('id', id).firstOrFail();
|
const dataset = await Dataset.query().preload('xmlCache').where('id', id).firstOrFail();
|
||||||
// add xml elements
|
// add xml elements
|
||||||
|
@ -655,6 +1113,19 @@ export default class DatasetsController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async download({ params, response }: HttpContext) {
|
||||||
|
const id = params.id;
|
||||||
|
// Find the file by ID
|
||||||
|
const file = await File.findOrFail(id);
|
||||||
|
// const filePath = await drive.use('local').getUrl('/'+ file.filePath)
|
||||||
|
const filePath = file.filePath;
|
||||||
|
const fileExt = file.filePath.split('.').pop() || '';
|
||||||
|
// Set the response headers and download the file
|
||||||
|
response.header('Content-Type', file.mime_type || 'application/octet-stream');
|
||||||
|
response.attachment(`${file.label}.${fileExt}`);
|
||||||
|
return response.download(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
public async destroy({}: HttpContext) {}
|
public async destroy({}: HttpContext) {}
|
||||||
|
|
||||||
private async createXmlRecord(dataset: Dataset, datasetNode: XMLBuilder) {
|
private async createXmlRecord(dataset: Dataset, datasetNode: XMLBuilder) {
|
||||||
|
|
|
@ -19,14 +19,13 @@ import XmlModel from '#app/Library/XmlModel';
|
||||||
import logger from '@adonisjs/core/services/logger';
|
import logger from '@adonisjs/core/services/logger';
|
||||||
import ResumptionToken from '#app/Library/Oai/ResumptionToken';
|
import ResumptionToken from '#app/Library/Oai/ResumptionToken';
|
||||||
// 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 { inject } from '@adonisjs/fold';
|
// import { inject } from '@adonisjs/fold';
|
||||||
import { inject } from '@adonisjs/core'
|
import { inject } from '@adonisjs/core';
|
||||||
// import { TokenWorkerContract } from "MyApp/Models/TokenWorker";
|
// import { TokenWorkerContract } from "MyApp/Models/TokenWorker";
|
||||||
import TokenWorkerContract from '#library/Oai/TokenWorkerContract';
|
import TokenWorkerContract from '#library/Oai/TokenWorkerContract';
|
||||||
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
|
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
|
||||||
|
|
||||||
|
|
||||||
interface XslTParameter {
|
interface XslTParameter {
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
@ -35,12 +34,14 @@ interface Dictionary {
|
||||||
[index: string]: string;
|
[index: string]: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ListParameter {
|
interface PagingParameter {
|
||||||
cursor: number;
|
cursor: number;
|
||||||
totalIds: number;
|
totalLength: number;
|
||||||
start: number;
|
start: number;
|
||||||
reldocIds: (number | null)[];
|
nextDocIds: number[];
|
||||||
|
activeWorkIds: number[];
|
||||||
metadataPrefix: string;
|
metadataPrefix: string;
|
||||||
|
queryParams: Object;
|
||||||
}
|
}
|
||||||
|
|
||||||
@inject()
|
@inject()
|
||||||
|
@ -49,6 +50,7 @@ export default class OaiController {
|
||||||
private sampleRegEx = /^[A-Za-zäüÄÜß0-9\-_.!~]+$/;
|
private sampleRegEx = /^[A-Za-zäüÄÜß0-9\-_.!~]+$/;
|
||||||
private xsltParameter: XslTParameter;
|
private xsltParameter: XslTParameter;
|
||||||
|
|
||||||
|
private firstPublishedDataset: Dataset | null;
|
||||||
/**
|
/**
|
||||||
* Holds xml representation of document information to be processed.
|
* Holds xml representation of document information to be processed.
|
||||||
*
|
*
|
||||||
|
@ -57,7 +59,6 @@ export default class OaiController {
|
||||||
private xml: XMLBuilder;
|
private xml: XMLBuilder;
|
||||||
private proc;
|
private proc;
|
||||||
|
|
||||||
|
|
||||||
constructor(public tokenWorker: TokenWorkerContract) {
|
constructor(public tokenWorker: TokenWorkerContract) {
|
||||||
// Load the XSLT file
|
// Load the XSLT file
|
||||||
this.proc = readFileSync('public/assets2/datasetxml2oai.sef.json');
|
this.proc = readFileSync('public/assets2/datasetxml2oai.sef.json');
|
||||||
|
@ -85,9 +86,9 @@ export default class OaiController {
|
||||||
let earliestDateFromDb;
|
let earliestDateFromDb;
|
||||||
// const oaiRequest: OaiParameter = request.body;
|
// const oaiRequest: OaiParameter = request.body;
|
||||||
try {
|
try {
|
||||||
const firstPublishedDataset: Dataset | null = await Dataset.earliestPublicationDate();
|
this.firstPublishedDataset = await Dataset.earliestPublicationDate();
|
||||||
firstPublishedDataset != null &&
|
this.firstPublishedDataset != null &&
|
||||||
(earliestDateFromDb = firstPublishedDataset.server_date_published.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"));
|
(earliestDateFromDb = this.firstPublishedDataset.server_date_published.toFormat("yyyy-MM-dd'T'HH:mm:ss'Z'"));
|
||||||
this.xsltParameter['earliestDatestamp'] = earliestDateFromDb;
|
this.xsltParameter['earliestDatestamp'] = earliestDateFromDb;
|
||||||
// start the request
|
// start the request
|
||||||
await this.handleRequest(oaiRequest, request);
|
await this.handleRequest(oaiRequest, request);
|
||||||
|
@ -162,22 +163,19 @@ export default class OaiController {
|
||||||
} else if (verb == 'GetRecord') {
|
} else if (verb == 'GetRecord') {
|
||||||
await this.handleGetRecord(oaiRequest);
|
await this.handleGetRecord(oaiRequest);
|
||||||
} else if (verb == 'ListRecords') {
|
} else if (verb == 'ListRecords') {
|
||||||
await this.handleListRecords(oaiRequest);
|
// Get browser fingerprint from the request:
|
||||||
|
const browserFingerprint = this.getBrowserFingerprint(request);
|
||||||
|
await this.handleListRecords(oaiRequest, browserFingerprint);
|
||||||
} else if (verb == 'ListIdentifiers') {
|
} else if (verb == 'ListIdentifiers') {
|
||||||
await this.handleListIdentifiers(oaiRequest);
|
// Get browser fingerprint from the request:
|
||||||
|
const browserFingerprint = this.getBrowserFingerprint(request);
|
||||||
|
await this.handleListIdentifiers(oaiRequest, browserFingerprint);
|
||||||
} else if (verb == 'ListSets') {
|
} else if (verb == 'ListSets') {
|
||||||
await this.handleListSets();
|
await this.handleListSets();
|
||||||
} else {
|
} else {
|
||||||
this.handleIllegalVerb();
|
this.handleIllegalVerb();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// // try {
|
|
||||||
// // console.log("Async code example.")
|
|
||||||
// const err = new PageNotFoundException("verb not found");
|
|
||||||
// throw err;
|
|
||||||
// // } catch (error) { // manually catching
|
|
||||||
// // next(error); // passing to default middleware error handler
|
|
||||||
// // }
|
|
||||||
throw new OaiModelException(
|
throw new OaiModelException(
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
'The verb provided in the request is illegal.',
|
'The verb provided in the request is illegal.',
|
||||||
|
@ -187,11 +185,11 @@ export default class OaiController {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected handleIdentify() {
|
protected handleIdentify() {
|
||||||
const email = process.env.OAI_EMAIL || 'repository@geosphere.at';
|
// Get configuration values from environment or a dedicated configuration service
|
||||||
const repositoryName = 'Tethys RDR';
|
const email = process.env.OAI_EMAIL ?? 'repository@geosphere.at';
|
||||||
const repIdentifier = 'tethys.at';
|
const repositoryName = process.env.OAI_REPOSITORY_NAME ?? 'Tethys RDR';
|
||||||
const sampleIdentifier = 'oai:' + repIdentifier + ':1'; //$this->_configuration->getSampleIdentifier();
|
const repIdentifier = process.env.OAI_REP_IDENTIFIER ?? 'tethys.at';
|
||||||
|
const sampleIdentifier = `oai:${repIdentifier}:1`;
|
||||||
// Dataset::earliestPublicationDate()->server_date_published->format('Y-m-d\TH:i:s\Z') : null;
|
// Dataset::earliestPublicationDate()->server_date_published->format('Y-m-d\TH:i:s\Z') : null;
|
||||||
// earliestDateFromDb!= null && (this.xsltParameter['earliestDatestamp'] = earliestDateFromDb?.server_date_published);
|
// earliestDateFromDb!= null && (this.xsltParameter['earliestDatestamp'] = earliestDateFromDb?.server_date_published);
|
||||||
|
|
||||||
|
@ -216,7 +214,7 @@ export default class OaiController {
|
||||||
|
|
||||||
const sets: { [key: string]: string } = {
|
const sets: { [key: string]: string } = {
|
||||||
'open_access': 'Set for open access licenses',
|
'open_access': 'Set for open access licenses',
|
||||||
'openaire_data': "OpenAIRE",
|
'openaire_data': 'OpenAIRE',
|
||||||
'doc-type:ResearchData': 'Set for document type ResearchData',
|
'doc-type:ResearchData': 'Set for document type ResearchData',
|
||||||
...(await this.getSetsForDatasetTypes()),
|
...(await this.getSetsForDatasetTypes()),
|
||||||
...(await this.getSetsForCollections()),
|
...(await this.getSetsForCollections()),
|
||||||
|
@ -234,7 +232,15 @@ export default class OaiController {
|
||||||
const repIdentifier = 'tethys.at';
|
const repIdentifier = 'tethys.at';
|
||||||
this.xsltParameter['repIdentifier'] = repIdentifier;
|
this.xsltParameter['repIdentifier'] = repIdentifier;
|
||||||
|
|
||||||
|
// Validate that required parameter exists early
|
||||||
|
if (!('identifier' in oaiRequest)) {
|
||||||
|
throw new BadOaiModelException('The prefix of the identifier argument is unknown.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and extract the dataset identifier from the request
|
||||||
const dataId = this.validateAndGetIdentifier(oaiRequest);
|
const dataId = this.validateAndGetIdentifier(oaiRequest);
|
||||||
|
|
||||||
|
// Retrieve dataset with associated XML cache and collection roles
|
||||||
const dataset = await Dataset.query()
|
const dataset = await Dataset.query()
|
||||||
.where('publish_id', dataId)
|
.where('publish_id', dataId)
|
||||||
.preload('xmlCache')
|
.preload('xmlCache')
|
||||||
|
@ -251,59 +257,61 @@ export default class OaiController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate and set the metadata prefix parameter
|
||||||
const metadataPrefix = this.validateAndGetMetadataPrefix(oaiRequest);
|
const metadataPrefix = this.validateAndGetMetadataPrefix(oaiRequest);
|
||||||
this.xsltParameter['oai_metadataPrefix'] = metadataPrefix;
|
this.xsltParameter['oai_metadataPrefix'] = metadataPrefix;
|
||||||
// do not deliver datasets which are restricted by document state defined in deliveringStates
|
|
||||||
|
// Ensure that the dataset is in an exportable state
|
||||||
this.validateDatasetState(dataset);
|
this.validateDatasetState(dataset);
|
||||||
|
|
||||||
// add xml elements
|
// Build the XML for the dataset record and add it to the root node
|
||||||
const datasetNode = this.xml.root().ele('Datasets');
|
const datasetNode = this.xml.root().ele('Datasets');
|
||||||
await this.createXmlRecord(dataset, datasetNode);
|
await this.createXmlRecord(dataset, datasetNode);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async handleListIdentifiers(oaiRequest: Dictionary) {
|
protected async handleListIdentifiers(oaiRequest: Dictionary, browserFingerprint: string) {
|
||||||
!this.tokenWorker.isConnected && (await this.tokenWorker.connect());
|
if (!this.tokenWorker.isConnected) {
|
||||||
|
await this.tokenWorker.connect();
|
||||||
|
}
|
||||||
|
|
||||||
const maxIdentifier: number = config.get('oai.max.listidentifiers', 100);
|
const maxIdentifier: number = config.get('oai.max.listidentifiers', 100);
|
||||||
await this.handleLists(oaiRequest, maxIdentifier);
|
await this.handleLists(oaiRequest, maxIdentifier, browserFingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async handleListRecords(oaiRequest: Dictionary) {
|
protected async handleListRecords(oaiRequest: Dictionary, browserFingerprint: string) {
|
||||||
!this.tokenWorker.isConnected && (await this.tokenWorker.connect());
|
if (!this.tokenWorker.isConnected) {
|
||||||
|
await this.tokenWorker.connect();
|
||||||
|
}
|
||||||
|
|
||||||
const maxRecords: number = config.get('oai.max.listrecords', 100);
|
const maxRecords: number = config.get('oai.max.listrecords', 100);
|
||||||
await this.handleLists(oaiRequest, maxRecords);
|
await this.handleLists(oaiRequest, maxRecords, browserFingerprint);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleLists(oaiRequest: Dictionary, maxRecords: number) {
|
private async handleLists(oaiRequest: Dictionary, maxRecords: number, browserFingerprint: string) {
|
||||||
maxRecords = maxRecords || 100;
|
|
||||||
const repIdentifier = 'tethys.at';
|
const repIdentifier = 'tethys.at';
|
||||||
this.xsltParameter['repIdentifier'] = repIdentifier;
|
this.xsltParameter['repIdentifier'] = repIdentifier;
|
||||||
const datasetNode = this.xml.root().ele('Datasets');
|
const datasetNode = this.xml.root().ele('Datasets');
|
||||||
|
|
||||||
// list initialisation
|
const paginationParams: PagingParameter ={
|
||||||
const numWrapper: ListParameter = {
|
|
||||||
cursor: 0,
|
cursor: 0,
|
||||||
totalIds: 0,
|
totalLength: 0,
|
||||||
start: maxRecords + 1,
|
start: maxRecords + 1,
|
||||||
reldocIds: [],
|
nextDocIds: [],
|
||||||
|
activeWorkIds: [],
|
||||||
metadataPrefix: '',
|
metadataPrefix: '',
|
||||||
|
queryParams: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
// resumptionToken is defined
|
|
||||||
if ('resumptionToken' in oaiRequest) {
|
if ('resumptionToken' in oaiRequest) {
|
||||||
await this.handleResumptionToken(oaiRequest, maxRecords, numWrapper);
|
await this.handleResumptionToken(oaiRequest, maxRecords, paginationParams);
|
||||||
} else {
|
} else {
|
||||||
// no resumptionToken is given
|
await this.handleNoResumptionToken(oaiRequest, paginationParams, maxRecords);
|
||||||
await this.handleNoResumptionToken(oaiRequest, numWrapper);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// handling of document ids
|
const nextIds: number[] = paginationParams.nextDocIds;
|
||||||
const restIds = numWrapper.reldocIds as number[];
|
const workIds: number[] = paginationParams.activeWorkIds;
|
||||||
const workIds = restIds.splice(0, maxRecords) as number[]; // array_splice(restIds, 0, maxRecords);
|
|
||||||
|
|
||||||
// no records returned
|
if (workIds.length === 0) {
|
||||||
if (workIds.length == 0) {
|
|
||||||
throw new OaiModelException(
|
throw new OaiModelException(
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
'The combination of the given values results in an empty list.',
|
'The combination of the given values results in an empty list.',
|
||||||
|
@ -311,169 +319,218 @@ export default class OaiController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const datasets: Dataset[] = await Dataset.query()
|
const datasets = await Dataset.query()
|
||||||
.whereIn('publish_id', workIds)
|
.whereIn('publish_id', workIds)
|
||||||
.preload('xmlCache')
|
.preload('xmlCache')
|
||||||
.preload('collections', (builder) => {
|
.preload('collections', (builder) => {
|
||||||
builder.preload('collectionRole');
|
builder.preload('collectionRole');
|
||||||
})
|
})
|
||||||
.orderBy('publish_id');
|
.orderBy('publish_id');
|
||||||
|
|
||||||
for (const dataset of datasets) {
|
for (const dataset of datasets) {
|
||||||
await this.createXmlRecord(dataset, datasetNode);
|
await this.createXmlRecord(dataset, datasetNode);
|
||||||
}
|
}
|
||||||
|
await this.setResumptionToken(nextIds, paginationParams, browserFingerprint);
|
||||||
// store the further Ids in a resumption-file
|
|
||||||
const countRestIds = restIds.length; //84
|
|
||||||
if (countRestIds > 0) {
|
|
||||||
const token = new ResumptionToken();
|
|
||||||
token.startPosition = numWrapper.start; //101
|
|
||||||
token.totalIds = numWrapper.totalIds; //184
|
|
||||||
token.documentIds = restIds; //101 -184
|
|
||||||
token.metadataPrefix = numWrapper.metadataPrefix;
|
|
||||||
|
|
||||||
// $tokenWorker->storeResumptionToken($token);
|
|
||||||
const res: string = await this.tokenWorker.set(token);
|
|
||||||
|
|
||||||
// set parameters for the resumptionToken-node
|
|
||||||
// const res = token.ResumptionId;
|
|
||||||
this.setParamResumption(res, numWrapper.cursor, numWrapper.totalIds);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleResumptionToken(oaiRequest: Dictionary, maxRecords: number, numWrapper: ListParameter) {
|
private async handleNoResumptionToken(oaiRequest: Dictionary, paginationParams: PagingParameter, maxRecords: number) {
|
||||||
const resParam = oaiRequest['resumptionToken']; //e.g. "158886496600000"
|
this.validateMetadataPrefix(oaiRequest, paginationParams);
|
||||||
|
const finder: ModelQueryBuilderContract<typeof Dataset, Dataset> = Dataset.query().whereIn(
|
||||||
|
'server_state',
|
||||||
|
this.deliveringDocumentStates,
|
||||||
|
);
|
||||||
|
this.applySetFilter(finder, oaiRequest);
|
||||||
|
this.applyDateFilters(finder, oaiRequest);
|
||||||
|
await this.fetchAndSetResults(finder, paginationParams, oaiRequest, maxRecords);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchAndSetResults(
|
||||||
|
finder: ModelQueryBuilderContract<typeof Dataset, Dataset>,
|
||||||
|
paginationParams: PagingParameter,
|
||||||
|
oaiRequest: Dictionary,
|
||||||
|
maxRecords: number
|
||||||
|
) {
|
||||||
|
const totalResult = await finder
|
||||||
|
.clone()
|
||||||
|
.count('* as total')
|
||||||
|
.first()
|
||||||
|
.then((res) => res?.$extras.total);
|
||||||
|
paginationParams.totalLength = Number(totalResult);
|
||||||
|
|
||||||
|
const combinedRecords: Dataset[] = await finder.select('publish_id').orderBy('publish_id').offset(0).limit(maxRecords*2);
|
||||||
|
|
||||||
|
paginationParams.activeWorkIds = combinedRecords.slice(0, 100).map((dat) => Number(dat.publish_id));
|
||||||
|
paginationParams.nextDocIds = combinedRecords.slice(100).map((dat) => Number(dat.publish_id));
|
||||||
|
|
||||||
|
// No resumption token was used – set queryParams from the current oaiRequest
|
||||||
|
paginationParams.queryParams = {
|
||||||
|
...oaiRequest,
|
||||||
|
deliveringStates: this.deliveringDocumentStates,
|
||||||
|
};
|
||||||
|
|
||||||
|
// paginationParams.totalLength = 230;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async handleResumptionToken(oaiRequest: Dictionary, maxRecords: number, paginationParams: PagingParameter) {
|
||||||
|
const resParam = oaiRequest['resumptionToken'];
|
||||||
const token = await this.tokenWorker.get(resParam);
|
const token = await this.tokenWorker.get(resParam);
|
||||||
|
|
||||||
if (!token) {
|
if (!token) {
|
||||||
throw new OaiModelException(StatusCodes.INTERNAL_SERVER_ERROR, 'cache is outdated.', OaiErrorCodes.BADRESUMPTIONTOKEN);
|
throw new OaiModelException(StatusCodes.INTERNAL_SERVER_ERROR, 'cache is outdated.', OaiErrorCodes.BADRESUMPTIONTOKEN);
|
||||||
}
|
}
|
||||||
|
|
||||||
numWrapper.cursor = token.startPosition - 1; //startet dann bei Index 10
|
// this.setResumptionParameters(token, maxRecords, paginationParams);
|
||||||
numWrapper.start = token.startPosition + maxRecords;
|
paginationParams.cursor = token.startPosition - 1;
|
||||||
numWrapper.totalIds = token.totalIds;
|
paginationParams.start = token.startPosition + maxRecords;
|
||||||
numWrapper.reldocIds = token.documentIds;
|
paginationParams.totalLength = token.totalIds;
|
||||||
numWrapper.metadataPrefix = token.metadataPrefix;
|
paginationParams.activeWorkIds = token.documentIds;
|
||||||
|
paginationParams.metadataPrefix = token.metadataPrefix;
|
||||||
|
paginationParams.queryParams = token.queryParams;
|
||||||
|
this.xsltParameter['oai_metadataPrefix'] = token.metadataPrefix;
|
||||||
|
|
||||||
this.xsltParameter['oai_metadataPrefix'] = numWrapper.metadataPrefix;
|
const finder = this.buildDatasetQueryViaToken(token);
|
||||||
|
const nextRecords: Dataset[] = await this.fetchNextRecords(finder, token, maxRecords);
|
||||||
|
paginationParams.nextDocIds = nextRecords.map((dat) => Number(dat.publish_id));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async handleNoResumptionToken(oaiRequest: Dictionary, numWrapper: ListParameter) {
|
private async setResumptionToken(nextIds: number[], paginationParams: PagingParameter, browserFingerprint: string) {
|
||||||
// no resumptionToken is given
|
const countRestIds = nextIds.length;
|
||||||
if ('metadataPrefix' in oaiRequest) {
|
if (countRestIds > 0) {
|
||||||
numWrapper.metadataPrefix = oaiRequest['metadataPrefix'];
|
// const token = this.createResumptionToken(paginationParams, nextIds);
|
||||||
} else {
|
const token = new ResumptionToken();
|
||||||
|
token.startPosition = paginationParams.start;
|
||||||
|
token.totalIds = paginationParams.totalLength;
|
||||||
|
token.documentIds = nextIds;
|
||||||
|
token.metadataPrefix = paginationParams.metadataPrefix;
|
||||||
|
token.queryParams = paginationParams.queryParams;
|
||||||
|
const res: string = await this.tokenWorker.set(token, browserFingerprint);
|
||||||
|
this.setParamResumption(res, paginationParams.cursor, paginationParams.totalLength);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildDatasetQueryViaToken(token: ResumptionToken) {
|
||||||
|
const finder = Dataset.query();
|
||||||
|
const originalQuery = token.queryParams || {};
|
||||||
|
const deliveringStates = originalQuery.deliveringStates || this.deliveringDocumentStates;
|
||||||
|
|
||||||
|
finder.whereIn('server_state', deliveringStates);
|
||||||
|
this.applySetFilter(finder, originalQuery);
|
||||||
|
this.applyDateFilters(finder, originalQuery);
|
||||||
|
|
||||||
|
return finder;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async fetchNextRecords(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, token: ResumptionToken, maxRecords: number) {
|
||||||
|
return finder
|
||||||
|
.select('publish_id')
|
||||||
|
.orderBy('publish_id')
|
||||||
|
.offset(token.startPosition - 1 + maxRecords)
|
||||||
|
.limit(100);
|
||||||
|
}
|
||||||
|
|
||||||
|
private validateMetadataPrefix(oaiRequest: Dictionary, paginationParams: PagingParameter) {
|
||||||
|
if (!('metadataPrefix' in oaiRequest)) {
|
||||||
throw new OaiModelException(
|
throw new OaiModelException(
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
'The prefix of the metadata argument is unknown.',
|
'The prefix of the metadata argument is unknown.',
|
||||||
OaiErrorCodes.BADARGUMENT,
|
OaiErrorCodes.BADARGUMENT,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
this.xsltParameter['oai_metadataPrefix'] = numWrapper.metadataPrefix;
|
paginationParams.metadataPrefix = oaiRequest['metadataPrefix'];
|
||||||
|
this.xsltParameter['oai_metadataPrefix'] = paginationParams.metadataPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
let finder: ModelQueryBuilderContract<typeof Dataset, Dataset> = Dataset.query();
|
private applySetFilter(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, queryParams: any) {
|
||||||
// add server state restrictions
|
if ('set' in queryParams) {
|
||||||
finder.whereIn('server_state', this.deliveringDocumentStates);
|
const [setType, setValue] = queryParams['set'].split(':');
|
||||||
if ('set' in oaiRequest) {
|
|
||||||
const set = oaiRequest['set'] as string;
|
|
||||||
const setArray = set.split(':');
|
|
||||||
|
|
||||||
if (setArray[0] == 'data-type') {
|
switch (setType) {
|
||||||
if (setArray.length == 2 && setArray[1]) {
|
case 'data-type':
|
||||||
finder.where('type', setArray[1]);
|
setValue && finder.where('type', setValue);
|
||||||
}
|
break;
|
||||||
} else if (setArray[0] == 'open_access') {
|
case 'open_access':
|
||||||
const openAccessLicences = ['CC-BY-4.0', 'CC-BY-SA-4.0'];
|
finder.andWhereHas('licenses', (query) => {
|
||||||
finder.andWhereHas('licenses', (query) => {
|
query.whereIn('name', ['CC-BY-4.0', 'CC-BY-SA-4.0']);
|
||||||
query.whereIn('name', openAccessLicences);
|
|
||||||
});
|
|
||||||
} else if (setArray[0] == 'ddc') {
|
|
||||||
if (setArray.length == 2 && setArray[1] != '') {
|
|
||||||
finder.andWhereHas('collections', (query) => {
|
|
||||||
query.where('number', setArray[1]);
|
|
||||||
});
|
});
|
||||||
}
|
break;
|
||||||
|
case 'ddc':
|
||||||
|
setValue &&
|
||||||
|
finder.andWhereHas('collections', (query) => {
|
||||||
|
query.where('number', setValue);
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// const timeZone = "Europe/Vienna"; // Canonical time zone name
|
private applyDateFilters(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, queryParams: any) {
|
||||||
// &from=2020-09-03&until2020-09-03
|
const { from, until } = queryParams;
|
||||||
// &from=2020-09-11&until=2021-05-11
|
|
||||||
if ('from' in oaiRequest && 'until' in oaiRequest) {
|
|
||||||
const from = oaiRequest['from'] as string;
|
|
||||||
let fromDate = dayjs(from); //.tz(timeZone);
|
|
||||||
const until = oaiRequest['until'] as string;
|
|
||||||
let untilDate = dayjs(until); //.tz(timeZone);
|
|
||||||
if (!fromDate.isValid() || !untilDate.isValid()) {
|
|
||||||
throw new OaiModelException(StatusCodes.INTERNAL_SERVER_ERROR, 'Date Parameter is not valid.', OaiErrorCodes.BADARGUMENT);
|
|
||||||
}
|
|
||||||
fromDate = dayjs.tz(from, 'Europe/Vienna');
|
|
||||||
untilDate = dayjs.tz(until, 'Europe/Vienna');
|
|
||||||
|
|
||||||
if (from.length != until.length) {
|
if (from && until) {
|
||||||
throw new OaiModelException(
|
this.handleFromUntilFilter(finder, from, until);
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
} else if (from) {
|
||||||
'The request has different granularities for the from and until parameters.',
|
this.handleFromFilter(finder, from);
|
||||||
OaiErrorCodes.BADARGUMENT,
|
} else if (until) {
|
||||||
);
|
this.handleUntilFilter(finder, until);
|
||||||
}
|
}
|
||||||
fromDate.hour() == 0 && (fromDate = fromDate.startOf('day'));
|
}
|
||||||
untilDate.hour() == 0 && (untilDate = untilDate.endOf('day'));
|
|
||||||
|
|
||||||
finder.whereBetween('server_date_published', [fromDate.format('YYYY-MM-DD HH:mm:ss'), untilDate.format('YYYY-MM-DD HH:mm:ss')]);
|
private handleFromUntilFilter(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, from: string, until: string) {
|
||||||
} else if ('from' in oaiRequest && !('until' in oaiRequest)) {
|
const fromDate = this.parseDateWithValidation(from, 'From');
|
||||||
const from = oaiRequest['from'] as string;
|
const untilDate = this.parseDateWithValidation(until, 'Until');
|
||||||
let fromDate = dayjs(from);
|
|
||||||
if (!fromDate.isValid()) {
|
|
||||||
throw new OaiModelException(
|
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
|
||||||
'From date parameter is not valid.',
|
|
||||||
OaiErrorCodes.BADARGUMENT,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fromDate = dayjs.tz(from, 'Europe/Vienna');
|
|
||||||
fromDate.hour() == 0 && (fromDate = fromDate.startOf('day'));
|
|
||||||
|
|
||||||
const now = dayjs();
|
if (from.length !== until.length) {
|
||||||
if (fromDate.isAfter(now)) {
|
throw new OaiModelException(
|
||||||
throw new OaiModelException(
|
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
'The request has different granularities for the from and until parameters.',
|
||||||
'Given from date is greater than now. The given values results in an empty list.',
|
OaiErrorCodes.BADARGUMENT,
|
||||||
OaiErrorCodes.NORECORDSMATCH,
|
);
|
||||||
);
|
|
||||||
} else {
|
|
||||||
finder.andWhere('server_date_published', '>=', fromDate.format('YYYY-MM-DD HH:mm:ss'));
|
|
||||||
}
|
|
||||||
} else if (!('from' in oaiRequest) && 'until' in oaiRequest) {
|
|
||||||
const until = oaiRequest['until'] as string;
|
|
||||||
let untilDate = dayjs(until);
|
|
||||||
if (!untilDate.isValid()) {
|
|
||||||
throw new OaiModelException(
|
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
|
||||||
'Until date parameter is not valid.',
|
|
||||||
OaiErrorCodes.BADARGUMENT,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
untilDate = dayjs.tz(until, 'Europe/Vienna');
|
|
||||||
untilDate.hour() == 0 && (untilDate = untilDate.endOf('day'));
|
|
||||||
|
|
||||||
const firstPublishedDataset: Dataset = (await Dataset.earliestPublicationDate()) as Dataset;
|
|
||||||
const earliestPublicationDate = dayjs(firstPublishedDataset.server_date_published.toISO()); //format("YYYY-MM-DDThh:mm:ss[Z]"));
|
|
||||||
if (earliestPublicationDate.isAfter(untilDate)) {
|
|
||||||
throw new OaiModelException(
|
|
||||||
StatusCodes.INTERNAL_SERVER_ERROR,
|
|
||||||
`earliestDatestamp is greater than given until date.
|
|
||||||
The given values results in an empty list.`,
|
|
||||||
OaiErrorCodes.NORECORDSMATCH,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
finder.andWhere('server_date_published', '<=', untilDate.format('YYYY-MM-DD HH:mm:ss'));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let reldocIdsDocs = await finder.select('publish_id').orderBy('publish_id');
|
finder.whereBetween('server_date_published', [fromDate.format('YYYY-MM-DD HH:mm:ss'), untilDate.format('YYYY-MM-DD HH:mm:ss')]);
|
||||||
numWrapper.reldocIds = reldocIdsDocs.map((dat) => dat.publish_id);
|
}
|
||||||
numWrapper.totalIds = numWrapper.reldocIds.length; //212
|
|
||||||
|
private handleFromFilter(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, from: string) {
|
||||||
|
const fromDate = this.parseDateWithValidation(from, 'From');
|
||||||
|
const now = dayjs();
|
||||||
|
|
||||||
|
if (fromDate.isAfter(now)) {
|
||||||
|
throw new OaiModelException(
|
||||||
|
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
|
'Given from date is greater than now. The given values results in an empty list.',
|
||||||
|
OaiErrorCodes.NORECORDSMATCH,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
finder.andWhere('server_date_published', '>=', fromDate.format('YYYY-MM-DD HH:mm:ss'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleUntilFilter(finder: ModelQueryBuilderContract<typeof Dataset, Dataset>, until: string) {
|
||||||
|
const untilDate = this.parseDateWithValidation(until, 'Until');
|
||||||
|
|
||||||
|
const earliestPublicationDate = dayjs(this.firstPublishedDataset?.server_date_published.toISO());
|
||||||
|
|
||||||
|
if (earliestPublicationDate.isAfter(untilDate)) {
|
||||||
|
throw new OaiModelException(
|
||||||
|
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
|
'earliestDatestamp is greater than given until date. The given values results in an empty list.',
|
||||||
|
OaiErrorCodes.NORECORDSMATCH,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
finder.andWhere('server_date_published', '<=', untilDate.format('YYYY-MM-DD HH:mm:ss'));
|
||||||
|
}
|
||||||
|
|
||||||
|
private parseDateWithValidation(dateStr: string, label: string) {
|
||||||
|
let date = dayjs(dateStr);
|
||||||
|
if (!date.isValid()) {
|
||||||
|
throw new OaiModelException(
|
||||||
|
StatusCodes.INTERNAL_SERVER_ERROR,
|
||||||
|
`${label} date parameter is not valid.`,
|
||||||
|
OaiErrorCodes.BADARGUMENT,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
date = dayjs.tz(dateStr, 'Europe/Vienna');
|
||||||
|
return date.hour() === 0 ? (label === 'From' ? date.startOf('day') : date.endOf('day')) : date;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setParamResumption(res: string, cursor: number, totalIds: number) {
|
private setParamResumption(res: string, cursor: number, totalIds: number) {
|
||||||
|
@ -641,4 +698,30 @@ export default class OaiController {
|
||||||
this.xsltParameter['oai_error_code'] = 'badVerb';
|
this.xsltParameter['oai_error_code'] = 'badVerb';
|
||||||
this.xsltParameter['oai_error_message'] = 'The verb provided in the request is illegal.';
|
this.xsltParameter['oai_error_message'] = 'The verb provided in the request is illegal.';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to build a browser fingerprint by combining:
|
||||||
|
* - User-Agent header,
|
||||||
|
* - the IP address,
|
||||||
|
* - Accept-Language header,
|
||||||
|
* - current timestamp rounded to the hour.
|
||||||
|
*
|
||||||
|
* Every new hour, this will return a different fingerprint.
|
||||||
|
*/
|
||||||
|
private getBrowserFingerprint(request: Request): string {
|
||||||
|
const userAgent = request.header('user-agent') || 'unknown';
|
||||||
|
// Check for X-Forwarded-For header to use the client IP from the proxy if available.
|
||||||
|
const xForwardedFor = request.header('x-forwarded-for');
|
||||||
|
let ip = request.ip();
|
||||||
|
// console.log(ip);
|
||||||
|
if (xForwardedFor) {
|
||||||
|
// X-Forwarded-For may contain a comma-separated list of IPs; the first one is the client IP.
|
||||||
|
ip = xForwardedFor.split(',')[0].trim();
|
||||||
|
// console.log('xforwardedfor ip' + ip);
|
||||||
|
}
|
||||||
|
const locale = request.header('accept-language') || 'default';
|
||||||
|
// Round the current time to the start of the hour.
|
||||||
|
const timestampHour = dayjs().startOf('hour').format('YYYY-MM-DDTHH');
|
||||||
|
return `${userAgent}-${ip}-${locale}-${timestampHour}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,6 +9,7 @@ import vine from '@vinejs/vine';
|
||||||
import mail from '@adonisjs/mail/services/main';
|
import mail from '@adonisjs/mail/services/main';
|
||||||
import logger from '@adonisjs/core/services/logger';
|
import logger from '@adonisjs/core/services/logger';
|
||||||
import { validate } from 'deep-email-validator';
|
import { validate } from 'deep-email-validator';
|
||||||
|
import File from '#models/file';
|
||||||
|
|
||||||
interface Dictionary {
|
interface Dictionary {
|
||||||
[index: string]: string;
|
[index: string]: string;
|
||||||
|
@ -38,13 +39,21 @@ export default class DatasetsController {
|
||||||
}
|
}
|
||||||
datasets.orderBy(attribute, sortOrder);
|
datasets.orderBy(attribute, sortOrder);
|
||||||
} else {
|
} else {
|
||||||
// users.orderBy('created_at', 'desc');
|
// datasets.orderBy('id', 'asc');
|
||||||
datasets.orderBy('id', 'asc');
|
// Custom ordering to prioritize rejected_editor state
|
||||||
|
datasets.orderByRaw(`
|
||||||
|
CASE
|
||||||
|
WHEN server_state = 'rejected_to_reviewer' THEN 0
|
||||||
|
ELSE 1
|
||||||
|
END ASC,
|
||||||
|
id ASC
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// const users = await User.query().orderBy('login').paginate(page, limit);
|
// const users = await User.query().orderBy('login').paginate(page, limit);
|
||||||
const myDatasets = await datasets
|
const myDatasets = await datasets
|
||||||
.where('server_state', 'approved')
|
// .where('server_state', 'approved')
|
||||||
|
.whereIn('server_state', ['approved', 'rejected_to_reviewer'])
|
||||||
.where('reviewer_id', user.id)
|
.where('reviewer_id', user.id)
|
||||||
|
|
||||||
.preload('titles')
|
.preload('titles')
|
||||||
|
@ -62,7 +71,52 @@ export default class DatasetsController {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async review({ request, inertia, response }: HttpContext) {
|
public async review({ request, inertia, response, auth }: HttpContext) {
|
||||||
|
const id = request.param('id');
|
||||||
|
const datasetQuery = Dataset.query().where('id', id);
|
||||||
|
|
||||||
|
datasetQuery
|
||||||
|
.preload('titles', (query) => query.orderBy('id', 'asc'))
|
||||||
|
.preload('descriptions', (query) => query.orderBy('id', 'asc'))
|
||||||
|
.preload('coverage')
|
||||||
|
.preload('licenses')
|
||||||
|
.preload('authors', (query) => query.orderBy('pivot_sort_order', 'asc'))
|
||||||
|
.preload('contributors', (query) => query.orderBy('pivot_sort_order', 'asc'))
|
||||||
|
// .preload('subjects')
|
||||||
|
.preload('subjects', (builder) => {
|
||||||
|
builder.orderBy('id', 'asc').withCount('datasets');
|
||||||
|
})
|
||||||
|
.preload('references')
|
||||||
|
.preload('project')
|
||||||
|
.preload('files', (query) => {
|
||||||
|
query.orderBy('sort_order', 'asc'); // Sort by sort_order column
|
||||||
|
});
|
||||||
|
|
||||||
|
const dataset = await datasetQuery.firstOrFail();
|
||||||
|
|
||||||
|
const validStates = ['approved', 'rejected_to_reviewer'];
|
||||||
|
if (!validStates.includes(dataset.server_state)) {
|
||||||
|
// session.flash('errors', 'Invalid server state!');
|
||||||
|
return response
|
||||||
|
.flash(
|
||||||
|
'warning',
|
||||||
|
`Invalid server state. Dataset with id ${id} cannot be reviewed. Datset has server state ${dataset.server_state}.`,
|
||||||
|
)
|
||||||
|
.redirect()
|
||||||
|
.toRoute('reviewer.dataset.list');
|
||||||
|
}
|
||||||
|
|
||||||
|
return inertia.render('Reviewer/Dataset/Review', {
|
||||||
|
dataset,
|
||||||
|
can: {
|
||||||
|
review: await auth.user?.can(['dataset-review']),
|
||||||
|
reject: await auth.user?.can(['dataset-review-reject']),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async review_old({ request, inertia, response, auth }: HttpContext) {
|
||||||
const id = request.param('id');
|
const id = request.param('id');
|
||||||
const dataset = await Dataset.query()
|
const dataset = await Dataset.query()
|
||||||
.where('id', id)
|
.where('id', id)
|
||||||
|
@ -158,6 +212,10 @@ export default class DatasetsController {
|
||||||
return inertia.render('Reviewer/Dataset/Review', {
|
return inertia.render('Reviewer/Dataset/Review', {
|
||||||
dataset,
|
dataset,
|
||||||
fields: fields,
|
fields: fields,
|
||||||
|
can: {
|
||||||
|
review: await auth.user?.can(['dataset-review']),
|
||||||
|
reject: await auth.user?.can(['dataset-review-reject']),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -166,7 +224,7 @@ export default class DatasetsController {
|
||||||
// const { id } = params;
|
// const { id } = params;
|
||||||
const dataset = await Dataset.findOrFail(id);
|
const dataset = await Dataset.findOrFail(id);
|
||||||
|
|
||||||
const validStates = ['approved'];
|
const validStates = ['approved', 'rejected_to_reviewer'];
|
||||||
if (!validStates.includes(dataset.server_state)) {
|
if (!validStates.includes(dataset.server_state)) {
|
||||||
// throw new Error('Invalid server state!');
|
// throw new Error('Invalid server state!');
|
||||||
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
|
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
|
||||||
|
@ -180,6 +238,10 @@ export default class DatasetsController {
|
||||||
}
|
}
|
||||||
|
|
||||||
dataset.server_state = 'reviewed';
|
dataset.server_state = 'reviewed';
|
||||||
|
// if editor has rejected to reviewer:
|
||||||
|
if (dataset.reject_editor_note != null) {
|
||||||
|
dataset.reject_editor_note = null;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// await dataset.related('editor').associate(user); // speichert schon ab
|
// await dataset.related('editor').associate(user); // speichert schon ab
|
||||||
|
@ -203,7 +265,7 @@ export default class DatasetsController {
|
||||||
})
|
})
|
||||||
.firstOrFail();
|
.firstOrFail();
|
||||||
|
|
||||||
const validStates = ['approved'];
|
const validStates = ['approved', 'rejected_to_reviewer'];
|
||||||
if (!validStates.includes(dataset.server_state)) {
|
if (!validStates.includes(dataset.server_state)) {
|
||||||
// session.flash('errors', 'Invalid server state!');
|
// session.flash('errors', 'Invalid server state!');
|
||||||
return response
|
return response
|
||||||
|
@ -250,7 +312,7 @@ export default class DatasetsController {
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
const validStates = ['approved'];
|
const validStates = ['approved', 'rejected_to_reviewer'];
|
||||||
if (!validStates.includes(dataset.server_state)) {
|
if (!validStates.includes(dataset.server_state)) {
|
||||||
// throw new Error('Invalid server state!');
|
// throw new Error('Invalid server state!');
|
||||||
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
|
// return response.flash('warning', 'Invalid server state. Dataset cannot be released to editor').redirect().back();
|
||||||
|
@ -276,7 +338,7 @@ export default class DatasetsController {
|
||||||
validateSMTP: false,
|
validateSMTP: false,
|
||||||
});
|
});
|
||||||
const validRecipientEmail: boolean = validationResult.valid;
|
const validRecipientEmail: boolean = validationResult.valid;
|
||||||
let emailStatusMessage = '';
|
// let emailStatusMessage = '';
|
||||||
|
|
||||||
if (sendMail == true) {
|
if (sendMail == true) {
|
||||||
if (dataset.editor.email && validRecipientEmail) {
|
if (dataset.editor.email && validRecipientEmail) {
|
||||||
|
@ -289,7 +351,7 @@ export default class DatasetsController {
|
||||||
<p>Best regards,<br>Your Tethys reviewer: ${authUser.login}</p>
|
<p>Best regards,<br>Your Tethys reviewer: ${authUser.login}</p>
|
||||||
`);
|
`);
|
||||||
});
|
});
|
||||||
emailStatusMessage = ` A rejection email was successfully sent to ${dataset.editor.email}.`;
|
// emailStatusMessage = ` A rejection email was successfully sent to ${dataset.editor.email}.`;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
return response
|
return response
|
||||||
|
@ -297,7 +359,7 @@ export default class DatasetsController {
|
||||||
.toRoute('reviewer.dataset.list');
|
.toRoute('reviewer.dataset.list');
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
emailStatusMessage = ` However, the email could not be sent because the editor's email address (${dataset.editor.email}) is not valid.`;
|
// emailStatusMessage = ` However, the email could not be sent because the editor's email address (${dataset.editor.email}) is not valid.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -307,4 +369,17 @@ export default class DatasetsController {
|
||||||
.toRoute('reviewer.dataset.list')
|
.toRoute('reviewer.dataset.list')
|
||||||
.flash(`You have rejected dataset ${dataset.id}! to editor ${dataset.editor.login}`, 'message');
|
.flash(`You have rejected dataset ${dataset.id}! to editor ${dataset.editor.login}`, 'message');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async download({ params, response }: HttpContext) {
|
||||||
|
const id = params.id;
|
||||||
|
// Find the file by ID
|
||||||
|
const file = await File.findOrFail(id);
|
||||||
|
// const filePath = await drive.use('local').getUrl('/'+ file.filePath)
|
||||||
|
const filePath = file.filePath;
|
||||||
|
const fileExt = file.filePath.split('.').pop() || '';
|
||||||
|
// Set the response headers and download the file
|
||||||
|
response.header('Content-Type', file.mime_type || 'application/octet-stream');
|
||||||
|
response.attachment(`${file.label}.${fileExt}`);
|
||||||
|
return response.download(filePath);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ import Description from '#models/description';
|
||||||
import Language from '#models/language';
|
import Language from '#models/language';
|
||||||
import Coverage from '#models/coverage';
|
import Coverage from '#models/coverage';
|
||||||
import Collection from '#models/collection';
|
import Collection from '#models/collection';
|
||||||
|
import CollectionRole from '#models/collection_role';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Person from '#models/person';
|
import Person from '#models/person';
|
||||||
import db from '@adonisjs/lucid/services/db';
|
import db from '@adonisjs/lucid/services/db';
|
||||||
|
@ -28,21 +29,30 @@ import {
|
||||||
} from '#contracts/enums';
|
} from '#contracts/enums';
|
||||||
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
|
import { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model';
|
||||||
import DatasetReference from '#models/dataset_reference';
|
import DatasetReference from '#models/dataset_reference';
|
||||||
import { cuid } from '@adonisjs/core/helpers';
|
|
||||||
import File from '#models/file';
|
import File from '#models/file';
|
||||||
import ClamScan from 'clamscan';
|
import ClamScan from 'clamscan';
|
||||||
// import { ValidationException } from '@adonisjs/validator';
|
import drive from '@adonisjs/drive/services/main';
|
||||||
// import Drive from '@ioc:Adonis/Core/Drive';
|
import path from 'path';
|
||||||
import drive from '#services/drive';
|
|
||||||
import { Exception } from '@adonisjs/core/exceptions';
|
import { Exception } from '@adonisjs/core/exceptions';
|
||||||
import { MultipartFile } from '@adonisjs/core/types/bodyparser';
|
import { MultipartFile } from '@adonisjs/core/types/bodyparser';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
import { pipeline } from 'node:stream/promises';
|
||||||
|
import { createWriteStream } from 'node:fs';
|
||||||
|
import type { Multipart } from '@adonisjs/bodyparser';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import { parseBytesSize, getConfigFor, getTmpPath, formatBytes } from '#app/utils/utility-functions';
|
||||||
|
|
||||||
interface Dictionary {
|
interface Dictionary {
|
||||||
[index: string]: string;
|
[index: string]: string;
|
||||||
}
|
}
|
||||||
import vine, { SimpleMessagesProvider, errors } from '@vinejs/vine';
|
import vine, { SimpleMessagesProvider, errors } from '@vinejs/vine';
|
||||||
|
|
||||||
export default class DatasetController {
|
export default class DatasetController {
|
||||||
|
/**
|
||||||
|
* Bodyparser config
|
||||||
|
*/
|
||||||
|
// config: BodyParserConfig = config.get('bodyparser');
|
||||||
|
|
||||||
public async index({ auth, request, inertia }: HttpContext) {
|
public async index({ auth, request, inertia }: HttpContext) {
|
||||||
const user = (await User.find(auth.user?.id)) as User;
|
const user = (await User.find(auth.user?.id)) as User;
|
||||||
const page = request.input('page', 1);
|
const page = request.input('page', 1);
|
||||||
|
@ -66,8 +76,16 @@ export default class DatasetController {
|
||||||
}
|
}
|
||||||
datasets.orderBy(attribute, sortOrder);
|
datasets.orderBy(attribute, sortOrder);
|
||||||
} else {
|
} else {
|
||||||
// users.orderBy('created_at', 'desc');
|
// datasets.orderBy('id', 'asc');
|
||||||
datasets.orderBy('id', 'asc');
|
// Custom ordering to prioritize rejected_editor state
|
||||||
|
datasets.orderByRaw(`
|
||||||
|
CASE
|
||||||
|
WHEN server_state = 'rejected_editor' THEN 0
|
||||||
|
WHEN server_state = 'rejected_reviewer' THEN 1
|
||||||
|
ELSE 2
|
||||||
|
END ASC,
|
||||||
|
id ASC
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// const results = await Database
|
// const results = await Database
|
||||||
|
@ -188,7 +206,8 @@ export default class DatasetController {
|
||||||
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.minLength(1),
|
// .minLength(2)
|
||||||
|
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
|
||||||
descriptions: vine
|
descriptions: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
|
@ -202,7 +221,8 @@ export default class DatasetController {
|
||||||
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.minLength(1),
|
// .minLength(1),
|
||||||
|
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
|
||||||
authors: vine
|
authors: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
|
@ -277,7 +297,8 @@ export default class DatasetController {
|
||||||
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.minLength(1),
|
// .minLength(2)
|
||||||
|
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
|
||||||
descriptions: vine
|
descriptions: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
|
@ -291,7 +312,8 @@ export default class DatasetController {
|
||||||
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.minLength(1),
|
// .minLength(1),
|
||||||
|
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
|
||||||
authors: vine
|
authors: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
|
@ -363,7 +385,8 @@ export default class DatasetController {
|
||||||
references: vine
|
references: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
value: vine.string().trim().minLength(3).maxLength(255),
|
// value: vine.string().trim().minLength(3).maxLength(255),
|
||||||
|
value: vine.string().trim().minLength(3).maxLength(255).validateReference({ typeField: 'type' }),
|
||||||
type: vine.enum(Object.values(ReferenceIdentifierTypes)),
|
type: vine.enum(Object.values(ReferenceIdentifierTypes)),
|
||||||
relation: vine.enum(Object.values(RelationTypes)),
|
relation: vine.enum(Object.values(RelationTypes)),
|
||||||
label: vine.string().trim().minLength(2).maxLength(255),
|
label: vine.string().trim().minLength(2).maxLength(255),
|
||||||
|
@ -398,21 +421,99 @@ export default class DatasetController {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async store({ auth, request, response, session }: HttpContext) {
|
public async store({ auth, request, response, session }: HttpContext) {
|
||||||
// node ace make:validator CreateDataset
|
// At the top of the store() method, declare an array to hold temporary file paths
|
||||||
|
const uploadedTmpFiles: string[] = [];
|
||||||
|
// Aggregated limit example (adjust as needed)
|
||||||
|
const multipartConfig = getConfigFor('multipart');
|
||||||
|
const aggregatedLimit = multipartConfig.limit ? parseBytesSize(multipartConfig.limit) : 100 * 1024 * 1024;
|
||||||
|
// const aggregatedLimit = 200 * 1024 * 1024;
|
||||||
|
let totalUploadedSize = 0;
|
||||||
|
|
||||||
|
// // Helper function to format bytes as human-readable text
|
||||||
|
// function formatBytes(bytes: number): string {
|
||||||
|
// if (bytes === 0) return '0 Bytes';
|
||||||
|
// const k = 1024;
|
||||||
|
// const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
// const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
// return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
// }
|
||||||
|
// const enabledExtensions = await this.getEnabledExtensions();
|
||||||
|
const multipart: Multipart = request.multipart;
|
||||||
|
|
||||||
|
multipart.onFile('files', { deferValidations: true }, async (part) => {
|
||||||
|
// Attach an individual file size accumulator if needed
|
||||||
|
let fileUploadedSize = 0;
|
||||||
|
|
||||||
|
// Simply accumulate the size in on('data') without performing the expensive check per chunk
|
||||||
|
part.on('data', (chunk) => {
|
||||||
|
// reporter(chunk);
|
||||||
|
// Increase counters using the chunk length
|
||||||
|
fileUploadedSize += chunk.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
// After the file is completely read, update the global counter and check aggregated limit
|
||||||
|
part.on('end', () => {
|
||||||
|
totalUploadedSize += fileUploadedSize;
|
||||||
|
part.file.size = fileUploadedSize;
|
||||||
|
// Record the temporary file path
|
||||||
|
if (part.file.tmpPath) {
|
||||||
|
uploadedTmpFiles.push(part.file.tmpPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalUploadedSize > aggregatedLimit) {
|
||||||
|
// Clean up all temporary files if aggregate limit is exceeded
|
||||||
|
uploadedTmpFiles.forEach((tmpPath) => {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(tmpPath);
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.error('Error cleaning up temporary file:', cleanupError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const error = new errors.E_VALIDATION_ERROR({
|
||||||
|
'upload error': `Aggregated upload limit of ${formatBytes(aggregatedLimit)} exceeded. The total size of files being uploaded would exceed the limit.`,
|
||||||
|
});
|
||||||
|
request.multipart.abort(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
part.on('error', (error) => {
|
||||||
|
// fileUploadError = error;
|
||||||
|
request.multipart.abort(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
// await pipeline(part, createWriteStream(filePath));
|
||||||
|
// return { filePath };
|
||||||
|
// Process file with error handling
|
||||||
|
try {
|
||||||
|
// Extract extension from the client file name, e.g. "Tethys 5 - Ampflwang_dataset.zip"
|
||||||
|
const ext = path.extname(part.file.clientName).replace('.', '');
|
||||||
|
// Attach the extracted extension to the file object for later use
|
||||||
|
part.file.extname = ext;
|
||||||
|
|
||||||
|
// part.file.sortOrder = part.file.sortOrder;
|
||||||
|
|
||||||
|
const tmpPath = getTmpPath(multipartConfig);
|
||||||
|
(part.file as any).tmpPath = tmpPath;
|
||||||
|
|
||||||
|
const writeStream = createWriteStream(tmpPath);
|
||||||
|
await pipeline(part, writeStream);
|
||||||
|
} catch (error) {
|
||||||
|
request.multipart.abort(new errors.E_VALIDATION_ERROR({ 'upload error': error.message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Step 2 - Validate request body against the schema
|
await multipart.process();
|
||||||
// await request.validate({ schema: newDatasetSchema, messages: this.messages });
|
// // Instead of letting an error abort the controller, check if any error occurred
|
||||||
// await request.validate(CreateDatasetValidator);
|
|
||||||
await request.validateUsing(createDatasetValidator);
|
|
||||||
// console.log({ payload });
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Step 3 - Handle errors
|
// This is where you'd expect to catch any errors.
|
||||||
// return response.badRequest(error.messages);
|
session.flash('errors', error.messages);
|
||||||
throw error;
|
return response.redirect().back();
|
||||||
}
|
}
|
||||||
|
|
||||||
let trx: TransactionClientContract | null = null;
|
let trx: TransactionClientContract | null = null;
|
||||||
try {
|
try {
|
||||||
|
await request.validateUsing(createDatasetValidator);
|
||||||
trx = await db.transaction();
|
trx = await db.transaction();
|
||||||
const user = (await User.find(auth.user?.id)) as User;
|
const user = (await User.find(auth.user?.id)) as User;
|
||||||
|
|
||||||
|
@ -421,6 +522,14 @@ export default class DatasetController {
|
||||||
await trx.commit();
|
await trx.commit();
|
||||||
console.log('Dataset and related models created successfully');
|
console.log('Dataset and related models created successfully');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Clean up temporary files if validation or later steps fail
|
||||||
|
uploadedTmpFiles.forEach((tmpPath) => {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(tmpPath);
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.error('Error cleaning up temporary file:', cleanupError);
|
||||||
|
}
|
||||||
|
});
|
||||||
if (trx !== null) {
|
if (trx !== null) {
|
||||||
await trx.rollback();
|
await trx.rollback();
|
||||||
}
|
}
|
||||||
|
@ -433,14 +542,19 @@ export default class DatasetController {
|
||||||
return response.redirect().toRoute('dataset.list');
|
return response.redirect().toRoute('dataset.list');
|
||||||
// return response.redirect().back();
|
// return response.redirect().back();
|
||||||
}
|
}
|
||||||
|
private async createDatasetAndAssociations(
|
||||||
private async createDatasetAndAssociations(user: User, request: HttpContext['request'], trx: TransactionClientContract) {
|
user: User,
|
||||||
|
request: HttpContext['request'],
|
||||||
|
trx: TransactionClientContract,
|
||||||
|
// uploadedFiles: Array<MultipartFile>,
|
||||||
|
) {
|
||||||
// Create a new instance of the Dataset model:
|
// Create a new instance of the Dataset model:
|
||||||
const dataset = new Dataset();
|
const dataset = new Dataset();
|
||||||
dataset.type = request.input('type');
|
dataset.type = request.input('type');
|
||||||
dataset.creating_corporation = request.input('creating_corporation');
|
dataset.creating_corporation = request.input('creating_corporation');
|
||||||
dataset.language = request.input('language');
|
dataset.language = request.input('language');
|
||||||
dataset.embargo_date = request.input('embargo_date');
|
dataset.embargo_date = request.input('embargo_date');
|
||||||
|
dataset.project_id = request.input('project_id');
|
||||||
//await dataset.related('user').associate(user); // speichert schon ab
|
//await dataset.related('user').associate(user); // speichert schon ab
|
||||||
// Dataset.$getRelation('user').boot();
|
// Dataset.$getRelation('user').boot();
|
||||||
// Dataset.$getRelation('user').setRelated(dataset, user);
|
// Dataset.$getRelation('user').setRelated(dataset, user);
|
||||||
|
@ -498,7 +612,7 @@ export default class DatasetController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// save collection
|
// save collection
|
||||||
const collection: Collection | null = await Collection.query().where('id', 21).first();
|
const collection: Collection | null = await Collection.query().where('id', 594).first();
|
||||||
collection && (await dataset.useTransaction(trx).related('collections').attach([collection.id]));
|
collection && (await dataset.useTransaction(trx).related('collections').attach([collection.id]));
|
||||||
|
|
||||||
// save coverage
|
// save coverage
|
||||||
|
@ -531,18 +645,25 @@ export default class DatasetController {
|
||||||
const fileName = this.generateFilename(file.extname as string);
|
const fileName = this.generateFilename(file.extname as string);
|
||||||
const mimeType = file.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type
|
const mimeType = file.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type
|
||||||
const datasetFolder = `files/${dataset.id}`;
|
const datasetFolder = `files/${dataset.id}`;
|
||||||
|
const datasetFullPath = path.join(`${datasetFolder}`, fileName);
|
||||||
// const size = file.size;
|
// const size = file.size;
|
||||||
await file.move(drive.makePath(datasetFolder), {
|
// await file.move(drive.makePath(datasetFolder), {
|
||||||
|
// name: fileName,
|
||||||
|
// overwrite: true, // overwrite in case of conflict
|
||||||
|
// });
|
||||||
|
await file.moveToDisk(datasetFullPath, 'local', {
|
||||||
name: fileName,
|
name: fileName,
|
||||||
overwrite: true, // overwrite in case of conflict
|
overwrite: true, // overwrite in case of conflict
|
||||||
|
disk: 'local',
|
||||||
});
|
});
|
||||||
|
|
||||||
// save file metadata into db
|
// save file metadata into db
|
||||||
const newFile = new File();
|
const newFile = new File();
|
||||||
newFile.pathName = `${datasetFolder}/${fileName}`;
|
newFile.pathName = `${datasetFolder}/${fileName}`;
|
||||||
newFile.fileSize = file.size;
|
newFile.fileSize = file.size;
|
||||||
newFile.mimeType = mimeType;
|
newFile.mimeType = mimeType;
|
||||||
newFile.label = file.clientName;
|
newFile.label = file.clientName;
|
||||||
newFile.sortOrder = index;
|
newFile.sortOrder = index + 1;
|
||||||
newFile.visibleInFrontdoor = true;
|
newFile.visibleInFrontdoor = true;
|
||||||
newFile.visibleInOai = true;
|
newFile.visibleInOai = true;
|
||||||
// let path = coverImage.filePath;
|
// let path = coverImage.filePath;
|
||||||
|
@ -693,6 +814,8 @@ export default class DatasetController {
|
||||||
'files.array.minLength': 'At least {{ min }} file upload is required.',
|
'files.array.minLength': 'At least {{ min }} file upload is required.',
|
||||||
'files.*.size': 'file size is to big',
|
'files.*.size': 'file size is to big',
|
||||||
'files.*.extnames': 'file extension is not supported',
|
'files.*.extnames': 'file extension is not supported',
|
||||||
|
|
||||||
|
'embargo_date.date.afterOrEqual': `Embargo date must be on or after ${dayjs().add(10, 'day').format('DD.MM.YYYY')}`,
|
||||||
};
|
};
|
||||||
|
|
||||||
// public async release({ params, view }) {
|
// public async release({ params, view }) {
|
||||||
|
@ -803,7 +926,7 @@ export default class DatasetController {
|
||||||
// throw new GeneralException(trans('exceptions.publish.release.update_error'));
|
// throw new GeneralException(trans('exceptions.publish.release.update_error'));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async edit({ request, inertia, response }: HttpContext) {
|
public async edit({ request, inertia, response, auth }: HttpContext) {
|
||||||
const id = request.param('id');
|
const id = request.param('id');
|
||||||
const datasetQuery = Dataset.query().where('id', id);
|
const datasetQuery = Dataset.query().where('id', id);
|
||||||
datasetQuery
|
datasetQuery
|
||||||
|
@ -811,8 +934,8 @@ export default class DatasetController {
|
||||||
.preload('descriptions', (query) => query.orderBy('id', 'asc'))
|
.preload('descriptions', (query) => query.orderBy('id', 'asc'))
|
||||||
.preload('coverage')
|
.preload('coverage')
|
||||||
.preload('licenses')
|
.preload('licenses')
|
||||||
.preload('authors')
|
.preload('authors', (query) => query.orderBy('pivot_sort_order', 'asc'))
|
||||||
.preload('contributors')
|
.preload('contributors', (query) => query.orderBy('pivot_sort_order', 'asc'))
|
||||||
// .preload('subjects')
|
// .preload('subjects')
|
||||||
.preload('subjects', (builder) => {
|
.preload('subjects', (builder) => {
|
||||||
builder.orderBy('id', 'asc').withCount('datasets');
|
builder.orderBy('id', 'asc').withCount('datasets');
|
||||||
|
@ -828,10 +951,9 @@ export default class DatasetController {
|
||||||
// session.flash('errors', 'Invalid server state!');
|
// session.flash('errors', 'Invalid server state!');
|
||||||
return response
|
return response
|
||||||
.flash(
|
.flash(
|
||||||
'warning',
|
|
||||||
`Invalid server state. Dataset with id ${id} cannot be edited. Datset has server state ${dataset.server_state}.`,
|
`Invalid server state. Dataset with id ${id} cannot be edited. Datset has server state ${dataset.server_state}.`,
|
||||||
|
'warning',
|
||||||
)
|
)
|
||||||
.redirect()
|
|
||||||
.toRoute('dataset.list');
|
.toRoute('dataset.list');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -865,15 +987,15 @@ export default class DatasetController {
|
||||||
// const datasetHasLicenses = await dataset.related('licenses').query().pluck('id');
|
// const datasetHasLicenses = await dataset.related('licenses').query().pluck('id');
|
||||||
// const checkeds = dataset.licenses.first().id;
|
// const checkeds = dataset.licenses.first().id;
|
||||||
|
|
||||||
const doctypes = {
|
// const doctypes = {
|
||||||
analysisdata: { label: 'Analysis', value: 'analysisdata' },
|
// analysisdata: { label: 'Analysis', value: 'analysisdata' },
|
||||||
measurementdata: { label: 'Measurements', value: 'measurementdata' },
|
// measurementdata: { label: 'Measurements', value: 'measurementdata' },
|
||||||
monitoring: 'Monitoring',
|
// monitoring: 'Monitoring',
|
||||||
remotesensing: 'Remote Sensing',
|
// remotesensing: 'Remote Sensing',
|
||||||
gis: 'GIS',
|
// gis: 'GIS',
|
||||||
models: 'Models',
|
// models: 'Models',
|
||||||
mixedtype: 'Mixed Type',
|
// mixedtype: 'Mixed Type',
|
||||||
};
|
// };
|
||||||
|
|
||||||
return inertia.render('Submitter/Dataset/Edit', {
|
return inertia.render('Submitter/Dataset/Edit', {
|
||||||
dataset,
|
dataset,
|
||||||
|
@ -892,25 +1014,95 @@ export default class DatasetController {
|
||||||
subjectTypes: SubjectTypes,
|
subjectTypes: SubjectTypes,
|
||||||
referenceIdentifierTypes: Object.entries(ReferenceIdentifierTypes).map(([key, value]) => ({ value: key, label: value })),
|
referenceIdentifierTypes: Object.entries(ReferenceIdentifierTypes).map(([key, value]) => ({ value: key, label: value })),
|
||||||
relationTypes: Object.entries(RelationTypes).map(([key, value]) => ({ value: key, label: value })),
|
relationTypes: Object.entries(RelationTypes).map(([key, value]) => ({ value: key, label: value })),
|
||||||
doctypes,
|
doctypes: DatasetTypes,
|
||||||
|
can: {
|
||||||
|
edit: await auth.user?.can(['dataset-edit']),
|
||||||
|
delete: await auth.user?.can(['dataset-delete']),
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async update({ request, response, session }: HttpContext) {
|
public async update({ request, response, session }: HttpContext) {
|
||||||
try {
|
// Get the dataset id from the route parameter
|
||||||
// await request.validate(UpdateDatasetValidator);
|
const datasetId = request.param('id');
|
||||||
await request.validateUsing(updateDatasetValidator);
|
// Retrieve the dataset and load its existing files
|
||||||
} catch (error) {
|
const dataset = await Dataset.findOrFail(datasetId);
|
||||||
// - Handle errors
|
await dataset.load('files');
|
||||||
// return response.badRequest(error.messages);
|
// Accumulate the size of the already related files
|
||||||
throw error;
|
// const preExistingFileSize = dataset.files.reduce((acc, file) => acc + file.fileSize, 0);
|
||||||
// return response.badRequest(error.messages);
|
let preExistingFileSize = 0;
|
||||||
|
for (const file of dataset.files) {
|
||||||
|
preExistingFileSize += Number(file.fileSize);
|
||||||
}
|
}
|
||||||
// await request.validate(UpdateDatasetValidator);
|
|
||||||
const id = request.param('id');
|
|
||||||
|
|
||||||
|
const uploadedTmpFiles: string[] = [];
|
||||||
|
// Only process multipart if the request has a multipart content type
|
||||||
|
const contentType = request.request.headers['content-type'] || '';
|
||||||
|
if (contentType.includes('multipart/form-data')) {
|
||||||
|
const multipart: Multipart = request.multipart;
|
||||||
|
// Aggregated limit example (adjust as needed)
|
||||||
|
const multipartConfig = getConfigFor('multipart');
|
||||||
|
const aggregatedLimit = multipartConfig.limit ? parseBytesSize(multipartConfig.limit) : 100 * 1024 * 1024;
|
||||||
|
// Initialize totalUploadedSize with the size of existing files
|
||||||
|
let totalUploadedSize = preExistingFileSize;
|
||||||
|
|
||||||
|
multipart.onFile('files', { deferValidations: true }, async (part) => {
|
||||||
|
let fileUploadedSize = 0;
|
||||||
|
|
||||||
|
part.on('data', (chunk) => {
|
||||||
|
fileUploadedSize += chunk.length;
|
||||||
|
});
|
||||||
|
|
||||||
|
part.on('end', () => {
|
||||||
|
totalUploadedSize += fileUploadedSize;
|
||||||
|
part.file.size = fileUploadedSize;
|
||||||
|
if (part.file.tmpPath) {
|
||||||
|
uploadedTmpFiles.push(part.file.tmpPath);
|
||||||
|
}
|
||||||
|
if (totalUploadedSize > aggregatedLimit) {
|
||||||
|
uploadedTmpFiles.forEach((tmpPath) => {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(tmpPath);
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.error('Error cleaning up temporary file:', cleanupError);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const error = new errors.E_VALIDATION_ERROR({
|
||||||
|
'upload error': `Aggregated upload limit of ${formatBytes(aggregatedLimit)} exceeded. The total size of files being uploaded would exceed the limit.`,
|
||||||
|
});
|
||||||
|
request.multipart.abort(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
part.on('error', (error) => {
|
||||||
|
request.multipart.abort(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const fileNameWithoutParams = part.file.clientName.split('?')[0];
|
||||||
|
const ext = path.extname(fileNameWithoutParams).replace('.', '');
|
||||||
|
part.file.extname = ext;
|
||||||
|
const tmpPath = getTmpPath(multipartConfig);
|
||||||
|
(part.file as any).tmpPath = tmpPath;
|
||||||
|
const writeStream = createWriteStream(tmpPath);
|
||||||
|
await pipeline(part, writeStream);
|
||||||
|
} catch (error) {
|
||||||
|
request.multipart.abort(new errors.E_VALIDATION_ERROR({ 'upload error': error.message }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
await multipart.process();
|
||||||
|
} catch (error) {
|
||||||
|
session.flash('errors', error.messages);
|
||||||
|
return response.redirect().back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = request.param('id');
|
||||||
let trx: TransactionClientContract | null = null;
|
let trx: TransactionClientContract | null = null;
|
||||||
try {
|
try {
|
||||||
|
await request.validateUsing(updateDatasetValidator);
|
||||||
trx = await db.transaction();
|
trx = await db.transaction();
|
||||||
// const user = (await User.find(auth.user?.id)) as User;
|
// const user = (await User.find(auth.user?.id)) as User;
|
||||||
// await this.createDatasetAndAssociations(user, request, trx);
|
// await this.createDatasetAndAssociations(user, request, trx);
|
||||||
|
@ -971,22 +1163,97 @@ export default class DatasetController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// await dataset.useTransaction(trx).related('subjects').sync([]);
|
// Process all subjects/keywords from the request
|
||||||
const keywords = request.input('subjects');
|
const subjects = request.input('subjects');
|
||||||
for (const keywordData of keywords) {
|
for (const subjectData of subjects) {
|
||||||
if (keywordData.id) {
|
// Case 1: Subject already exists in the database (has an ID)
|
||||||
const subject = await Subject.findOrFail(keywordData.id);
|
if (subjectData.id) {
|
||||||
// await dataset.useTransaction(trx).related('subjects').attach([keywordData.id]);
|
// Retrieve the existing subject
|
||||||
subject.value = keywordData.value;
|
const existingSubject = await Subject.findOrFail(subjectData.id);
|
||||||
subject.type = keywordData.type;
|
|
||||||
subject.external_key = keywordData.external_key;
|
// Update subject properties from the request data
|
||||||
if (subject.$isDirty) {
|
existingSubject.value = subjectData.value;
|
||||||
await subject.save();
|
existingSubject.type = subjectData.type;
|
||||||
|
existingSubject.external_key = subjectData.external_key;
|
||||||
|
|
||||||
|
// Only save if there are actual changes
|
||||||
|
if (existingSubject.$isDirty) {
|
||||||
|
await existingSubject.save();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: The relationship between dataset and subject is already established,
|
||||||
|
// so we don't need to attach it again
|
||||||
|
}
|
||||||
|
// Case 2: New subject being added (no ID)
|
||||||
|
else {
|
||||||
|
// Check if a subject with the same value and type already exists in the database
|
||||||
|
const subject = await Subject.firstOrNew({ value: subjectData.value, type: subjectData.type }, subjectData);
|
||||||
|
|
||||||
|
if (subject.$isNew === true) {
|
||||||
|
// If it's a completely new subject, create and associate it with the dataset
|
||||||
|
await dataset.useTransaction(trx).related('subjects').save(subject);
|
||||||
|
} else {
|
||||||
|
// If the subject already exists, just create the relationship
|
||||||
|
await dataset.useTransaction(trx).related('subjects').attach([subject.id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const subjectsToDelete = request.input('subjectsToDelete', []);
|
||||||
|
for (const subjectData of subjectsToDelete) {
|
||||||
|
if (subjectData.id) {
|
||||||
|
// const subject = await Subject.findOrFail(subjectData.id);
|
||||||
|
const subject = await Subject.query()
|
||||||
|
.where('id', subjectData.id)
|
||||||
|
.preload('datasets', (builder) => {
|
||||||
|
builder.orderBy('id', 'asc');
|
||||||
|
})
|
||||||
|
.withCount('datasets')
|
||||||
|
.firstOrFail();
|
||||||
|
|
||||||
|
// Check if the subject is used by multiple datasets
|
||||||
|
if (subject.$extras.datasets_count > 1) {
|
||||||
|
// If used by multiple datasets, just detach it from the current dataset
|
||||||
|
await dataset.useTransaction(trx).related('subjects').detach([subject.id]);
|
||||||
|
} else {
|
||||||
|
// If only used by this dataset, delete the subject completely
|
||||||
|
|
||||||
|
await dataset.useTransaction(trx).related('subjects').detach([subject.id]);
|
||||||
|
await subject.useTransaction(trx).delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process references
|
||||||
|
const references = request.input('references', []);
|
||||||
|
// First, get existing references to determine which ones to update vs. create
|
||||||
|
const existingReferences = await dataset.related('references').query();
|
||||||
|
const existingReferencesMap: Map<number, DatasetReference> = new Map(existingReferences.map((ref) => [ref.id, ref]));
|
||||||
|
|
||||||
|
for (const referenceData of references) {
|
||||||
|
if (existingReferencesMap.has(referenceData.id) && referenceData.id) {
|
||||||
|
// Update existing reference
|
||||||
|
const reference = existingReferencesMap.get(referenceData.id);
|
||||||
|
if (reference) {
|
||||||
|
reference.merge(referenceData);
|
||||||
|
if (reference.$isDirty) {
|
||||||
|
await reference.useTransaction(trx).save();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const keyword = new Subject();
|
// Create new reference
|
||||||
keyword.fill(keywordData);
|
const dataReference = new DatasetReference();
|
||||||
await dataset.useTransaction(trx).related('subjects').save(keyword, false);
|
dataReference.fill(referenceData);
|
||||||
|
await dataset.useTransaction(trx).related('references').save(dataReference);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle references to delete if provided
|
||||||
|
const referencesToDelete = request.input('referencesToDelete', []);
|
||||||
|
for (const referenceData of referencesToDelete) {
|
||||||
|
if (referenceData.id) {
|
||||||
|
const reference = await DatasetReference.findOrFail(referenceData.id);
|
||||||
|
await reference.useTransaction(trx).delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1018,9 +1285,9 @@ export default class DatasetController {
|
||||||
// handle new uploaded files:
|
// handle new uploaded files:
|
||||||
const uploadedFiles: MultipartFile[] = request.files('files');
|
const uploadedFiles: MultipartFile[] = request.files('files');
|
||||||
if (Array.isArray(uploadedFiles) && uploadedFiles.length > 0) {
|
if (Array.isArray(uploadedFiles) && uploadedFiles.length > 0) {
|
||||||
for (const [index, fileData] of uploadedFiles.entries()) {
|
for (const [index, file] of uploadedFiles.entries()) {
|
||||||
try {
|
try {
|
||||||
await this.scanFileForViruses(fileData.tmpPath); //, 'gitea.lan', 3310);
|
await this.scanFileForViruses(file.tmpPath); //, 'gitea.lan', 3310);
|
||||||
// await this.scanFileForViruses("/tmp/testfile.txt");
|
// await this.scanFileForViruses("/tmp/testfile.txt");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// If the file is infected or there's an error scanning the file, throw a validation exception
|
// If the file is infected or there's an error scanning the file, throw a validation exception
|
||||||
|
@ -1028,23 +1295,29 @@ export default class DatasetController {
|
||||||
}
|
}
|
||||||
|
|
||||||
// move to disk:
|
// move to disk:
|
||||||
const fileName = `file-${cuid()}.${fileData.extname}`; //'file-ls0jyb8xbzqtrclufu2z2e0c.pdf'
|
const fileName = this.generateFilename(file.extname as string);
|
||||||
const datasetFolder = `files/${dataset.id}`; // 'files/307'
|
const datasetFolder = `files/${dataset.id}`; // 'files/307'
|
||||||
// await fileData.moveToDisk(datasetFolder, { name: fileName, overwrite: true }, 'local');
|
const datasetFullPath = path.join(`${datasetFolder}`, fileName);
|
||||||
await fileData.move(drive.makePath(datasetFolder), {
|
// await file.moveToDisk(datasetFolder, { name: fileName, overwrite: true }, 'local');
|
||||||
|
// await file.move(drive.makePath(datasetFolder), {
|
||||||
|
// name: fileName,
|
||||||
|
// overwrite: true, // overwrite in case of conflict
|
||||||
|
// });
|
||||||
|
await file.moveToDisk(datasetFullPath, 'local', {
|
||||||
name: fileName,
|
name: fileName,
|
||||||
overwrite: true, // overwrite in case of conflict
|
overwrite: true, // overwrite in case of conflict
|
||||||
|
disk: 'local',
|
||||||
});
|
});
|
||||||
|
|
||||||
//save to db:
|
//save to db:
|
||||||
const { clientFileName, sortOrder } = this.extractVariableNameAndSortOrder(fileData.clientName);
|
const { clientFileName, sortOrder } = this.extractVariableNameAndSortOrder(file.clientName);
|
||||||
const mimeType = fileData.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type
|
const mimeType = file.headers['content-type'] || 'application/octet-stream'; // Fallback to a default MIME type
|
||||||
const newFile = await dataset
|
const newFile = await dataset
|
||||||
.useTransaction(trx)
|
.useTransaction(trx)
|
||||||
.related('files')
|
.related('files')
|
||||||
.create({
|
.create({
|
||||||
pathName: `${datasetFolder}/${fileName}`,
|
pathName: `${datasetFolder}/${fileName}`,
|
||||||
fileSize: fileData.size,
|
fileSize: file.size,
|
||||||
mimeType,
|
mimeType,
|
||||||
label: clientFileName,
|
label: clientFileName,
|
||||||
sortOrder: sortOrder || index,
|
sortOrder: sortOrder || index,
|
||||||
|
@ -1084,16 +1357,24 @@ export default class DatasetController {
|
||||||
await dataset.useTransaction(trx).save();
|
await dataset.useTransaction(trx).save();
|
||||||
|
|
||||||
await trx.commit();
|
await trx.commit();
|
||||||
console.log('Dataset and related models created successfully');
|
console.log('Dataset has been updated successfully');
|
||||||
|
|
||||||
session.flash('message', 'Dataset has been updated successfully');
|
session.flash('message', 'Dataset has been updated successfully');
|
||||||
// return response.redirect().toRoute('user.index');
|
// return response.redirect().toRoute('user.index');
|
||||||
return response.redirect().toRoute('dataset.edit', [dataset.id]);
|
return response.redirect().toRoute('dataset.edit', [dataset.id]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
// Clean up temporary files if validation or later steps fail
|
||||||
|
uploadedTmpFiles.forEach((tmpPath) => {
|
||||||
|
try {
|
||||||
|
fs.unlinkSync(tmpPath);
|
||||||
|
} catch (cleanupError) {
|
||||||
|
console.error('Error cleaning up temporary file:', cleanupError);
|
||||||
|
}
|
||||||
|
});
|
||||||
if (trx !== null) {
|
if (trx !== null) {
|
||||||
await trx.rollback();
|
await trx.rollback();
|
||||||
}
|
}
|
||||||
console.error('Failed to create dataset and related models:', error);
|
console.error('Failed to update dataset and related models:', error);
|
||||||
// throw new ValidationException(true, { 'upload error': `failed to create dataset and related models. ${error}` });
|
// throw new ValidationException(true, { 'upload error': `failed to create dataset and related models. ${error}` });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
@ -1160,31 +1441,32 @@ export default class DatasetController {
|
||||||
if (validStates.includes(dataset.server_state)) {
|
if (validStates.includes(dataset.server_state)) {
|
||||||
if (dataset.files && dataset.files.length > 0) {
|
if (dataset.files && dataset.files.length > 0) {
|
||||||
for (const file of dataset.files) {
|
for (const file of dataset.files) {
|
||||||
// overwritten delete method also delets file on filespace
|
// overwritten delete method also delets file on filespace and db object
|
||||||
await file.delete();
|
await file.delete();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const datasetFolder = `files/${params.id}`;
|
const datasetFolder = `files/${params.id}`;
|
||||||
const folderExists = await drive.exists(datasetFolder);
|
// const folderExists = await drive.use('local').exists(datasetFolder);
|
||||||
if (folderExists) {
|
// if (folderExists) {
|
||||||
const dirListing = drive.list(datasetFolder);
|
// const dirListing = drive.list(datasetFolder);
|
||||||
const folderContents = await dirListing.toArray();
|
// const folderContents = await dirListing.toArray();
|
||||||
if (folderContents.length === 0) {
|
// if (folderContents.length === 0) {
|
||||||
await drive.delete(datasetFolder);
|
// await drive.delete(datasetFolder);
|
||||||
}
|
// }
|
||||||
// delete dataset wirh relation in db
|
await drive.use('local').deleteAll(datasetFolder);
|
||||||
await dataset.delete();
|
// delete dataset wirh relation in db
|
||||||
session.flash({ message: 'You have deleted 1 dataset!' });
|
await dataset.delete();
|
||||||
return response.redirect().toRoute('dataset.list');
|
session.flash({ message: 'You have deleted 1 dataset!' });
|
||||||
} else {
|
return response.redirect().toRoute('dataset.list');
|
||||||
// session.flash({
|
// } else {
|
||||||
// warning: `You cannot delete this dataset! Invalid server_state: "${dataset.server_state}"!`,
|
// // session.flash({
|
||||||
// });
|
// // warning: `You cannot delete this dataset! Invalid server_state: "${dataset.server_state}"!`,
|
||||||
return response
|
// // });
|
||||||
.flash({ warning: `You cannot delete this dataset! Dataset folder "${datasetFolder}" doesn't exist!` })
|
// return response
|
||||||
.redirect()
|
// .flash({ warning: `You cannot delete this dataset! Dataset folder "${datasetFolder}" doesn't exist!` })
|
||||||
.back();
|
// .redirect()
|
||||||
}
|
// .back();
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof errors.E_VALIDATION_ERROR) {
|
if (error instanceof errors.E_VALIDATION_ERROR) {
|
||||||
|
@ -1192,11 +1474,89 @@ export default class DatasetController {
|
||||||
throw error;
|
throw error;
|
||||||
} else if (error instanceof Exception) {
|
} else if (error instanceof Exception) {
|
||||||
// General exception handling
|
// General exception handling
|
||||||
return response.flash('errors', { error: error.message }).redirect().back();
|
session.flash({ error: error.message });
|
||||||
|
return response.redirect().back();
|
||||||
} else {
|
} else {
|
||||||
session.flash({ error: 'An error occurred while deleting the dataset.' });
|
session.flash({ error: 'An error occurred while deleting the dataset.' });
|
||||||
return response.redirect().back();
|
return response.redirect().back();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async categorize({ inertia, request, response }: HttpContext) {
|
||||||
|
const id = request.param('id');
|
||||||
|
// Preload dataset and its "collections" relation
|
||||||
|
const dataset = await Dataset.query().where('id', id).preload('collections').firstOrFail();
|
||||||
|
const validStates = ['inprogress', 'rejected_editor'];
|
||||||
|
if (!validStates.includes(dataset.server_state)) {
|
||||||
|
// session.flash('errors', 'Invalid server state!');
|
||||||
|
return response
|
||||||
|
.flash(
|
||||||
|
'warning',
|
||||||
|
`Invalid server state. Dataset with id ${id} cannot be edited. Datset has server state ${dataset.server_state}.`,
|
||||||
|
)
|
||||||
|
.redirect()
|
||||||
|
.toRoute('dataset.list');
|
||||||
|
}
|
||||||
|
|
||||||
|
const collectionRoles = await CollectionRole.query()
|
||||||
|
.preload('collections', (coll: Collection) => {
|
||||||
|
// preloa only top level collection with noparent_id
|
||||||
|
coll.whereNull('parent_id').orderBy('number', 'asc');
|
||||||
|
})
|
||||||
|
.exec();
|
||||||
|
|
||||||
|
return inertia.render('Submitter/Dataset/Category', {
|
||||||
|
collectionRoles: collectionRoles,
|
||||||
|
dataset: dataset,
|
||||||
|
relatedCollections: dataset.collections,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async categorizeUpdate({ request, response, session }: HttpContext) {
|
||||||
|
// Get the dataset id from the route parameter
|
||||||
|
const id = request.param('id');
|
||||||
|
const dataset = await Dataset.query().preload('files').where('id', id).firstOrFail();
|
||||||
|
|
||||||
|
const validStates = ['inprogress', 'rejected_editor'];
|
||||||
|
if (!validStates.includes(dataset.server_state)) {
|
||||||
|
return response
|
||||||
|
.flash(
|
||||||
|
'warning',
|
||||||
|
`Invalid server state. Dataset with id ${id} cannot be categorized. Dataset has server state ${dataset.server_state}.`,
|
||||||
|
)
|
||||||
|
.redirect()
|
||||||
|
.toRoute('dataset.list');
|
||||||
|
}
|
||||||
|
|
||||||
|
let trx: TransactionClientContract | null = null;
|
||||||
|
try {
|
||||||
|
trx = await db.transaction();
|
||||||
|
// const user = (await User.find(auth.user?.id)) as User;
|
||||||
|
// await this.createDatasetAndAssociations(user, request, trx);
|
||||||
|
|
||||||
|
// Retrieve the selected collections from the request.
|
||||||
|
// This should be an array of collection ids.
|
||||||
|
const collections: number[] = request.input('collections', []);
|
||||||
|
|
||||||
|
// Synchronize the dataset collections using the transaction.
|
||||||
|
await dataset.useTransaction(trx).related('collections').sync(collections);
|
||||||
|
|
||||||
|
// Commit the transaction.await trx.commit()
|
||||||
|
await trx.commit();
|
||||||
|
|
||||||
|
// Redirect with a success flash message.
|
||||||
|
// return response.flash('success', 'Dataset collections updated successfully!').redirect().toRoute('dataset.list');
|
||||||
|
|
||||||
|
session.flash('message', 'Dataset collections updated successfully!');
|
||||||
|
return response.redirect().toRoute('dataset.list');
|
||||||
|
} catch (error) {
|
||||||
|
if (trx !== null) {
|
||||||
|
await trx.rollback();
|
||||||
|
}
|
||||||
|
console.error('Failed tocatgorize dataset collections:', error);
|
||||||
|
// throw new ValidationException(true, { 'upload error': `failed to create dataset and related models. ${error}` });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ import DoiClientException from '#app/exceptions/DoiClientException';
|
||||||
import { StatusCodes } from 'http-status-codes';
|
import { StatusCodes } from 'http-status-codes';
|
||||||
import logger from '@adonisjs/core/services/logger';
|
import logger from '@adonisjs/core/services/logger';
|
||||||
import { AxiosResponse } from 'axios';
|
import { AxiosResponse } from 'axios';
|
||||||
import axios from 'axios';
|
import { default as axios } from 'axios';
|
||||||
|
|
||||||
export class DoiClient implements DoiClientContract {
|
export class DoiClient implements DoiClientContract {
|
||||||
public username: string;
|
public username: string;
|
||||||
|
@ -50,7 +50,7 @@ export class DoiClient implements DoiClientContract {
|
||||||
'Content-Type': 'application/xml;charset=UTF-8',
|
'Content-Type': 'application/xml;charset=UTF-8',
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
const metadataResponse = await axios.default.put(`${this.serviceUrl}/metadata/${doiValue}`, xmlMeta, { auth, headers });
|
const metadataResponse = await axios.put(`${this.serviceUrl}/metadata/${doiValue}`, xmlMeta, { auth, headers });
|
||||||
|
|
||||||
// Response Codes
|
// Response Codes
|
||||||
// 201 Created: operation successful
|
// 201 Created: operation successful
|
||||||
|
@ -65,7 +65,7 @@ export class DoiClient implements DoiClientContract {
|
||||||
throw new DoiClientException(metadataResponse.status, message);
|
throw new DoiClientException(metadataResponse.status, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
const doiResponse = await axios.default.put(`${this.serviceUrl}/doi/${doiValue}`, `doi=${doiValue}\nurl=${landingPageUrl}`, {
|
const doiResponse = await axios.put(`${this.serviceUrl}/doi/${doiValue}`, `doi=${doiValue}\nurl=${landingPageUrl}`, {
|
||||||
auth,
|
auth,
|
||||||
headers,
|
headers,
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,6 +4,7 @@ export default class ResumptionToken {
|
||||||
private _resumptionId = '';
|
private _resumptionId = '';
|
||||||
private _startPosition = 0;
|
private _startPosition = 0;
|
||||||
private _totalIds = 0;
|
private _totalIds = 0;
|
||||||
|
private _queryParams: Record<string, any> = {};
|
||||||
|
|
||||||
get key(): string {
|
get key(): string {
|
||||||
return this.metadataPrefix + this.startPosition + this.totalIds;
|
return this.metadataPrefix + this.startPosition + this.totalIds;
|
||||||
|
@ -48,4 +49,12 @@ export default class ResumptionToken {
|
||||||
set totalIds(totalIds: number) {
|
set totalIds(totalIds: number) {
|
||||||
this._totalIds = totalIds;
|
this._totalIds = totalIds;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get queryParams(): Record<string, any> {
|
||||||
|
return this._queryParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
set queryParams(params: Record<string, any>) {
|
||||||
|
this._queryParams = params;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,6 @@ export default abstract class TokenWorkerContract {
|
||||||
abstract connect(): void;
|
abstract connect(): void;
|
||||||
abstract close(): void;
|
abstract close(): void;
|
||||||
abstract get(key: string): Promise<ResumptionToken | null>;
|
abstract get(key: string): Promise<ResumptionToken | null>;
|
||||||
abstract set(token: ResumptionToken): Promise<string>;
|
abstract set(token: ResumptionToken, browserFingerprint: string): Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -40,14 +40,64 @@ export default class TokenWorkerService implements TokenWorkerContract {
|
||||||
return result !== undefined && result !== null;
|
return result !== undefined && result !== null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async set(token: ResumptionToken): Promise<string> {
|
/**
|
||||||
const uniqueName = await this.generateUniqueName();
|
* Simplified set method that stores the token using a browser fingerprint key.
|
||||||
|
* If the token for that fingerprint already exists and its documentIds match the new token,
|
||||||
|
* then the fingerprint key is simply returned.
|
||||||
|
*/
|
||||||
|
public async set(token: ResumptionToken, browserFingerprint: string): Promise<string> {
|
||||||
|
// Generate a 15-digit unique number string based on the fingerprint
|
||||||
|
const uniqueNumberKey = this.createUniqueNumberFromFingerprint(browserFingerprint, token.documentIds, token.totalIds);
|
||||||
|
// Optionally, you could prefix it if desired, e.g. 'rs_' + uniqueNumberKey
|
||||||
|
const fingerprintKey = uniqueNumberKey;
|
||||||
|
|
||||||
|
// const fingerprintKey = `rs_fp_${browserFingerprint}`;
|
||||||
|
const existingTokenString = await this.cache.get(fingerprintKey);
|
||||||
|
|
||||||
|
if (existingTokenString) {
|
||||||
|
const existingToken = this.parseToken(existingTokenString);
|
||||||
|
if (this.arraysAreEqual(existingToken.documentIds, token.documentIds)) {
|
||||||
|
return fingerprintKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const serialToken = JSON.stringify(token);
|
const serialToken = JSON.stringify(token);
|
||||||
await this.cache.setEx(uniqueName, this.ttl, serialToken);
|
await this.cache.setEx(fingerprintKey, this.ttl, serialToken);
|
||||||
return uniqueName;
|
return fingerprintKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Updated helper method to generate a unique key based on fingerprint and documentIds
|
||||||
|
private createUniqueNumberFromFingerprint(browserFingerprint: string, documentIds: number[], totalIds: number): string {
|
||||||
|
// Combine the fingerprint, document IDs and totalIds to produce the input string
|
||||||
|
const combined = browserFingerprint + ':' + documentIds.join('-') + ':' + totalIds;
|
||||||
|
// Simple hash algorithm
|
||||||
|
let hash = 0;
|
||||||
|
for (let i = 0; i < combined.length; i++) {
|
||||||
|
hash = (hash << 5) - hash + combined.charCodeAt(i);
|
||||||
|
hash |= 0; // Convert to 32-bit integer
|
||||||
|
}
|
||||||
|
// Ensure positive number and limit it to at most 15 digits
|
||||||
|
const positiveHash = Math.abs(hash) % 1000000000000000;
|
||||||
|
// Pad with trailing zeros to ensure a 15-digit string
|
||||||
|
return positiveHash.toString().padEnd(15, '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a helper function to compare two arrays of numbers with identical order
|
||||||
|
private arraysAreEqual(arr1: number[], arr2: number[]): boolean {
|
||||||
|
if (arr1.length !== arr2.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return arr1.every((num, index) => num === arr2[index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// public async set(token: ResumptionToken): Promise<string> {
|
||||||
|
// const uniqueName = await this.generateUniqueName();
|
||||||
|
|
||||||
|
// const serialToken = JSON.stringify(token);
|
||||||
|
// await this.cache.setEx(uniqueName, this.ttl, serialToken);
|
||||||
|
// return uniqueName;
|
||||||
|
// }
|
||||||
|
|
||||||
private async generateUniqueName(): Promise<string> {
|
private async generateUniqueName(): Promise<string> {
|
||||||
let fc = 0;
|
let fc = 0;
|
||||||
const uniqueId = dayjs().unix().toString();
|
const uniqueId = dayjs().unix().toString();
|
||||||
|
|
43
app/exceptions/db_handler.ts
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
// import { Exception } from '@adonisjs/core/exceptions'
|
||||||
|
import { HttpContext, ExceptionHandler } from '@adonisjs/core/http';
|
||||||
|
|
||||||
|
export default class DbHandlerException extends ExceptionHandler {
|
||||||
|
// constructor() {
|
||||||
|
// super(Logger)
|
||||||
|
// }
|
||||||
|
|
||||||
|
async handle(error: any, ctx: HttpContext) {
|
||||||
|
// Check for AggregateError type
|
||||||
|
if (error.type === 'AggregateError' && error.aggregateErrors) {
|
||||||
|
const dbErrors = error.aggregateErrors.some((err: any) => err.code === 'ECONNREFUSED' && err.port === 5432);
|
||||||
|
|
||||||
|
if (dbErrors) {
|
||||||
|
return ctx.response.status(503).json({
|
||||||
|
status: 'error',
|
||||||
|
message: 'PostgreSQL database connection failed. Please ensure the database service is running.',
|
||||||
|
details: {
|
||||||
|
code: error.code,
|
||||||
|
type: error.type,
|
||||||
|
ports: error.aggregateErrors.map((err: any) => ({
|
||||||
|
port: err.port,
|
||||||
|
address: err.address,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle simple ECONNREFUSED errors
|
||||||
|
if (error.code === 'ECONNREFUSED') {
|
||||||
|
return ctx.response.status(503).json({
|
||||||
|
status: 'error',
|
||||||
|
message: 'Database connection failed. Please ensure PostgreSQL is running.',
|
||||||
|
code: error.code,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.handle(error, ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
static status = 500;
|
||||||
|
}
|
|
@ -46,6 +46,7 @@ export default class HttpExceptionHandler extends ExceptionHandler {
|
||||||
// return view.render('./errors/server-error', { error });
|
// return view.render('./errors/server-error', { error });
|
||||||
// },
|
// },
|
||||||
// };
|
// };
|
||||||
|
|
||||||
protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
|
protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
|
||||||
'404': (error, { inertia }) => {
|
'404': (error, { inertia }) => {
|
||||||
return inertia.render('Errors/ServerError', {
|
return inertia.render('Errors/ServerError', {
|
||||||
|
@ -60,7 +61,45 @@ export default class HttpExceptionHandler extends ExceptionHandler {
|
||||||
code: error.status,
|
code: error.status,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
'500..599': (error, { inertia }) => inertia.render('Errors/ServerError', { error: error.message, code: error.status }),
|
// '500': (error, { inertia }) => {
|
||||||
|
// return inertia.render('Errors/postgres_error', {
|
||||||
|
// status: 'error',
|
||||||
|
// message: 'PostgreSQL database connection failed. Please ensure the database service is running.',
|
||||||
|
// details: {
|
||||||
|
// code: error.code,
|
||||||
|
// type: error.status,
|
||||||
|
// ports: error.errors.map((err: any) => ({
|
||||||
|
// port: err.port,
|
||||||
|
// address: err.address,
|
||||||
|
// })),
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// },
|
||||||
|
'500..599': (error, { inertia }) => {
|
||||||
|
if (error.code === 'ECONNREFUSED') {
|
||||||
|
const dbErrors = error.errors.some((err: any) => err.code === 'ECONNREFUSED' && err.port === 5432);
|
||||||
|
|
||||||
|
if (dbErrors) {
|
||||||
|
return inertia.render('Errors/postgres_error', {
|
||||||
|
status: 'error',
|
||||||
|
message: 'PostgreSQL database connection failed. Please ensure the database service is running.',
|
||||||
|
details: {
|
||||||
|
code: error.code,
|
||||||
|
type: error.status,
|
||||||
|
ports: error.errors.map((err: any) => ({
|
||||||
|
port: err.port,
|
||||||
|
address: err.address,
|
||||||
|
})),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return inertia.render('Errors/ServerError', {
|
||||||
|
error: error.message,
|
||||||
|
code: error.status,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// constructor() {
|
// constructor() {
|
||||||
|
@ -68,7 +107,7 @@ export default class HttpExceptionHandler extends ExceptionHandler {
|
||||||
// }
|
// }
|
||||||
|
|
||||||
public async handle(error: any, ctx: HttpContext) {
|
public async handle(error: any, ctx: HttpContext) {
|
||||||
const { response, request, session } = ctx;
|
const { response, request, session, inertia } = ctx;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle failed authentication attempt
|
* Handle failed authentication attempt
|
||||||
|
@ -82,6 +121,47 @@ export default class HttpExceptionHandler extends ExceptionHandler {
|
||||||
// return response.redirect('/dashboard');
|
// return response.redirect('/dashboard');
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// Handle Axios errors
|
||||||
|
if (error.code === 'ECONNREFUSED') {
|
||||||
|
const dbErrors = error.errors.some((err: any) => err.code === 'ECONNREFUSED' && err.port === 5432);
|
||||||
|
|
||||||
|
if (dbErrors) {
|
||||||
|
// return ctx.response.status(503).json({
|
||||||
|
// status: 'error',
|
||||||
|
// message: 'PostgreSQL database connection failed. Please ensure the database service is running.',
|
||||||
|
// details: {
|
||||||
|
// code: error.code,
|
||||||
|
// type: error.status,
|
||||||
|
// ports: error.errors.map((err: any) => ({
|
||||||
|
// port: err.port,
|
||||||
|
// address: err.address,
|
||||||
|
// })),
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
// return inertia.render('Errors/postgres_error', {
|
||||||
|
// status: 'error',
|
||||||
|
// message: 'PostgreSQL database connection failed. Please ensure the database service is running.',
|
||||||
|
// details: {
|
||||||
|
// code: error.code,
|
||||||
|
// type: error.status,
|
||||||
|
// ports: error.errors.map((err: any) => ({
|
||||||
|
// port: err.port,
|
||||||
|
// address: err.address,
|
||||||
|
// })),
|
||||||
|
// },
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle simple ECONNREFUSED errors
|
||||||
|
// if (error.code === 'ECONNREFUSED') {
|
||||||
|
// return ctx.response.status(503).json({
|
||||||
|
// status: 'error',
|
||||||
|
// message: 'Database connection failed. Please ensure PostgreSQL is running.',
|
||||||
|
// code: error.code,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
// https://github.com/inertiajs/inertia-laravel/issues/56
|
// https://github.com/inertiajs/inertia-laravel/issues/56
|
||||||
// let test = response.getStatus(); //200
|
// let test = response.getStatus(); //200
|
||||||
// let header = request.header('X-Inertia'); // true
|
// let header = request.header('X-Inertia'); // true
|
||||||
|
@ -98,12 +178,21 @@ export default class HttpExceptionHandler extends ExceptionHandler {
|
||||||
// ->toResponse($request)
|
// ->toResponse($request)
|
||||||
// ->setStatusCode($response->status());
|
// ->setStatusCode($response->status());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle simple ECONNREFUSED errors
|
||||||
|
// if (error.code === 'ECONNREFUSED') {
|
||||||
|
// return ctx.response.status(503).json({
|
||||||
|
// status: 'error',
|
||||||
|
// message: 'Database connection failed. Please ensure PostgreSQL is running.',
|
||||||
|
// code: error.code,
|
||||||
|
// });
|
||||||
|
// }
|
||||||
// Dynamically change the error templates based on the absence of X-Inertia header
|
// Dynamically change the error templates based on the absence of X-Inertia header
|
||||||
// if (!ctx.request.header('X-Inertia')) {
|
// if (!ctx.request.header('X-Inertia')) {
|
||||||
// this.statusPages = {
|
// this.statusPages = {
|
||||||
// '401..403': (error, { view }) => view.render('./errors/unauthorized', { error }),
|
// '401..403': (error, { inertia }) => inertia.render('Errors/ServerError', { error: error.message, code: error.status }),
|
||||||
// '404': (error, { view }) => view.render('./errors/not-found', { error }),
|
// '404': (error, { inertia }) => inertia.render('Errors/ServerError', { error: error.message, code: error.status }),
|
||||||
// '500..599': (error, { view }) => view.render('./errors/server-error', { error }),
|
// '500..599': (error, { inertia }) => inertia.render('Errors/ServerError', { error: error.message, code: error.status }),
|
||||||
// };
|
// };
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
|
|
@ -209,6 +209,15 @@ export default class Dataset extends DatasetExtension {
|
||||||
return mainTitle ? mainTitle.value : null;
|
return mainTitle ? mainTitle.value : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed({
|
||||||
|
serializeAs: 'doi_identifier',
|
||||||
|
})
|
||||||
|
public get doiIdentifier() {
|
||||||
|
// return `${this.firstName} ${this.lastName}`;
|
||||||
|
const identifier: DatasetIdentifier = this.identifier;
|
||||||
|
return identifier ? identifier.value : null;
|
||||||
|
}
|
||||||
|
|
||||||
@manyToMany(() => Person, {
|
@manyToMany(() => Person, {
|
||||||
pivotForeignKey: 'document_id',
|
pivotForeignKey: 'document_id',
|
||||||
pivotRelatedForeignKey: 'person_id',
|
pivotRelatedForeignKey: 'person_id',
|
||||||
|
|
|
@ -3,12 +3,12 @@ import { column, hasMany, belongsTo, SnakeCaseNamingStrategy, computed } from '@
|
||||||
import HashValue from './hash_value.js';
|
import HashValue from './hash_value.js';
|
||||||
import Dataset from './dataset.js';
|
import Dataset from './dataset.js';
|
||||||
import BaseModel from './base_model.js';
|
import BaseModel from './base_model.js';
|
||||||
// import { Buffer } from 'buffer';
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
import crypto from 'crypto';
|
import crypto from 'crypto';
|
||||||
// import Drive from '@ioc:Adonis/Core/Drive';
|
// import Drive from '@ioc:Adonis/Core/Drive';
|
||||||
// import Drive from '@adonisjs/drive';
|
// import Drive from '@adonisjs/drive';
|
||||||
import drive from '#services/drive';
|
// import drive from '#services/drive';
|
||||||
|
import drive from '@adonisjs/drive/services/main';
|
||||||
|
|
||||||
import type { HasMany } from "@adonisjs/lucid/types/relations";
|
import type { HasMany } from "@adonisjs/lucid/types/relations";
|
||||||
import type { BelongsTo } from "@adonisjs/lucid/types/relations";
|
import type { BelongsTo } from "@adonisjs/lucid/types/relations";
|
||||||
|
@ -88,7 +88,8 @@ export default class File extends BaseModel {
|
||||||
serializeAs: 'filePath',
|
serializeAs: 'filePath',
|
||||||
})
|
})
|
||||||
public get filePath() {
|
public get filePath() {
|
||||||
return `/storage/app/public/${this.pathName}`;
|
// return `/storage/app/public/${this.pathName}`;
|
||||||
|
return `/storage/app/data/${this.pathName}`;
|
||||||
// const mainTitle = this.titles?.find((title) => title.type === 'Main');
|
// const mainTitle = this.titles?.find((title) => title.type === 'Main');
|
||||||
// return mainTitle ? mainTitle.value : null;
|
// return mainTitle ? mainTitle.value : null;
|
||||||
}
|
}
|
||||||
|
@ -165,7 +166,7 @@ export default class File extends BaseModel {
|
||||||
public async delete() {
|
public async delete() {
|
||||||
if (this.pathName) {
|
if (this.pathName) {
|
||||||
// Delete file from additional storage
|
// Delete file from additional storage
|
||||||
await drive.delete(this.pathName);
|
await drive.use('local').delete(this.pathName);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Call the original delete method of the BaseModel to remove the record from the database
|
// Call the original delete method of the BaseModel to remove the record from the database
|
||||||
|
|
|
@ -16,9 +16,14 @@ export default class MimeType extends BaseModel {
|
||||||
@column({})
|
@column({})
|
||||||
public name: string;
|
public name: string;
|
||||||
|
|
||||||
|
// 1 : n file_extensions are separated by '|' in the database
|
||||||
@column({})
|
@column({})
|
||||||
public file_extension: string;
|
public file_extension: string;
|
||||||
|
|
||||||
|
// 1 : n alternate_mimetype are separated by '|' in the database
|
||||||
|
@column({})
|
||||||
|
public alternate_mimetype: string;
|
||||||
|
|
||||||
@column({})
|
@column({})
|
||||||
public enabled: boolean;
|
public enabled: boolean;
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { DateTime } from 'luxon';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import Dataset from './dataset.js';
|
import Dataset from './dataset.js';
|
||||||
import BaseModel from './base_model.js';
|
import BaseModel from './base_model.js';
|
||||||
import type { ManyToMany } from "@adonisjs/lucid/types/relations";
|
import type { ManyToMany } from '@adonisjs/lucid/types/relations';
|
||||||
|
|
||||||
export default class Person extends BaseModel {
|
export default class Person extends BaseModel {
|
||||||
public static namingStrategy = new SnakeCaseNamingStrategy();
|
public static namingStrategy = new SnakeCaseNamingStrategy();
|
||||||
|
@ -51,7 +51,7 @@ export default class Person extends BaseModel {
|
||||||
serializeAs: 'name',
|
serializeAs: 'name',
|
||||||
})
|
})
|
||||||
public get fullName() {
|
public get fullName() {
|
||||||
return `${this.firstName} ${this.lastName}`;
|
return [this.firstName, this.lastName].filter(Boolean).join(' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
// @computed()
|
// @computed()
|
||||||
|
@ -64,10 +64,12 @@ export default class Person extends BaseModel {
|
||||||
// return '2023-03-21 08:45:00';
|
// return '2023-03-21 08:45:00';
|
||||||
// }
|
// }
|
||||||
|
|
||||||
@computed()
|
@computed({
|
||||||
|
serializeAs: 'dataset_count',
|
||||||
|
})
|
||||||
public get datasetCount() {
|
public get datasetCount() {
|
||||||
const stock = this.$extras.datasets_count; //my pivot column name was "stock"
|
const stock = this.$extras.datasets_count; //my pivot column name was "stock"
|
||||||
return stock;
|
return Number(stock);
|
||||||
}
|
}
|
||||||
|
|
||||||
@computed()
|
@computed()
|
||||||
|
@ -76,6 +78,16 @@ export default class Person extends BaseModel {
|
||||||
return contributor_type;
|
return contributor_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@computed({ serializeAs: 'allow_email_contact' })
|
||||||
|
public get allowEmailContact() {
|
||||||
|
// If the datasets relation is missing or empty, return false instead of null.
|
||||||
|
if (!this.datasets || this.datasets.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Otherwise return the pivot attribute from the first related dataset.
|
||||||
|
return this.datasets[0].$extras?.pivot_allow_email_contact;
|
||||||
|
}
|
||||||
|
|
||||||
@manyToMany(() => Dataset, {
|
@manyToMany(() => Dataset, {
|
||||||
pivotForeignKey: 'person_id',
|
pivotForeignKey: 'person_id',
|
||||||
pivotRelatedForeignKey: 'document_id',
|
pivotRelatedForeignKey: 'document_id',
|
||||||
|
|
57
app/models/types.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
/**
|
||||||
|
* Qs module config
|
||||||
|
*/
|
||||||
|
type QueryStringConfig = {
|
||||||
|
depth?: number
|
||||||
|
allowPrototypes?: boolean
|
||||||
|
plainObjects?: boolean
|
||||||
|
parameterLimit?: number
|
||||||
|
arrayLimit?: number
|
||||||
|
ignoreQueryPrefix?: boolean
|
||||||
|
delimiter?: RegExp | string
|
||||||
|
allowDots?: boolean
|
||||||
|
charset?: 'utf-8' | 'iso-8859-1' | undefined
|
||||||
|
charsetSentinel?: boolean
|
||||||
|
interpretNumericEntities?: boolean
|
||||||
|
parseArrays?: boolean
|
||||||
|
comma?: boolean
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Base config used by all types
|
||||||
|
*/
|
||||||
|
type BodyParserBaseConfig = {
|
||||||
|
encoding: string
|
||||||
|
limit: string | number
|
||||||
|
types: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Body parser config for parsing JSON requests
|
||||||
|
*/
|
||||||
|
export type BodyParserJSONConfig = BodyParserBaseConfig & {
|
||||||
|
strict: boolean
|
||||||
|
convertEmptyStringsToNull: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser config for parsing form data
|
||||||
|
*/
|
||||||
|
export type BodyParserFormConfig = BodyParserBaseConfig & {
|
||||||
|
queryString: QueryStringConfig
|
||||||
|
convertEmptyStringsToNull: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser config for parsing raw body (untouched)
|
||||||
|
*/
|
||||||
|
export type BodyParserRawConfig = BodyParserBaseConfig
|
||||||
|
/**
|
||||||
|
* Body parser config for all supported form types
|
||||||
|
*/
|
||||||
|
export type BodyParserConfig = {
|
||||||
|
allowedMethods: string[]
|
||||||
|
json: BodyParserJSONConfig
|
||||||
|
form: BodyParserFormConfig
|
||||||
|
raw: BodyParserRawConfig
|
||||||
|
multipart: BodyParserMultipartConfig
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid';
|
import { withAuthFinder } from '@adonisjs/auth/mixins/lucid';
|
||||||
import { column, manyToMany, hasMany, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm';
|
import { column, manyToMany, hasMany, SnakeCaseNamingStrategy, computed, beforeFetch, beforeFind } from '@adonisjs/lucid/orm';
|
||||||
import hash from '@adonisjs/core/services/hash';
|
import hash from '@adonisjs/core/services/hash';
|
||||||
import Role from './role.js';
|
import Role from './role.js';
|
||||||
import db from '@adonisjs/lucid/services/db';
|
import db from '@adonisjs/lucid/services/db';
|
||||||
|
@ -49,7 +49,6 @@ export default class User extends compose(BaseModel, AuthFinder) {
|
||||||
@column()
|
@column()
|
||||||
public login: string;
|
public login: string;
|
||||||
|
|
||||||
|
|
||||||
@column()
|
@column()
|
||||||
public firstName: string;
|
public firstName: string;
|
||||||
|
|
||||||
|
@ -87,6 +86,9 @@ export default class User extends compose(BaseModel, AuthFinder) {
|
||||||
@column({})
|
@column({})
|
||||||
public state: number;
|
public state: number;
|
||||||
|
|
||||||
|
@column({})
|
||||||
|
public avatar: string;
|
||||||
|
|
||||||
// @hasOne(() => TotpSecret, {
|
// @hasOne(() => TotpSecret, {
|
||||||
// foreignKey: 'user_id',
|
// foreignKey: 'user_id',
|
||||||
// })
|
// })
|
||||||
|
@ -104,6 +106,7 @@ export default class User extends compose(BaseModel, AuthFinder) {
|
||||||
// return Boolean(this.totp_secret?.twoFactorSecret);
|
// return Boolean(this.totp_secret?.twoFactorSecret);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@manyToMany(() => Role, {
|
@manyToMany(() => Role, {
|
||||||
pivotForeignKey: 'account_id',
|
pivotForeignKey: 'account_id',
|
||||||
pivotRelatedForeignKey: 'role_id',
|
pivotRelatedForeignKey: 'role_id',
|
||||||
|
@ -121,6 +124,27 @@ export default class User extends compose(BaseModel, AuthFinder) {
|
||||||
})
|
})
|
||||||
public backupcodes: HasMany<typeof BackupCode>;
|
public backupcodes: HasMany<typeof BackupCode>;
|
||||||
|
|
||||||
|
@computed({
|
||||||
|
serializeAs: 'is_admin',
|
||||||
|
})
|
||||||
|
public get isAdmin(): boolean {
|
||||||
|
const roles = this.roles;
|
||||||
|
const isAdmin = roles?.map((role: Role) => role.name).includes('administrator');
|
||||||
|
return isAdmin;
|
||||||
|
}
|
||||||
|
|
||||||
|
// public toJSON() {
|
||||||
|
// return {
|
||||||
|
// ...super.toJSON(),
|
||||||
|
// roles: []
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
@beforeFind()
|
||||||
|
@beforeFetch()
|
||||||
|
public static preloadRoles(user: User) {
|
||||||
|
user.preload('roles')
|
||||||
|
}
|
||||||
|
|
||||||
public async getBackupCodes(this: User): Promise<BackupCode[]> {
|
public async getBackupCodes(this: User): Promise<BackupCode[]> {
|
||||||
const test = await this.related('backupcodes').query();
|
const test = await this.related('backupcodes').query();
|
||||||
// return test.map((role) => role.code);
|
// return test.map((role) => role.code);
|
||||||
|
|
|
@ -1,3 +1,16 @@
|
||||||
|
import { join, isAbsolute } from 'node:path';
|
||||||
|
import type { BodyParserConfig } from '#models/types';
|
||||||
|
import { createId } from '@paralleldrive/cuid2';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import config from '@adonisjs/core/services/config';
|
||||||
|
import Dataset from '#models/dataset';
|
||||||
|
import { TransactionClientContract } from '@adonisjs/lucid/types/database';
|
||||||
|
import Person from '#models/person';
|
||||||
|
|
||||||
|
interface Dictionary {
|
||||||
|
[index: string]: string;
|
||||||
|
}
|
||||||
|
|
||||||
export function sum(a: number, b: number): number {
|
export function sum(a: number, b: number): number {
|
||||||
return a + b;
|
return a + b;
|
||||||
}
|
}
|
||||||
|
@ -24,3 +37,88 @@ export function preg_match(regex: RegExp, str: string) {
|
||||||
const result: boolean = regex.test(str);
|
const result: boolean = regex.test(str);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the tmp path for storing the files temporarly
|
||||||
|
*/
|
||||||
|
export function getTmpPath(config: BodyParserConfig['multipart']): string {
|
||||||
|
if (typeof config.tmpFileName === 'function') {
|
||||||
|
const tmpPath = config.tmpFileName();
|
||||||
|
return isAbsolute(tmpPath) ? tmpPath : join(tmpdir(), tmpPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return join(tmpdir(), createId());
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Returns config for a given type
|
||||||
|
*/
|
||||||
|
export function getConfigFor<K extends keyof BodyParserConfig>(type: K): BodyParserConfig[K] {
|
||||||
|
const bodyParserConfig: BodyParserConfig = config.get('bodyparser');
|
||||||
|
const configType = bodyParserConfig[type];
|
||||||
|
return configType;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseBytesSize(size: string): number {
|
||||||
|
const units: Record<string, number> = {
|
||||||
|
kb: 1024,
|
||||||
|
mb: 1024 * 1024,
|
||||||
|
gb: 1024 * 1024 * 1024,
|
||||||
|
tb: 1024 * 1024 * 1024 * 1024,
|
||||||
|
};
|
||||||
|
|
||||||
|
const match = size.match(/^(\d+)(kb|mb|gb|tb)$/i); // Regex to match size format
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
throw new Error('Invalid size format');
|
||||||
|
}
|
||||||
|
|
||||||
|
const [, value, unit] = match;
|
||||||
|
return parseInt(value) * units[unit.toLowerCase()];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to format bytes as human-readable text
|
||||||
|
|
||||||
|
export function formatBytes(bytes: number): string {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function savePersons(dataset: Dataset, persons: any[], role: string, trx: TransactionClientContract) {
|
||||||
|
for (const [key, person] of persons.entries()) {
|
||||||
|
const pivotData = {
|
||||||
|
role: role,
|
||||||
|
sort_order: key + 1,
|
||||||
|
allow_email_contact: false,
|
||||||
|
...extractPivotAttributes(person), // Merge pivot attributes here
|
||||||
|
};
|
||||||
|
|
||||||
|
if (person.id !== undefined) {
|
||||||
|
await dataset
|
||||||
|
.useTransaction(trx)
|
||||||
|
.related('persons')
|
||||||
|
.attach({
|
||||||
|
[person.id]: pivotData,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const dataPerson = new Person();
|
||||||
|
dataPerson.fill(person);
|
||||||
|
await dataset.useTransaction(trx).related('persons').save(dataPerson, false, pivotData);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to extract pivot attributes from a person object
|
||||||
|
function extractPivotAttributes(person: any) {
|
||||||
|
const pivotAttributes: Dictionary = {};
|
||||||
|
for (const key in person) {
|
||||||
|
if (key.startsWith('pivot_')) {
|
||||||
|
// pivotAttributes[key] = person[key];
|
||||||
|
const cleanKey = key.replace('pivot_', ''); // Remove 'pivot_' prefix
|
||||||
|
pivotAttributes[cleanKey] = person[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return pivotAttributes;
|
||||||
|
}
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
|
import vine, { SimpleMessagesProvider } from '@vinejs/vine';
|
||||||
import { TitleTypes, DescriptionTypes, ContributorTypes, ReferenceIdentifierTypes, RelationTypes } from '#contracts/enums';
|
import { TitleTypes, DescriptionTypes, ContributorTypes, ReferenceIdentifierTypes, RelationTypes } from '#contracts/enums';
|
||||||
import dayjs from 'dayjs';
|
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();
|
||||||
|
@ -39,7 +40,8 @@ export const createDatasetValidator = vine.compile(
|
||||||
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.minLength(1),
|
// .minLength(2)
|
||||||
|
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
|
||||||
descriptions: vine
|
descriptions: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
|
@ -53,7 +55,8 @@ export const createDatasetValidator = vine.compile(
|
||||||
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.minLength(1),
|
// .minLength(1),
|
||||||
|
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
|
||||||
authors: vine
|
authors: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
|
@ -125,7 +128,7 @@ export const createDatasetValidator = vine.compile(
|
||||||
references: vine
|
references: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
value: vine.string().trim().minLength(3).maxLength(255),
|
value: vine.string().trim().minLength(3).maxLength(255).validateReference({ typeField: 'type' }),
|
||||||
type: vine.enum(Object.values(ReferenceIdentifierTypes)),
|
type: vine.enum(Object.values(ReferenceIdentifierTypes)),
|
||||||
relation: vine.enum(Object.values(RelationTypes)),
|
relation: vine.enum(Object.values(RelationTypes)),
|
||||||
label: vine.string().trim().minLength(2).maxLength(255),
|
label: vine.string().trim().minLength(2).maxLength(255),
|
||||||
|
@ -155,8 +158,7 @@ export const createDatasetValidator = vine.compile(
|
||||||
.fileScan({ removeInfected: true }),
|
.fileScan({ removeInfected: true }),
|
||||||
)
|
)
|
||||||
.minLength(1),
|
.minLength(1),
|
||||||
}),
|
}),);
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validates the dataset's update action
|
* Validates the dataset's update action
|
||||||
|
@ -186,7 +188,8 @@ export const updateDatasetValidator = vine.compile(
|
||||||
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.minLength(1),
|
// .minLength(2)
|
||||||
|
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
|
||||||
descriptions: vine
|
descriptions: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
|
@ -200,7 +203,7 @@ export const updateDatasetValidator = vine.compile(
|
||||||
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.minLength(1),
|
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
|
||||||
authors: vine
|
authors: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
|
@ -272,7 +275,7 @@ export const updateDatasetValidator = vine.compile(
|
||||||
references: vine
|
references: vine
|
||||||
.array(
|
.array(
|
||||||
vine.object({
|
vine.object({
|
||||||
value: vine.string().trim().minLength(3).maxLength(255),
|
value: vine.string().trim().minLength(3).maxLength(255).validateReference({ typeField: 'type' }),
|
||||||
type: vine.enum(Object.values(ReferenceIdentifierTypes)),
|
type: vine.enum(Object.values(ReferenceIdentifierTypes)),
|
||||||
relation: vine.enum(Object.values(RelationTypes)),
|
relation: vine.enum(Object.values(RelationTypes)),
|
||||||
label: vine.string().trim().minLength(2).maxLength(255),
|
label: vine.string().trim().minLength(2).maxLength(255),
|
||||||
|
@ -311,12 +314,137 @@ export const updateDatasetValidator = vine.compile(
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// files: schema.array([rules.minLength(1)]).members(
|
export const updateEditorDatasetValidator = vine.compile(
|
||||||
// schema.file({
|
vine.object({
|
||||||
// size: '512mb',
|
// first step
|
||||||
// extnames: ['jpg', 'gif', 'png', 'tif', 'pdf', 'zip', 'fgb', 'nc', 'qml', 'ovr', 'gpkg', 'gml', 'gpx', 'kml', 'kmz', 'json'],
|
language: vine
|
||||||
// }),
|
.string()
|
||||||
// ),
|
.trim()
|
||||||
|
.regex(/^[a-zA-Z0-9]+$/),
|
||||||
|
licenses: vine.array(vine.number()).minLength(1), // define at least one license for the new dataset
|
||||||
|
rights: vine.string().in(['true']),
|
||||||
|
// second step
|
||||||
|
type: vine.string().trim().minLength(3).maxLength(255),
|
||||||
|
creating_corporation: vine.string().trim().minLength(3).maxLength(255),
|
||||||
|
titles: vine
|
||||||
|
.array(
|
||||||
|
vine.object({
|
||||||
|
value: vine.string().trim().minLength(3).maxLength(255),
|
||||||
|
type: vine.enum(Object.values(TitleTypes)),
|
||||||
|
language: vine
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.minLength(2)
|
||||||
|
.maxLength(255)
|
||||||
|
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
// .minLength(2)
|
||||||
|
.arrayContainsTypes({ typeA: 'main', typeB: 'translated' }),
|
||||||
|
descriptions: vine
|
||||||
|
.array(
|
||||||
|
vine.object({
|
||||||
|
value: vine.string().trim().minLength(3).maxLength(2500),
|
||||||
|
type: vine.enum(Object.values(DescriptionTypes)),
|
||||||
|
language: vine
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.minLength(2)
|
||||||
|
.maxLength(255)
|
||||||
|
.translatedLanguage({ mainLanguageField: 'language', typeField: 'type' }),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.arrayContainsTypes({ typeA: 'abstract', typeB: 'translated' }),
|
||||||
|
authors: vine
|
||||||
|
.array(
|
||||||
|
vine.object({
|
||||||
|
email: vine
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.maxLength(255)
|
||||||
|
.email()
|
||||||
|
.normalizeEmail()
|
||||||
|
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
|
||||||
|
first_name: vine.string().trim().minLength(3).maxLength(255),
|
||||||
|
last_name: vine.string().trim().minLength(3).maxLength(255),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.minLength(1)
|
||||||
|
.distinct('email'),
|
||||||
|
contributors: vine
|
||||||
|
.array(
|
||||||
|
vine.object({
|
||||||
|
email: vine
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.maxLength(255)
|
||||||
|
.email()
|
||||||
|
.normalizeEmail()
|
||||||
|
.isUniquePerson({ table: 'persons', column: 'email', idField: 'id' }),
|
||||||
|
first_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)),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.distinct('email')
|
||||||
|
.optional(),
|
||||||
|
// third step
|
||||||
|
project_id: vine.number().optional(),
|
||||||
|
// embargo_date: schema.date.optional({ format: 'yyyy-MM-dd' }, [rules.after(10, 'days')]),
|
||||||
|
embargo_date: vine
|
||||||
|
.date({
|
||||||
|
formats: ['YYYY-MM-DD'],
|
||||||
|
})
|
||||||
|
.afterOrEqual((_field) => {
|
||||||
|
return dayjs().add(10, 'day').format('YYYY-MM-DD');
|
||||||
|
})
|
||||||
|
.optional(),
|
||||||
|
coverage: vine.object({
|
||||||
|
x_min: vine.number(),
|
||||||
|
x_max: vine.number(),
|
||||||
|
y_min: vine.number(),
|
||||||
|
y_max: vine.number(),
|
||||||
|
elevation_absolut: vine.number().positive().optional(),
|
||||||
|
elevation_min: vine.number().positive().optional().requiredIfExists('elevation_max'),
|
||||||
|
elevation_max: vine.number().positive().optional().requiredIfExists('elevation_min'),
|
||||||
|
// type: vine.enum(Object.values(DescriptionTypes)),
|
||||||
|
depth_absolut: vine.number().negative().optional(),
|
||||||
|
depth_min: vine.number().negative().optional().requiredIfExists('depth_max'),
|
||||||
|
depth_max: vine.number().negative().optional().requiredIfExists('depth_min'),
|
||||||
|
time_abolute: vine.date({ formats: { utc: true } }).optional(),
|
||||||
|
time_min: vine
|
||||||
|
.date({ formats: { utc: true } })
|
||||||
|
.beforeField('time_max')
|
||||||
|
.optional()
|
||||||
|
.requiredIfExists('time_max'),
|
||||||
|
time_max: vine
|
||||||
|
.date({ formats: { utc: true } })
|
||||||
|
.afterField('time_min')
|
||||||
|
.optional()
|
||||||
|
.requiredIfExists('time_min'),
|
||||||
|
}),
|
||||||
|
references: vine
|
||||||
|
.array(
|
||||||
|
vine.object({
|
||||||
|
value: vine.string().trim().minLength(3).maxLength(255).validateReference({ typeField: 'type' }),
|
||||||
|
type: vine.enum(Object.values(ReferenceIdentifierTypes)),
|
||||||
|
relation: vine.enum(Object.values(RelationTypes)),
|
||||||
|
label: vine.string().trim().minLength(2).maxLength(255),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.optional(),
|
||||||
|
subjects: vine
|
||||||
|
.array(
|
||||||
|
vine.object({
|
||||||
|
value: vine.string().trim().minLength(3).maxLength(255),
|
||||||
|
// pivot_contributor_type: vine.enum(Object.keys(ContributorTypes)),
|
||||||
|
language: vine.string().trim().minLength(2).maxLength(255),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.minLength(3)
|
||||||
|
.distinct('value'),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
let messagesProvider = new SimpleMessagesProvider({
|
let messagesProvider = new SimpleMessagesProvider({
|
||||||
'minLength': '{{ field }} must be at least {{ min }} characters long',
|
'minLength': '{{ field }} must be at least {{ min }} characters long',
|
||||||
|
@ -368,8 +496,10 @@ let messagesProvider = new SimpleMessagesProvider({
|
||||||
'files.array.minLength': 'At least {{ min }} file upload is required.',
|
'files.array.minLength': 'At least {{ min }} file upload is required.',
|
||||||
'files.*.size': 'file size is to big',
|
'files.*.size': 'file size is to big',
|
||||||
'files.*.extnames': 'file extension is not supported',
|
'files.*.extnames': 'file extension is not supported',
|
||||||
|
'embargo_date.date.afterOrEqual': `Embargo date must be on or after ${dayjs().add(10, 'day').format('DD.MM.YYYY')}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
createDatasetValidator.messagesProvider = messagesProvider;
|
createDatasetValidator.messagesProvider = messagesProvider;
|
||||||
updateDatasetValidator.messagesProvider = messagesProvider;
|
updateDatasetValidator.messagesProvider = messagesProvider;
|
||||||
|
updateEditorDatasetValidator.messagesProvider = messagesProvider;
|
||||||
// export default createDatasetValidator;
|
// export default createDatasetValidator;
|
||||||
|
|
|
@ -16,7 +16,7 @@ export const createUserValidator = vine.compile(
|
||||||
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),
|
||||||
email: vine.string().maxLength(255).email().normalizeEmail().isUnique({ table: 'accounts', column: 'email' }),
|
email: vine.string().maxLength(255).email().normalizeEmail().isUnique({ table: 'accounts', column: 'email' }),
|
||||||
password: vine.string().confirmed().trim().minLength(3).maxLength(60),
|
new_password: vine.string().confirmed({ confirmationField: 'password_confirmation' }).trim().minLength(3).maxLength(60),
|
||||||
roles: vine.array(vine.number()).minLength(1), // define at least one role for the new user
|
roles: vine.array(vine.number()).minLength(1), // define at least one role for the new user
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
@ -42,7 +42,7 @@ export const updateUserValidator = vine.withMetaData<{ objId: number }>().compil
|
||||||
.email()
|
.email()
|
||||||
.normalizeEmail()
|
.normalizeEmail()
|
||||||
.isUnique({ table: 'accounts', column: 'email', whereNot: (field) => field.meta.objId }),
|
.isUnique({ table: 'accounts', column: 'email', whereNot: (field) => field.meta.objId }),
|
||||||
password: vine.string().confirmed().trim().minLength(3).maxLength(60).optional(),
|
new_password: vine.string().confirmed({ confirmationField: 'password_confirmation' }).trim().minLength(3).maxLength(60).optional(),
|
||||||
roles: vine.array(vine.number()).minLength(1), // define at least one role for the new user
|
roles: vine.array(vine.number()).minLength(1), // define at least one role for the new user
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
|
@ -142,7 +142,7 @@ export class VanillaErrorReporter implements ErrorReporterContract {
|
||||||
// }
|
// }
|
||||||
this.hasErrors = true;
|
this.hasErrors = true;
|
||||||
|
|
||||||
var test = field.getFieldPath();
|
// var test = field.getFieldPath();
|
||||||
|
|
||||||
// this.errors.push(error);
|
// this.errors.push(error);
|
||||||
// if (this.errors[error.field]) {
|
// if (this.errors[error.field]) {
|
||||||
|
|
|
@ -88,7 +88,7 @@ export default class ValidateChecksum extends BaseCommand {
|
||||||
);
|
);
|
||||||
|
|
||||||
// Construct the file path
|
// Construct the file path
|
||||||
const filePath = '/storage/app/public/' + file.pathName;
|
const filePath = '/storage/app/data/' + file.pathName;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Calculate the MD5 checksum of the file
|
// Calculate the MD5 checksum of the file
|
||||||
|
|
|
@ -80,7 +80,8 @@ export const http = defineConfig({
|
||||||
| headers.
|
| headers.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
trustProxy: proxyAddr.compile('loopback'),
|
// trustProxy: proxyAddr.compile('loopback'),
|
||||||
|
trustProxy: proxyAddr.compile(['127.0.0.1', '::1/128']),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
@ -128,7 +128,7 @@ allowedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
|
||||||
| projects/:id/file
|
| projects/:id/file
|
||||||
| ```
|
| ```
|
||||||
*/
|
*/
|
||||||
processManually: [],
|
processManually: ['/submitter/dataset/submit', '/submitter/dataset/:id/update'],
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
@ -185,8 +185,8 @@ allowedMethods: ['POST', 'PUT', 'PATCH', 'DELETE'],
|
||||||
| and fields data.
|
| and fields data.
|
||||||
|
|
|
|
||||||
*/
|
*/
|
||||||
// limit: '20mb',
|
limit: '513mb',
|
||||||
limit: env.get('UPLOAD_LIMIT', '513mb'),
|
//limit: env.get('UPLOAD_LIMIT', '513mb'),
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
@ -47,7 +47,7 @@ const databaseConfig = defineConfig({
|
||||||
migrations: {
|
migrations: {
|
||||||
naturalSort: true,
|
naturalSort: true,
|
||||||
},
|
},
|
||||||
healthCheck: false,
|
// healthCheck: false,
|
||||||
debug: false,
|
debug: false,
|
||||||
pool: { min: 1, max: 100 },
|
pool: { min: 1, max: 100 },
|
||||||
},
|
},
|
||||||
|
|
180
config/drive.ts
|
@ -1,151 +1,45 @@
|
||||||
/**
|
// import env from '#start/env'
|
||||||
* Config source: https://git.io/JBt3o
|
// import app from '@adonisjs/core/services/app'
|
||||||
*
|
import { defineConfig, services } from '@adonisjs/drive'
|
||||||
* Feel free to let us know via PR, if you find something broken in this config
|
|
||||||
* file.
|
|
||||||
*/
|
|
||||||
import { defineConfig } from '#providers/drive/src/types/define_config';
|
|
||||||
import env from '#start/env';
|
|
||||||
// import { driveConfig } from '@adonisjs/core/build/config';
|
|
||||||
// import { driveConfig } from "@adonisjs/drive/build/config.js";
|
|
||||||
// import Application from '@ioc:Adonis/Core/Application';
|
|
||||||
|
|
||||||
/*
|
const driveConfig = defineConfig({
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Drive Config
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The `DriveConfig` relies on the `DisksList` interface which is
|
|
||||||
| defined inside the `contracts` directory.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
export default defineConfig({
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Default disk
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| The default disk to use for managing file uploads. The value is driven by
|
|
||||||
| the `DRIVE_DISK` environment variable.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
disk: env.get('DRIVE_DISK', 'local'),
|
|
||||||
|
|
||||||
disks: {
|
default: 'public',
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Local
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Uses the local file system to manage files. Make sure to turn off serving
|
|
||||||
| files when not using this disk.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
local: {
|
|
||||||
driver: 'local',
|
|
||||||
visibility: 'public',
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Storage root - Local driver only
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Define an absolute path to the storage directory from where to read the
|
|
||||||
| files.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
// root: Application.tmpPath('uploads'),
|
|
||||||
root: '/storage/app/public',
|
|
||||||
|
|
||||||
/*
|
services: {
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Serve files - Local driver only
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When this is set to true, AdonisJS will configure a files server to serve
|
|
||||||
| files from the disk root. This is done to mimic the behavior of cloud
|
|
||||||
| storage services that has inbuilt capabilities to serve files.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
serveFiles: true,
|
|
||||||
|
|
||||||
/*
|
/**
|
||||||
|--------------------------------------------------------------------------
|
* Persist files on the local filesystem
|
||||||
| Base path - Local driver only
|
*/
|
||||||
|--------------------------------------------------------------------------
|
public: services.fs({
|
||||||
|
|
location: '/storage/app/public/',
|
||||||
| Base path is always required when "serveFiles = true". Also make sure
|
serveFiles: true,
|
||||||
| the `basePath` is unique across all the disks using "local" driver and
|
routeBasePath: '/public',
|
||||||
| you are not registering routes with this prefix.
|
visibility: 'public',
|
||||||
|
|
}),
|
||||||
*/
|
local: services.fs({
|
||||||
basePath: '/uploads',
|
location: '/storage/app/data/',
|
||||||
},
|
serveFiles: true,
|
||||||
|
routeBasePath: '/data',
|
||||||
|
visibility: 'public',
|
||||||
|
}),
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| S3 Driver
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| Uses the S3 cloud storage to manage files. Make sure to install the s3
|
|
||||||
| drive separately when using it.
|
|
||||||
|
|
|
||||||
|**************************************************************************
|
|
||||||
| npm i @adonisjs/drive-s3
|
|
||||||
|**************************************************************************
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
// s3: {
|
|
||||||
// driver: 's3',
|
|
||||||
// visibility: 'public',
|
|
||||||
// key: Env.get('S3_KEY'),
|
|
||||||
// secret: Env.get('S3_SECRET'),
|
|
||||||
// region: Env.get('S3_REGION'),
|
|
||||||
// bucket: Env.get('S3_BUCKET'),
|
|
||||||
// endpoint: Env.get('S3_ENDPOINT'),
|
|
||||||
//
|
|
||||||
// // For minio to work
|
|
||||||
// // forcePathStyle: true,
|
|
||||||
// },
|
|
||||||
|
|
||||||
/*
|
/**
|
||||||
|--------------------------------------------------------------------------
|
* Persist files on Digital Ocean spaces
|
||||||
| GCS Driver
|
*/
|
||||||
|--------------------------------------------------------------------------
|
// spaces: services.s3({
|
||||||
|
|
// credentials: {
|
||||||
| Uses the Google cloud storage to manage files. Make sure to install the GCS
|
// accessKeyId: env.get('SPACES_KEY'),
|
||||||
| drive separately when using it.
|
// secretAccessKey: env.get('SPACES_SECRET'),
|
||||||
|
|
// },
|
||||||
|**************************************************************************
|
// region: env.get('SPACES_REGION'),
|
||||||
| npm i @adonisjs/drive-gcs
|
// bucket: env.get('SPACES_BUCKET'),
|
||||||
|**************************************************************************
|
// endpoint: env.get('SPACES_ENDPOINT'),
|
||||||
|
|
// visibility: 'public',
|
||||||
*/
|
// }),
|
||||||
// gcs: {
|
|
||||||
// driver: 'gcs',
|
|
||||||
// visibility: 'public',
|
|
||||||
// keyFilename: Env.get('GCS_KEY_FILENAME'),
|
|
||||||
// bucket: Env.get('GCS_BUCKET'),
|
|
||||||
|
|
||||||
/*
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
| Uniform ACL - Google cloud storage only
|
|
||||||
|--------------------------------------------------------------------------
|
|
||||||
|
|
|
||||||
| When using the Uniform ACL on the bucket, the "visibility" option is
|
|
||||||
| ignored. Since, the files ACL is managed by the google bucket policies
|
|
||||||
| directly.
|
|
||||||
|
|
|
||||||
|**************************************************************************
|
|
||||||
| Learn more: https://cloud.google.com/storage/docs/uniform-bucket-level-access
|
|
||||||
|**************************************************************************
|
|
||||||
|
|
|
||||||
| The following option just informs drive whether your bucket is using uniform
|
|
||||||
| ACL or not. The actual setting needs to be toggled within the Google cloud
|
|
||||||
| console.
|
|
||||||
|
|
|
||||||
*/
|
|
||||||
// usingUniformAcl: false,
|
|
||||||
// },
|
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
|
export default driveConfig
|
233
config/drive_self.ts
Normal file
|
@ -0,0 +1,233 @@
|
||||||
|
/**
|
||||||
|
* Config source: https://git.io/JBt3o
|
||||||
|
*
|
||||||
|
* Feel free to let us know via PR, if you find something broken in this config
|
||||||
|
* file.
|
||||||
|
*/
|
||||||
|
import { defineConfig } from '#providers/drive/src/types/define_config';
|
||||||
|
import env from '#start/env';
|
||||||
|
// import { driveConfig } from '@adonisjs/core/build/config';
|
||||||
|
// import { driveConfig } from "@adonisjs/drive/build/config.js";
|
||||||
|
// import Application from '@ioc:Adonis/Core/Application';
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Drive Config
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The `DriveConfig` relies on the `DisksList` interface which is
|
||||||
|
| defined inside the `contracts` directory.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
export default defineConfig({
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Default disk
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The default disk to use for managing file uploads. The value is driven by
|
||||||
|
| the `DRIVE_DISK` environment variable.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
disk: env.get('DRIVE_DISK', 'local'),
|
||||||
|
|
||||||
|
disks: {
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Local
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Uses the local file system to manage files. Make sure to turn off serving
|
||||||
|
| files when not using this disk.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
local: {
|
||||||
|
driver: 'local',
|
||||||
|
visibility: 'public',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Storage root - Local driver only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Define an absolute path to the storage directory from where to read the
|
||||||
|
| files.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
// root: Application.tmpPath('uploads'),
|
||||||
|
root: '/storage/app/data',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Serve files - Local driver only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When this is set to true, AdonisJS will configure a files server to serve
|
||||||
|
| files from the disk root. This is done to mimic the behavior of cloud
|
||||||
|
| storage services that has inbuilt capabilities to serve files.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
serveFiles: true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Base path - Local driver only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Base path is always required when "serveFiles = true". Also make sure
|
||||||
|
| the `basePath` is unique across all the disks using "local" driver and
|
||||||
|
| you are not registering routes with this prefix.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
basePath: '/files',
|
||||||
|
},
|
||||||
|
|
||||||
|
local: {
|
||||||
|
driver: 'local',
|
||||||
|
visibility: 'public',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Storage root - Local driver only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Define an absolute path to the storage directory from where to read the
|
||||||
|
| files.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
// root: Application.tmpPath('uploads'),
|
||||||
|
root: '/storage/app/data',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Serve files - Local driver only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When this is set to true, AdonisJS will configure a files server to serve
|
||||||
|
| files from the disk root. This is done to mimic the behavior of cloud
|
||||||
|
| storage services that has inbuilt capabilities to serve files.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
serveFiles: true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Base path - Local driver only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Base path is always required when "serveFiles = true". Also make sure
|
||||||
|
| the `basePath` is unique across all the disks using "local" driver and
|
||||||
|
| you are not registering routes with this prefix.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
basePath: '/files',
|
||||||
|
},
|
||||||
|
|
||||||
|
fs: {
|
||||||
|
driver: 'local',
|
||||||
|
visibility: 'public',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Storage root - Local driver only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Define an absolute path to the storage directory from where to read the
|
||||||
|
| files.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
// root: Application.tmpPath('uploads'),
|
||||||
|
root: '/storage/app/public',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Serve files - Local driver only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When this is set to true, AdonisJS will configure a files server to serve
|
||||||
|
| files from the disk root. This is done to mimic the behavior of cloud
|
||||||
|
| storage services that has inbuilt capabilities to serve files.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
serveFiles: true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Base path - Local driver only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Base path is always required when "serveFiles = true". Also make sure
|
||||||
|
| the `basePath` is unique across all the disks using "local" driver and
|
||||||
|
| you are not registering routes with this prefix.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
basePath: '/public',
|
||||||
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| S3 Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Uses the S3 cloud storage to manage files. Make sure to install the s3
|
||||||
|
| drive separately when using it.
|
||||||
|
|
|
||||||
|
|**************************************************************************
|
||||||
|
| npm i @adonisjs/drive-s3
|
||||||
|
|**************************************************************************
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
// s3: {
|
||||||
|
// driver: 's3',
|
||||||
|
// visibility: 'public',
|
||||||
|
// key: Env.get('S3_KEY'),
|
||||||
|
// secret: Env.get('S3_SECRET'),
|
||||||
|
// region: Env.get('S3_REGION'),
|
||||||
|
// bucket: Env.get('S3_BUCKET'),
|
||||||
|
// endpoint: Env.get('S3_ENDPOINT'),
|
||||||
|
//
|
||||||
|
// // For minio to work
|
||||||
|
// // forcePathStyle: true,
|
||||||
|
// },
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| GCS Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Uses the Google cloud storage to manage files. Make sure to install the GCS
|
||||||
|
| drive separately when using it.
|
||||||
|
|
|
||||||
|
|**************************************************************************
|
||||||
|
| npm i @adonisjs/drive-gcs
|
||||||
|
|**************************************************************************
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
// gcs: {
|
||||||
|
// driver: 'gcs',
|
||||||
|
// visibility: 'public',
|
||||||
|
// keyFilename: Env.get('GCS_KEY_FILENAME'),
|
||||||
|
// bucket: Env.get('GCS_BUCKET'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Uniform ACL - Google cloud storage only
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When using the Uniform ACL on the bucket, the "visibility" option is
|
||||||
|
| ignored. Since, the files ACL is managed by the google bucket policies
|
||||||
|
| directly.
|
||||||
|
|
|
||||||
|
|**************************************************************************
|
||||||
|
| Learn more: https://cloud.google.com/storage/docs/uniform-bucket-level-access
|
||||||
|
|**************************************************************************
|
||||||
|
|
|
||||||
|
| The following option just informs drive whether your bucket is using uniform
|
||||||
|
| ACL or not. The actual setting needs to be toggled within the Google cloud
|
||||||
|
| console.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
// usingUniformAcl: false,
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
});
|
|
@ -1,7 +1,8 @@
|
||||||
import { defineConfig } from '@adonisjs/inertia';
|
import { defineConfig } from '@adonisjs/inertia';
|
||||||
import type { HttpContext } from '@adonisjs/core/http';
|
import type { HttpContext } from '@adonisjs/core/http';
|
||||||
|
import type { InferSharedProps } from '@adonisjs/inertia/types'
|
||||||
|
|
||||||
export default defineConfig({
|
const inertiaConfig = defineConfig({
|
||||||
/**
|
/**
|
||||||
* Path to the Edge view that will be used as the root view for Inertia responses
|
* Path to the Edge view that will be used as the root view for Inertia responses
|
||||||
*/
|
*/
|
||||||
|
@ -52,6 +53,12 @@ export default defineConfig({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export default inertiaConfig
|
||||||
|
|
||||||
|
declare module '@adonisjs/inertia/types' {
|
||||||
|
export interface SharedProps extends InferSharedProps<typeof inertiaConfig> {}
|
||||||
|
}
|
||||||
|
|
||||||
// import { InertiaConfig } from '@ioc:EidelLev/Inertia';
|
// import { InertiaConfig } from '@ioc:EidelLev/Inertia';
|
||||||
|
|
||||||
// /*
|
// /*
|
||||||
|
|
|
@ -12,7 +12,7 @@ const mailConfig = defineConfig({
|
||||||
mailers: {
|
mailers: {
|
||||||
|
|
||||||
smtp: transports.smtp({
|
smtp: transports.smtp({
|
||||||
socketTimeout: 5000,// Overall timeout (5 seconds)
|
// socketTimeout: 5000,// Overall timeout (5 seconds)
|
||||||
host: env.get('SMTP_HOST', ''),
|
host: env.get('SMTP_HOST', ''),
|
||||||
port: env.get('SMTP_PORT'),
|
port: env.get('SMTP_PORT'),
|
||||||
secure: false,
|
secure: false,
|
||||||
|
@ -30,10 +30,10 @@ const mailConfig = defineConfig({
|
||||||
}, */
|
}, */
|
||||||
}),
|
}),
|
||||||
|
|
||||||
resend: transports.resend({
|
// resend: transports.resend({
|
||||||
key: env.get('RESEND_API_KEY'),
|
// key: env.get('RESEND_API_KEY'),
|
||||||
baseUrl: 'https://api.resend.com',
|
// baseUrl: 'https://api.resend.com',
|
||||||
}),
|
// }),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import env from '#start/env';
|
import env from '#start/env';
|
||||||
import app from '@adonisjs/core/services/app';
|
// import app from '@adonisjs/core/services/app';
|
||||||
import { defineConfig, stores } from '@adonisjs/session';
|
import { defineConfig, stores } from '@adonisjs/session';
|
||||||
|
|
||||||
const sessionConfig = defineConfig({
|
const sessionConfig = defineConfig({
|
||||||
|
|
32
config/vite.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import { defineConfig } from '@adonisjs/vite';
|
||||||
|
|
||||||
|
const viteBackendConfig = defineConfig({
|
||||||
|
/**
|
||||||
|
* The output of vite will be written inside this
|
||||||
|
* directory. The path should be relative from
|
||||||
|
* the application root.
|
||||||
|
*/
|
||||||
|
buildDirectory: 'public/assets',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The path to the manifest file generated by the
|
||||||
|
* "vite build" command.
|
||||||
|
*/
|
||||||
|
manifestFile: 'public/assets/.vite/manifest.json',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feel free to change the value of the "assetsUrl" to
|
||||||
|
* point to a CDN in production.
|
||||||
|
*/
|
||||||
|
assetsUrl: '/assets',
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add defer attribute to scripts for better performance.
|
||||||
|
*/
|
||||||
|
scriptAttributes: {
|
||||||
|
defer: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
export default viteBackendConfig;
|
|
@ -21,6 +21,7 @@ export enum ServerStates {
|
||||||
rejected_reviewer = 'rejected_reviewer',
|
rejected_reviewer = 'rejected_reviewer',
|
||||||
rejected_editor = 'rejected_editor',
|
rejected_editor = 'rejected_editor',
|
||||||
reviewed = 'reviewed',
|
reviewed = 'reviewed',
|
||||||
|
rejected_to_reviewer = 'rejected_to_reviewer',
|
||||||
}
|
}
|
||||||
|
|
||||||
// for table dataset_titles
|
// for table dataset_titles
|
||||||
|
|
|
@ -18,6 +18,7 @@ export default class Accounts extends BaseSchema {
|
||||||
table.text("two_factor_recovery_codes").nullable();
|
table.text("two_factor_recovery_codes").nullable();
|
||||||
table.smallint('state').nullable();
|
table.smallint('state').nullable();
|
||||||
table.bigint('last_counter').nullable();
|
table.bigint('last_counter').nullable();
|
||||||
|
table.string('avatar').nullable();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +44,7 @@ export default class Accounts extends BaseSchema {
|
||||||
// two_factor_recovery_codes text COLLATE pg_catalog."default",
|
// two_factor_recovery_codes text COLLATE pg_catalog."default",
|
||||||
// state smallint,
|
// state smallint,
|
||||||
// last_counter bigint,
|
// last_counter bigint,
|
||||||
|
// avatar character varying(255),
|
||||||
// )
|
// )
|
||||||
|
|
||||||
// ALTER TABLE gba.accounts
|
// ALTER TABLE gba.accounts
|
||||||
|
@ -85,3 +87,6 @@ export default class Accounts extends BaseSchema {
|
||||||
// GRANT ALL ON SEQUENCE gba.totp_secrets_id_seq TO tethys_admin;
|
// GRANT ALL ON SEQUENCE gba.totp_secrets_id_seq TO tethys_admin;
|
||||||
|
|
||||||
// ALTER TABLE gba.totp_secrets ALTER COLUMN id SET DEFAULT nextval('gba.totp_secrets_id_seq');
|
// ALTER TABLE gba.totp_secrets ALTER COLUMN id SET DEFAULT nextval('gba.totp_secrets_id_seq');
|
||||||
|
|
||||||
|
|
||||||
|
// ALTER TABLE "accounts" ADD COLUMN "avatar" VARCHAR(255) NULL
|
||||||
|
|
|
@ -86,3 +86,22 @@ export default class Documents extends BaseSchema {
|
||||||
// CONSTRAINT documents_server_state_check CHECK (server_state::text = ANY (ARRAY['deleted'::character varying::text, 'inprogress'::character varying::text, 'published'::character varying::text, 'released'::character varying::text, 'editor_accepted'::character varying::text, 'approved'::character varying::text, 'rejected_reviewer'::character varying::text, 'rejected_editor'::character varying::text, 'reviewed'::character varying::text])),
|
// CONSTRAINT documents_server_state_check CHECK (server_state::text = ANY (ARRAY['deleted'::character varying::text, 'inprogress'::character varying::text, 'published'::character varying::text, 'released'::character varying::text, 'editor_accepted'::character varying::text, 'approved'::character varying::text, 'rejected_reviewer'::character varying::text, 'rejected_editor'::character varying::text, 'reviewed'::character varying::text])),
|
||||||
// CONSTRAINT documents_type_check CHECK (type::text = ANY (ARRAY['analysisdata'::character varying::text, 'measurementdata'::character varying::text, 'monitoring'::character varying::text, 'remotesensing'::character varying::text, 'gis'::character varying::text, 'models'::character varying::text, 'mixedtype'::character varying::text]))
|
// CONSTRAINT documents_type_check CHECK (type::text = ANY (ARRAY['analysisdata'::character varying::text, 'measurementdata'::character varying::text, 'monitoring'::character varying::text, 'remotesensing'::character varying::text, 'gis'::character varying::text, 'models'::character varying::text, 'mixedtype'::character varying::text]))
|
||||||
// )
|
// )
|
||||||
|
|
||||||
|
|
||||||
|
// ALTER TABLE documents DROP CONSTRAINT documents_server_state_check;
|
||||||
|
|
||||||
|
// ALTER TABLE documents
|
||||||
|
// ADD CONSTRAINT documents_server_state_check CHECK (
|
||||||
|
// server_state::text = ANY (ARRAY[
|
||||||
|
// 'deleted',
|
||||||
|
// 'inprogress',
|
||||||
|
// 'published',
|
||||||
|
// 'released',
|
||||||
|
// 'editor_accepted',
|
||||||
|
// 'approved',
|
||||||
|
// 'rejected_reviewer',
|
||||||
|
// 'rejected_editor',
|
||||||
|
// 'reviewed',
|
||||||
|
// 'rejected_to_reviewer' -- new value added
|
||||||
|
// ]::text[])
|
||||||
|
// );
|
|
@ -5,7 +5,7 @@ export default class Collections extends BaseSchema {
|
||||||
|
|
||||||
public async up() {
|
public async up() {
|
||||||
this.schema.createTable(this.tableName, (table) => {
|
this.schema.createTable(this.tableName, (table) => {
|
||||||
table.increments('id').defaultTo("nextval('collections_id_seq')");
|
table.increments('id');//.defaultTo("nextval('collections_id_seq')");
|
||||||
table.integer('role_id').unsigned();
|
table.integer('role_id').unsigned();
|
||||||
table
|
table
|
||||||
.foreign('role_id', 'collections_role_id_foreign')
|
.foreign('role_id', 'collections_role_id_foreign')
|
||||||
|
@ -54,3 +54,8 @@ export default class Collections extends BaseSchema {
|
||||||
// ON UPDATE CASCADE
|
// ON UPDATE CASCADE
|
||||||
// ON DELETE CASCADE
|
// ON DELETE CASCADE
|
||||||
// )
|
// )
|
||||||
|
|
||||||
|
|
||||||
|
// change to normal intzeger:
|
||||||
|
// ALTER TABLE collections ALTER COLUMN id DROP DEFAULT;
|
||||||
|
// DROP SEQUENCE IF EXISTS collections_id_seq;
|
||||||
|
|
18
database/migrations/update_1_to_mime_types.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { BaseSchema } from "@adonisjs/lucid/schema";
|
||||||
|
|
||||||
|
export default class AddAlternateMimetypeToMimeTypes extends BaseSchema {
|
||||||
|
protected tableName = 'mime_types';
|
||||||
|
|
||||||
|
public async up () {
|
||||||
|
this.schema.alterTable(this.tableName, (table) => {
|
||||||
|
table.string('alternate_mimetype').nullable();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async down () {
|
||||||
|
this.schema.alterTable(this.tableName, (table) => {
|
||||||
|
table.dropColumn('alternate_mimetype');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// ALTER TABLE "mime_types" ADD COLUMN "alternate_mimetype" VARCHAR(255) NULL
|
6
index.d.ts
vendored
|
@ -183,3 +183,9 @@ declare module 'saxon-js' {
|
||||||
|
|
||||||
export function transform(options: ITransformOptions): Promise<ITransformOutput> | ITransformOutput;
|
export function transform(options: ITransformOptions): Promise<ITransformOutput> | ITransformOutput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface File {
|
||||||
|
sort_order?: number;
|
||||||
|
}
|
||||||
|
}
|
10813
package-lock.json
generated
84
package.json
|
@ -4,7 +4,8 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"type-check": "tsc --noEmit",
|
"type-check": "tsc --noEmit",
|
||||||
"dev": "node ace serve --watch",
|
"dev": "node ace serve",
|
||||||
|
"devInspect": "node ace serve --watch --node-args='--inspect'",
|
||||||
"compress:xslt": "./node_modules/xslt3/xslt3.js -xsl:public/assets2/datasetxml2oai-pmh.xslt -export:public/assets2/datasetxml2oai.sef.json -t -nogo '-ns:##html5'",
|
"compress:xslt": "./node_modules/xslt3/xslt3.js -xsl:public/assets2/datasetxml2oai-pmh.xslt -export:public/assets2/datasetxml2oai.sef.json -t -nogo '-ns:##html5'",
|
||||||
"compress:solr": "./node_modules/xslt3/xslt3.js -xsl:public/assets2/solr.xslt -export:public/assets2/solr.sef.json -t -nogo '-ns:##html5'",
|
"compress:solr": "./node_modules/xslt3/xslt3.js -xsl:public/assets2/solr.xslt -export:public/assets2/solr.sef.json -t -nogo '-ns:##html5'",
|
||||||
"compress:doi": "./node_modules/xslt3/xslt3.js -xsl:public/assets2/doi_datacite.xslt -export:public/assets2/doi_datacite.sef.json -t -nogo '-ns:##html5'",
|
"compress:doi": "./node_modules/xslt3/xslt3.js -xsl:public/assets2/doi_datacite.xslt -export:public/assets2/doi_datacite.sef.json -t -nogo '-ns:##html5'",
|
||||||
|
@ -15,59 +16,58 @@
|
||||||
"format-check": "prettier --check ./**/*.{ts,js}",
|
"format-check": "prettier --check ./**/*.{ts,js}",
|
||||||
"test": "node ace test"
|
"test": "node ace test"
|
||||||
},
|
},
|
||||||
"eslintIgnore": [
|
"eslintConfig": {
|
||||||
"build"
|
"ignorePatterns": [
|
||||||
],
|
"build"
|
||||||
|
]
|
||||||
|
},
|
||||||
"alias": {
|
"alias": {
|
||||||
"vue": "./node_modules/vue/dist/vue.esm-bundler.js"
|
"vue": "./node_modules/vue/dist/vue.esm-bundler.js"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@adonisjs/assembler": "^7.1.1",
|
"@adonisjs/assembler": "^7.1.1",
|
||||||
"@adonisjs/tsconfig": "^1.2.1",
|
"@adonisjs/tsconfig": "^1.4.0",
|
||||||
"@babel/core": "^7.20.12",
|
"@headlessui/vue": "^1.7.23",
|
||||||
"@babel/plugin-proposal-class-properties": "^7.18.6",
|
"@japa/assert": "^4.0.1",
|
||||||
"@babel/plugin-proposal-decorators": "^7.20.13",
|
"@japa/plugin-adonisjs": "^4.0.0",
|
||||||
"@babel/plugin-transform-runtime": "^7.19.6",
|
"@japa/runner": "^4.2.0",
|
||||||
"@babel/preset-env": "^7.20.2",
|
|
||||||
"@babel/preset-typescript": "^7.18.6",
|
|
||||||
"@japa/api-client": "^2.0.3",
|
|
||||||
"@japa/assert": "^3.0.0",
|
|
||||||
"@japa/plugin-adonisjs": "^3.0.0",
|
|
||||||
"@japa/runner": "^3.1.1",
|
|
||||||
"@mdi/js": "^7.1.96",
|
"@mdi/js": "^7.1.96",
|
||||||
"@poppinss/utils": "^6.7.2",
|
"@poppinss/utils": "^6.7.2",
|
||||||
"@swc/core": "^1.4.2",
|
"@swc/wasm": "^1.10.14",
|
||||||
"@symfony/webpack-encore": "^5.0.1",
|
|
||||||
"@tailwindcss/forms": "^0.5.2",
|
"@tailwindcss/forms": "^0.5.2",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/clamscan": "^2.0.4",
|
"@types/clamscan": "^2.0.4",
|
||||||
"@types/escape-html": "^1.0.4",
|
"@types/escape-html": "^1.0.4",
|
||||||
"@types/leaflet": "^1.9.3",
|
"@types/fs-extra": "^11.0.4",
|
||||||
|
"@types/leaflet": "^1.9.16",
|
||||||
"@types/luxon": "^3.4.2",
|
"@types/luxon": "^3.4.2",
|
||||||
"@types/node": "^22.5.5",
|
"@types/node": "^22.10.2",
|
||||||
"@types/proxy-addr": "^2.0.0",
|
"@types/proxy-addr": "^2.0.0",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/source-map-support": "^0.5.6",
|
"@types/source-map-support": "^0.5.6",
|
||||||
"@types/sprintf-js": "^1.1.4",
|
"@types/sprintf-js": "^1.1.4",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"babel-preset-typescript-vue3": "^2.0.17",
|
"babel-preset-typescript-vue3": "^2.0.17",
|
||||||
"chart.js": "^4.2.0",
|
"chart.js": "^4.2.0",
|
||||||
"dotenv-webpack": "^8.0.1",
|
"dotenv-webpack": "^8.0.1",
|
||||||
"eslint": "^8.57.1",
|
"eslint": "^8.57.1",
|
||||||
"eslint-config-prettier": "^9.0.0",
|
"eslint-config-prettier": "^10.0.1",
|
||||||
"eslint-plugin-adonis": "^2.1.1",
|
"eslint-plugin-adonis": "^2.1.1",
|
||||||
"eslint-plugin-prettier": "^5.0.0-alpha.2",
|
"eslint-plugin-prettier": "^5.0.0-alpha.2",
|
||||||
|
"hot-hook": "^0.4.0",
|
||||||
"numeral": "^2.0.6",
|
"numeral": "^2.0.6",
|
||||||
"pinia": "^2.0.30",
|
"pinia": "^3.0.2",
|
||||||
"pino-pretty": "^11.2.2",
|
"pino-pretty": "^13.0.0",
|
||||||
"postcss-loader": "^8.1.1",
|
"postcss-loader": "^8.1.1",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.4.2",
|
||||||
"supertest": "^6.3.3",
|
"supertest": "^6.3.3",
|
||||||
"tailwindcss": "^3.2.4",
|
"tailwindcss": "^3.4.17",
|
||||||
"ts-loader": "^9.4.2",
|
"ts-loader": "^9.4.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node-maintained": "^10.9.5",
|
||||||
"typescript": "^5.1.3",
|
"typescript": "~5.7",
|
||||||
|
"vite": "^6.0.11",
|
||||||
"vue": "^3.4.26",
|
"vue": "^3.4.26",
|
||||||
"vue-facing-decorator": "^3.0.0",
|
"vue-facing-decorator": "^3.0.0",
|
||||||
"vue-loader": "^17.0.1",
|
"vue-loader": "^17.0.1",
|
||||||
|
@ -75,30 +75,32 @@
|
||||||
"xslt3": "^2.5.0"
|
"xslt3": "^2.5.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@adonisjs/auth": "^9.1.1",
|
"@adonisjs/auth": "^9.2.4",
|
||||||
"@adonisjs/core": "^6.3.1",
|
"@adonisjs/bodyparser": "^10.0.1",
|
||||||
|
"@adonisjs/core": "^6.17.0",
|
||||||
"@adonisjs/cors": "^2.2.1",
|
"@adonisjs/cors": "^2.2.1",
|
||||||
"@adonisjs/drive": "^2.3.0",
|
"@adonisjs/drive": "^3.2.0",
|
||||||
"@adonisjs/encore": "^1.0.0",
|
"@adonisjs/inertia": "^2.1.3",
|
||||||
"@adonisjs/inertia": "^1.0.0-7",
|
"@adonisjs/lucid": "^21.5.1",
|
||||||
"@adonisjs/lucid": "^21.1.0",
|
|
||||||
"@adonisjs/mail": "^9.2.2",
|
"@adonisjs/mail": "^9.2.2",
|
||||||
"@adonisjs/redis": "^9.1.0",
|
"@adonisjs/redis": "^9.1.0",
|
||||||
"@adonisjs/session": "^7.1.1",
|
"@adonisjs/session": "^7.5.0",
|
||||||
"@adonisjs/shield": "^8.1.1",
|
"@adonisjs/shield": "^8.1.1",
|
||||||
"@adonisjs/static": "^1.1.1",
|
"@adonisjs/static": "^1.1.1",
|
||||||
|
"@adonisjs/vite": "^4.0.0",
|
||||||
"@eidellev/adonis-stardust": "^3.0.0",
|
"@eidellev/adonis-stardust": "^3.0.0",
|
||||||
"@fontsource/archivo-black": "^5.0.1",
|
"@fontsource/archivo-black": "^5.0.1",
|
||||||
"@fontsource/inter": "^5.0.1",
|
"@fontsource/inter": "^5.0.1",
|
||||||
"@inertiajs/inertia": "^0.11.1",
|
"@inertiajs/inertia": "^0.11.1",
|
||||||
"@inertiajs/vue3": "^1.0.0",
|
"@inertiajs/vue3": "^2.0.3",
|
||||||
"@opensearch-project/opensearch": "^2.4.0",
|
"@opensearch-project/opensearch": "^3.2.0",
|
||||||
"@phc/format": "^1.0.0",
|
"@phc/format": "^1.0.0",
|
||||||
"@vinejs/vine": "^2.0.0",
|
"@poppinss/manager": "^5.0.2",
|
||||||
|
"@vinejs/vine": "^3.0.0",
|
||||||
|
"axios": "^1.7.9",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"clamscan": "^2.1.2",
|
"clamscan": "^2.1.2",
|
||||||
"crypto": "^1.0.1",
|
|
||||||
"dayjs": "^1.11.7",
|
"dayjs": "^1.11.7",
|
||||||
"deep-email-validator": "^0.1.21",
|
"deep-email-validator": "^0.1.21",
|
||||||
"edge.js": "^6.0.1",
|
"edge.js": "^6.0.1",
|
||||||
|
@ -114,13 +116,19 @@
|
||||||
"notiwind": "^2.0.0",
|
"notiwind": "^2.0.0",
|
||||||
"pg": "^8.9.0",
|
"pg": "^8.9.0",
|
||||||
"qrcode": "^1.5.3",
|
"qrcode": "^1.5.3",
|
||||||
"redis": "^4.6.10",
|
"redis": "^5.0.0",
|
||||||
"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",
|
||||||
"vuedraggable": "^4.1.0",
|
"vuedraggable": "^4.1.0",
|
||||||
"xmlbuilder2": "^3.1.1"
|
"xmlbuilder2": "^3.1.1"
|
||||||
},
|
},
|
||||||
|
"hotHook": {
|
||||||
|
"boundaries": [
|
||||||
|
"./app/Controllers/**/*.ts",
|
||||||
|
"./app/middleware/*.ts"
|
||||||
|
]
|
||||||
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"imports": {
|
"imports": {
|
||||||
"#controllers/*": "./app/Controllers/*.js",
|
"#controllers/*": "./app/Controllers/*.js",
|
||||||
|
|
|
@ -1,7 +1,10 @@
|
||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: {
|
plugins: {
|
||||||
// 'postcss-import': {},
|
// 'postcss-import': {},
|
||||||
'tailwindcss/nesting': {},
|
// 'postcss-nesting': {},
|
||||||
|
'tailwindcss/nesting': {},
|
||||||
|
// "@tailwindcss/postcss": {},
|
||||||
|
// tailwindcss: {},
|
||||||
tailwindcss: {},
|
tailwindcss: {},
|
||||||
autoprefixer: {},
|
autoprefixer: {},
|
||||||
},
|
},
|
||||||
|
|
|
@ -74,7 +74,8 @@ export class LocalDriver implements LocalDriverContract {
|
||||||
*/
|
*/
|
||||||
public async exists(location: string): Promise<boolean> {
|
public async exists(location: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
return await this.adapter.pathExists(this.makePath(location));
|
let path_temp = this.makePath(location); //'/storage/app/files/421'
|
||||||
|
return await this.adapter.pathExists(path_temp);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw CannotGetMetaDataException.invoke(location, 'exists', error);
|
throw CannotGetMetaDataException.invoke(location, 'exists', error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,7 +69,7 @@ export default class MailProvider {
|
||||||
const mailConfigProvider = this.app.config.get('mail');
|
const mailConfigProvider = this.app.config.get('mail');
|
||||||
const config = await configProvider.resolve<any>(this.app, mailConfigProvider);
|
const config = await configProvider.resolve<any>(this.app, mailConfigProvider);
|
||||||
|
|
||||||
const iwas = await config.mailers.smtp();
|
await config.mailers.smtp();
|
||||||
// iwas.config.host = 'hhhost';
|
// iwas.config.host = 'hhhost';
|
||||||
// this.app.config.set('mail.mailers.smtp.host', 'xhost');
|
// this.app.config.set('mail.mailers.smtp.host', 'xhost');
|
||||||
// const iwas = await config.mailers.smtp();
|
// const iwas = await config.mailers.smtp();
|
||||||
|
|
|
@ -63,6 +63,15 @@ export default class QueryBuilderProvider {
|
||||||
|
|
||||||
public register() {
|
public register() {
|
||||||
// Register your own bindings
|
// Register your own bindings
|
||||||
|
// const ModelQueryBuilder = this.app.container.bind('@adonisjs/lucid/orm/ModelQueryBuilder');
|
||||||
|
|
||||||
|
// ModelQueryBuilder.macro('whereTrue', function (columnName: string) {
|
||||||
|
// return this.where(columnName, true);
|
||||||
|
// });
|
||||||
|
|
||||||
|
// ModelQueryBuilder.macro('whereFalse', function (columnName: string) {
|
||||||
|
// return this.where(columnName, false);
|
||||||
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
public async boot() {
|
public async boot() {
|
||||||
|
@ -73,15 +82,14 @@ export default class QueryBuilderProvider {
|
||||||
// let rolesPluck = {};
|
// let rolesPluck = {};
|
||||||
let rolesPluck: { [key: number]: any } = {};
|
let rolesPluck: { [key: number]: any } = {};
|
||||||
const result = await this.exec();
|
const result = await this.exec();
|
||||||
result.forEach((user, index) => {
|
result.forEach((user: { [key: string]: any }, index: number) => {
|
||||||
let idc;
|
let idc: number;
|
||||||
if (!id) {
|
if (!id) {
|
||||||
idc = index;
|
idc = index;
|
||||||
} else {
|
} else {
|
||||||
idc = user[id];
|
idc = user[id];
|
||||||
}
|
}
|
||||||
const value = user[valueColumn];
|
const value: any = user[valueColumn];
|
||||||
// rolesPluck[idc] = user.name;
|
|
||||||
rolesPluck[idc] = value;
|
rolesPluck[idc] = value;
|
||||||
});
|
});
|
||||||
return rolesPluck;
|
return rolesPluck;
|
||||||
|
|
|
@ -4,14 +4,15 @@
|
||||||
|--------------------------------------------------------------------------
|
|--------------------------------------------------------------------------
|
||||||
|*/
|
|*/
|
||||||
import type { ApplicationService } from '@adonisjs/core/types';
|
import type { ApplicationService } from '@adonisjs/core/types';
|
||||||
import vine, { BaseLiteralType, Vine } from '@vinejs/vine';
|
import vine, { symbols, BaseLiteralType, Vine } from '@vinejs/vine';
|
||||||
import type { Validation, FieldContext, FieldOptions } from '@vinejs/vine/types';
|
import type { FieldContext, FieldOptions } from '@vinejs/vine/types';
|
||||||
// import type { MultipartFile, FileValidationOptions } from '@adonisjs/bodyparser/types';
|
// import type { MultipartFile, FileValidationOptions } from '@adonisjs/bodyparser/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';
|
import MimeType from '#models/mime_type';
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validation options accepted by the "file" rule
|
* Validation options accepted by the "file" rule
|
||||||
*/
|
*/
|
||||||
|
@ -28,8 +29,7 @@ declare module '@vinejs/vine' {
|
||||||
* Extend HTTP request class
|
* Extend HTTP request class
|
||||||
*/
|
*/
|
||||||
declare module '@adonisjs/core/http' {
|
declare module '@adonisjs/core/http' {
|
||||||
interface Request extends RequestValidator {
|
interface Request extends RequestValidator {}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,7 +48,7 @@ export async function getEnabledExtensions() {
|
||||||
.flat();
|
.flat();
|
||||||
|
|
||||||
return extensions;
|
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.
|
||||||
|
@ -79,11 +79,13 @@ const isMultipartFile = vine.createRule(async (file: MultipartFile | unknown, op
|
||||||
// if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) {
|
// if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) {
|
||||||
// validatedFile.allowedExtensions = validationOptions.extnames;
|
// validatedFile.allowedExtensions = validationOptions.extnames;
|
||||||
// }
|
// }
|
||||||
if (validatedFile.allowedExtensions === undefined && validationOptions.extnames) {
|
if (validatedFile.allowedExtensions === undefined && validationOptions.extnames !== undefined) {
|
||||||
|
validatedFile.allowedExtensions = validationOptions.extnames; // await getEnabledExtensions();
|
||||||
|
} else if (validatedFile.allowedExtensions === undefined && validationOptions.extnames === undefined) {
|
||||||
validatedFile.allowedExtensions = await getEnabledExtensions();
|
validatedFile.allowedExtensions = await getEnabledExtensions();
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* wieder löschen
|
* wieder löschen
|
||||||
* 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
|
||||||
*/
|
*/
|
||||||
|
@ -102,7 +104,20 @@ const isMultipartFile = vine.createRule(async (file: MultipartFile | unknown, op
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const MULTIPART_FILE: typeof symbols.SUBTYPE = symbols.SUBTYPE;
|
||||||
|
|
||||||
export class VineMultipartFile extends BaseLiteralType<MultipartFile, MultipartFile, MultipartFile> {
|
export class VineMultipartFile extends BaseLiteralType<MultipartFile, MultipartFile, MultipartFile> {
|
||||||
|
|
||||||
|
[MULTIPART_FILE]: string;
|
||||||
|
// constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions) {
|
||||||
|
// super(options, [isMultipartFile(validationOptions || {})]);
|
||||||
|
// this.validationOptions = validationOptions;
|
||||||
|
// this.#private = true;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// clone(): this {
|
||||||
|
// return new VineMultipartFile(this.validationOptions, this.cloneOptions()) as this;
|
||||||
|
// }
|
||||||
// #private;
|
// #private;
|
||||||
// constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions, validations?: Validation<any>[]);
|
// constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions, validations?: Validation<any>[]);
|
||||||
// clone(): this;
|
// clone(): this;
|
||||||
|
@ -111,14 +126,16 @@ export class VineMultipartFile extends BaseLiteralType<MultipartFile, MultipartF
|
||||||
// extnames: (18) ['gpkg', 'htm', 'html', 'csv', 'txt', 'asc', 'c', 'cc', 'h', 'srt', 'tiff', 'pdf', 'png', 'zip', 'jpg', 'jpeg', 'jpe', 'xlsx']
|
// extnames: (18) ['gpkg', 'htm', 'html', 'csv', 'txt', 'asc', 'c', 'cc', 'h', 'srt', 'tiff', 'pdf', 'png', 'zip', 'jpg', 'jpeg', 'jpe', 'xlsx']
|
||||||
// size: '512mb'
|
// size: '512mb'
|
||||||
|
|
||||||
public constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions, validations?: Validation<any>[]) {
|
// public constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions, validations?: Validation<any>[]) {
|
||||||
|
public constructor(validationOptions?: FileRuleValidationOptions, options?: FieldOptions) {
|
||||||
// super(options, validations);
|
// super(options, validations);
|
||||||
super(options, [isMultipartFile(validationOptions || {})]);
|
super(options, [isMultipartFile(validationOptions || {})]);
|
||||||
this.validationOptions = validationOptions;
|
this.validationOptions = validationOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public clone(): any {
|
public clone(): any {
|
||||||
return new VineMultipartFile(this.validationOptions, this.cloneOptions(), this.cloneValidations());
|
// return new VineMultipartFile(this.validationOptions, this.cloneOptions(), this.cloneValidations());
|
||||||
|
return new VineMultipartFile(this.validationOptions, this.cloneOptions());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,10 +169,12 @@ export default class VinejsProvider {
|
||||||
* The validate method can be used to validate the request
|
* The validate method can be used to validate the request
|
||||||
* data for the current request using VineJS validators
|
* data for the current request using VineJS validators
|
||||||
*/
|
*/
|
||||||
Request.macro('validateUsing', function (...args) {
|
Request.macro('validateUsing', function (this: Request, ...args) {
|
||||||
return new RequestValidator(this.ctx).validateUsing(...args);
|
if (!this.ctx) {
|
||||||
});
|
throw new Error('HttpContext is not available');
|
||||||
|
}
|
||||||
|
return new RequestValidator(this.ctx).validateUsing(...args);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
BIN
public/android-chrome-192x192.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
public/android-chrome-512x512.png
Normal file
After Width: | Height: | Size: 37 KiB |
BIN
public/apple-touch-icon.png
Normal file
After Width: | Height: | Size: 9.8 KiB |
|
@ -1,12 +0,0 @@
|
||||||
{
|
|
||||||
"entrypoints": {
|
|
||||||
"app": {
|
|
||||||
"css": [
|
|
||||||
"http://localhost:8080/assets/app.css"
|
|
||||||
],
|
|
||||||
"js": [
|
|
||||||
"http://localhost:8080/assets/app.js"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,103 +0,0 @@
|
||||||
{
|
|
||||||
"assets/app.css": "http://localhost:8080/assets/app.css",
|
|
||||||
"assets/app.js": "http://localhost:8080/assets/app.js",
|
|
||||||
"assets/resources_js_apps_settings_l18n_de_js.js": "http://localhost:8080/assets/resources_js_apps_settings_l18n_de_js.js",
|
|
||||||
"assets/resources_js_apps_settings_l18n_en_js.js": "http://localhost:8080/assets/resources_js_apps_settings_l18n_en_js.js",
|
|
||||||
"assets/resources_js_Pages_Admin_License_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_License_Index_vue.js",
|
|
||||||
"assets/resources_js_Pages_Admin_Mimetype_Create_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Mimetype_Create_vue.js",
|
|
||||||
"assets/resources_js_Pages_Admin_Mimetype_Delete_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Mimetype_Delete_vue.js",
|
|
||||||
"assets/resources_js_Pages_Admin_Mimetype_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Mimetype_Index_vue.js",
|
|
||||||
"assets/resources_js_Pages_Admin_Permission_Create_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Permission_Create_vue.js",
|
|
||||||
"assets/resources_js_Pages_Admin_Permission_Edit_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Permission_Edit_vue.js",
|
|
||||||
"assets/resources_js_Pages_Admin_Permission_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Permission_Index_vue.js",
|
|
||||||
"assets/resources_js_Pages_Admin_Permission_Show_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Permission_Show_vue.js",
|
|
||||||
"assets/resources_js_Pages_Admin_Role_Create_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Role_Create_vue.js",
|
|
||||||
"assets/resources_js_Pages_Admin_Role_Edit_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Role_Edit_vue.js",
|
|
||||||
"assets/resources_js_Pages_Admin_Role_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Role_Index_vue.js",
|
|
||||||
"assets/resources_js_Pages_Admin_Role_Show_vue.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Role_Show_vue.js",
|
|
||||||
"assets/resources_js_Pages_Admin_Settings_vue-resources_js_utils_toast_css.css": "http://localhost:8080/assets/resources_js_Pages_Admin_Settings_vue-resources_js_utils_toast_css.css",
|
|
||||||
"assets/resources_js_Pages_Admin_Settings_vue-resources_js_utils_toast_css.js": "http://localhost:8080/assets/resources_js_Pages_Admin_Settings_vue-resources_js_utils_toast_css.js",
|
|
||||||
"assets/resources_js_Pages_Admin_User_Create_vue-resources_js_Components_SimplePasswordMeter_password-f3312a.css": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Create_vue-resources_js_Components_SimplePasswordMeter_password-f3312a.css",
|
|
||||||
"assets/resources_js_Pages_Admin_User_Create_vue-resources_js_Components_SimplePasswordMeter_password-f3312a.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Create_vue-resources_js_Components_SimplePasswordMeter_password-f3312a.js",
|
|
||||||
"assets/resources_js_Pages_Admin_User_Edit_vue-resources_js_Components_SimplePasswordMeter_password-m-6dc207.css": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Edit_vue-resources_js_Components_SimplePasswordMeter_password-m-6dc207.css",
|
|
||||||
"assets/resources_js_Pages_Admin_User_Edit_vue-resources_js_Components_SimplePasswordMeter_password-m-6dc207.js": "http://localhost:8080/assets/resources_js_Pages_Admin_User_Edit_vue-resources_js_Components_SimplePasswordMeter_password-m-6dc207.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_App_vue.js": "http://localhost:8080/assets/resources_js_Pages_App_vue.js",
|
|
||||||
"assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css-resources_js_Components_-06c7b5.css": "http://localhost:8080/assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css-resources_js_Components_-06c7b5.css",
|
|
||||||
"assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css-resources_js_Components_-06c7b5.js": "http://localhost:8080/assets/resources_js_Pages_Auth_AccountInfo_vue-resources_js_utils_toast_css-resources_js_Components_-06c7b5.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_Dashboard_vue.js": "http://localhost:8080/assets/resources_js_Pages_Dashboard_vue.js",
|
|
||||||
"assets/resources_js_Pages_Editor_Dataset_Approve_vue.js": "http://localhost:8080/assets/resources_js_Pages_Editor_Dataset_Approve_vue.js",
|
|
||||||
"assets/resources_js_Pages_Editor_Dataset_Doi_vue.js": "http://localhost:8080/assets/resources_js_Pages_Editor_Dataset_Doi_vue.js",
|
|
||||||
"assets/resources_js_Pages_Editor_Dataset_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Editor_Dataset_Index_vue.js",
|
|
||||||
"assets/resources_js_Pages_Editor_Dataset_Publish_vue.js": "http://localhost:8080/assets/resources_js_Pages_Editor_Dataset_Publish_vue.js",
|
|
||||||
"assets/resources_js_Pages_Editor_Dataset_Receive_vue.js": "http://localhost:8080/assets/resources_js_Pages_Editor_Dataset_Receive_vue.js",
|
|
||||||
"assets/resources_js_Pages_Editor_Dataset_Reject_vue.js": "http://localhost:8080/assets/resources_js_Pages_Editor_Dataset_Reject_vue.js",
|
|
||||||
"assets/resources_js_Pages_Error_vue.js": "http://localhost:8080/assets/resources_js_Pages_Error_vue.js",
|
|
||||||
"assets/resources_js_Pages_Errors_ServerError_vue.js": "http://localhost:8080/assets/resources_js_Pages_Errors_ServerError_vue.js",
|
|
||||||
"assets/resources_js_Pages_Errors_not_found_vue.js": "http://localhost:8080/assets/resources_js_Pages_Errors_not_found_vue.js",
|
|
||||||
"assets/resources_js_Pages_Map_vue-resources_js_Components_Map_draw_component_vue-resources_js_Compon-b0925c.css": "http://localhost:8080/assets/resources_js_Pages_Map_vue-resources_js_Components_Map_draw_component_vue-resources_js_Compon-b0925c.css",
|
|
||||||
"assets/resources_js_Pages_Map_vue-resources_js_Components_Map_draw_component_vue-resources_js_Compon-b0925c.js": "http://localhost:8080/assets/resources_js_Pages_Map_vue-resources_js_Components_Map_draw_component_vue-resources_js_Compon-b0925c.js",
|
|
||||||
"assets/resources_js_Pages_ProfileView_vue.js": "http://localhost:8080/assets/resources_js_Pages_ProfileView_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_Review_vue.js": "http://localhost:8080/assets/resources_js_Pages_Reviewer_Dataset_Review_vue.js",
|
|
||||||
"assets/resources_js_Pages_Submitter_Dataset_Category_vue.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Category_vue.css",
|
|
||||||
"assets/resources_js_Pages_Submitter_Dataset_Category_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Category_vue.js",
|
|
||||||
"assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_utils_toast_css-resources_js_Com-03a898.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_utils_toast_css-resources_js_Com-03a898.css",
|
|
||||||
"assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_utils_toast_css-resources_js_Com-03a898.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Create_vue-resources_js_utils_toast_css-resources_js_Com-03a898.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_utils_toast_css-resources_js_Compo-a37b65.css": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-a37b65.css",
|
|
||||||
"assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-a37b65.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Dataset_Edit_vue-resources_js_utils_toast_css-resources_js_Compo-a37b65.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.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_Person_Index_vue.js": "http://localhost:8080/assets/resources_js_Pages_Submitter_Person_Index_vue.js",
|
|
||||||
"assets/resources_js_Pages_register-view_register-view-component_vue.js": "http://localhost:8080/assets/resources_js_Pages_register-view_register-view-component_vue.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_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_toastify-js_src_toastify_js.js": "http://localhost:8080/assets/vendors-node_modules_toastify-js_src_toastify_js.js",
|
|
||||||
"assets/vendors-node_modules_leaflet_dist_leaflet-src_js-node_modules_leaflet_src_control_Control_Att-adabdc.js": "http://localhost:8080/assets/vendors-node_modules_leaflet_dist_leaflet-src_js-node_modules_leaflet_src_control_Control_Att-adabdc.js",
|
|
||||||
"assets/vendors-node_modules_buffer_index_js-node_modules_vuedraggable_dist_vuedraggable_umd_js.js": "http://localhost:8080/assets/vendors-node_modules_buffer_index_js-node_modules_vuedraggable_dist_vuedraggable_umd_js.js",
|
|
||||||
"assets/vendors-node_modules_mime_dist_src_index_js.js": "http://localhost:8080/assets/vendors-node_modules_mime_dist_src_index_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/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_Layouts_LayoutAuthenticated_vue.css": "http://localhost:8080/assets/resources_js_Layouts_LayoutAuthenticated_vue.css",
|
|
||||||
"assets/resources_js_Layouts_LayoutAuthenticated_vue.js": "http://localhost:8080/assets/resources_js_Layouts_LayoutAuthenticated_vue.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-6f3a70.js": "http://localhost:8080/assets/resources_js_Components_Admin_Pagination_vue-resources_js_Components_BaseButtons_vue-resource-6f3a70.js",
|
|
||||||
"assets/resources_js_utils_toast_ts-resources_js_Components_NotificationBar_vue.js": "http://localhost:8080/assets/resources_js_utils_toast_ts-resources_js_Components_NotificationBar_vue.js",
|
|
||||||
"assets/resources_js_Components_Map_draw_component_vue-resources_js_Components_Map_zoom_component_vue-058bcc.js": "http://localhost:8080/assets/resources_js_Components_Map_draw_component_vue-resources_js_Components_Map_zoom_component_vue-058bcc.js",
|
|
||||||
"assets/resources_js_Components_SectionMain_vue-resources_js_Components_SectionTitleLineWithButton_vu-764dfe.js": "http://localhost:8080/assets/resources_js_Components_SectionMain_vue-resources_js_Components_SectionTitleLineWithButton_vu-764dfe.js",
|
|
||||||
"assets/resources_js_Components_BaseButtons_vue-resources_js_Components_NotificationBar_vue-resources-7e06d8.js": "http://localhost:8080/assets/resources_js_Components_BaseButtons_vue-resources_js_Components_NotificationBar_vue-resources-7e06d8.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_FileUpload_vue-resources_js_Components_FormCheckRadioGroup_vue-resour-25e686.js": "http://localhost:8080/assets/resources_js_Components_FileUpload_vue-resources_js_Components_FormCheckRadioGroup_vue-resour-25e686.js",
|
|
||||||
"assets/fonts/inter-latin-ext-400-normal.woff": "http://localhost:8080/assets/fonts/inter-latin-ext-400-normal.1c20f7dc.woff",
|
|
||||||
"assets/fonts/inter-latin-400-normal.woff": "http://localhost:8080/assets/fonts/inter-latin-400-normal.b0c8fe9d.woff",
|
|
||||||
"assets/fonts/inter-latin-ext-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-latin-ext-400-normal.3d10c85f.woff2",
|
|
||||||
"assets/fonts/inter-latin-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-latin-400-normal.9698cc7d.woff2",
|
|
||||||
"assets/fonts/archivo-black-latin-400-normal.woff2": "http://localhost:8080/assets/fonts/archivo-black-latin-400-normal.fc847a1f.woff2",
|
|
||||||
"assets/fonts/archivo-black-latin-ext-400-normal.woff2": "http://localhost:8080/assets/fonts/archivo-black-latin-ext-400-normal.21761451.woff2",
|
|
||||||
"assets/fonts/inter-cyrillic-ext-400-normal.woff": "http://localhost:8080/assets/fonts/inter-cyrillic-ext-400-normal.e8945162.woff",
|
|
||||||
"assets/fonts/archivo-black-latin-400-normal.woff": "http://localhost:8080/assets/fonts/archivo-black-latin-400-normal.58a301a6.woff",
|
|
||||||
"assets/fonts/inter-cyrillic-ext-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-cyrillic-ext-400-normal.fd1478dc.woff2",
|
|
||||||
"assets/fonts/inter-cyrillic-400-normal.woff": "http://localhost:8080/assets/fonts/inter-cyrillic-400-normal.e2841352.woff",
|
|
||||||
"assets/fonts/inter-greek-400-normal.woff": "http://localhost:8080/assets/fonts/inter-greek-400-normal.a42da273.woff",
|
|
||||||
"assets/fonts/archivo-black-latin-ext-400-normal.woff": "http://localhost:8080/assets/fonts/archivo-black-latin-ext-400-normal.5ab5ba92.woff",
|
|
||||||
"assets/fonts/inter-greek-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-greek-400-normal.a8de720a.woff2",
|
|
||||||
"assets/fonts/inter-cyrillic-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-cyrillic-400-normal.cb04b2ee.woff2",
|
|
||||||
"assets/fonts/inter-greek-ext-400-normal.woff": "http://localhost:8080/assets/fonts/inter-greek-ext-400-normal.b9e1e894.woff",
|
|
||||||
"assets/fonts/inter-vietnamese-400-normal.woff": "http://localhost:8080/assets/fonts/inter-vietnamese-400-normal.96f8adc7.woff",
|
|
||||||
"assets/fonts/inter-greek-ext-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-greek-ext-400-normal.f2fa0d9e.woff2",
|
|
||||||
"assets/fonts/inter-vietnamese-400-normal.woff2": "http://localhost:8080/assets/fonts/inter-vietnamese-400-normal.44c9df13.woff2",
|
|
||||||
"assets/images/marker-icon.png": "http://localhost:8080/assets/images/marker-icon.2b3e1faf.png",
|
|
||||||
"assets/images/layers-2x.png": "http://localhost:8080/assets/images/layers-2x.8f2c4d11.png",
|
|
||||||
"assets/images/layers.png": "http://localhost:8080/assets/images/layers.416d9136.png",
|
|
||||||
"assets/images/Close.svg": "http://localhost:8080/assets/images/Close.e4887675.svg",
|
|
||||||
"assets/vendors-node_modules_vue-facing-decorator_dist_esm_index_js-node_modules_vue-facing-decorator-818045.js": "http://localhost:8080/assets/vendors-node_modules_vue-facing-decorator_dist_esm_index_js-node_modules_vue-facing-decorator-818045.js"
|
|
||||||
}
|
|
BIN
public/favicon-16x16.png
Normal file
After Width: | Height: | Size: 526 B |
BIN
public/favicon-32x32.png
Normal file
After Width: | Height: | Size: 2.2 KiB |
3
public/favicon-32x32.png:Zone.Identifier
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
[ZoneTransfer]
|
||||||
|
ZoneId=3
|
||||||
|
HostUrl=https://sea1.geoinformation.dev/favicon-32x32.png
|
Before Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 18 KiB |
9
public/favicon.svg
Normal file
After Width: | Height: | Size: 952 KiB |
1
public/site.webmanifest
Normal file
|
@ -0,0 +1 @@
|
||||||
|
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
|
@ -1,19 +1,20 @@
|
||||||
/* @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500&display=swap'); */
|
/* @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@400;500&display=swap'); */
|
||||||
/* @import url('https://fonts.googleapis.com/css?family=Roboto:400,400i,600,700'); */
|
/* @import url('https://fonts.googleapis.com/css?family=Roboto:400,400i,600,700'); */
|
||||||
|
|
||||||
@tailwind base;
|
/* @import '_checkbox-radio-switch.css'; */
|
||||||
@tailwind components;
|
|
||||||
@tailwind utilities;
|
|
||||||
|
|
||||||
@import '_checkbox-radio-switch.css';
|
|
||||||
@import '_progress.css';
|
@import '_progress.css';
|
||||||
@import '_scrollbars.css';
|
@import '_scrollbars.css';
|
||||||
@import '_table.css';
|
@import '_table.css';
|
||||||
|
|
||||||
@import '~leaflet/dist/leaflet.css';
|
/* @import '~leaflet/dist/leaflet.css'; */
|
||||||
|
@import '~/leaflet/dist/leaflet.css';
|
||||||
@import '@fontsource/inter/index.css';
|
@import '@fontsource/inter/index.css';
|
||||||
@import '@fontsource/archivo-black/index.css';
|
@import '@fontsource/archivo-black/index.css';
|
||||||
|
|
||||||
|
|
||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
:root {
|
:root {
|
||||||
--color-main-background: #ffffff;
|
--color-main-background: #ffffff;
|
||||||
--color-main-background-rgb: 255,255,255;
|
--color-main-background-rgb: 255,255,255;
|
||||||
|
@ -108,6 +109,9 @@
|
||||||
--radius: 15;
|
--radius: 15;
|
||||||
--pi: 3.14159265358979;
|
--pi: 3.14159265358979;
|
||||||
}
|
}
|
||||||
|
.leaflet-container .leaflet-pane {
|
||||||
|
z-index: 30!important;
|
||||||
|
}
|
||||||
|
|
||||||
/* @layer base {
|
/* @layer base {
|
||||||
html,
|
html,
|
||||||
|
|
BIN
resources/js/Components/.FormField.vue.swo
Normal file
|
@ -1,161 +1,142 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { computed, ComputedRef } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { Link, usePage } from '@inertiajs/vue3';
|
import { Link, usePage } from '@inertiajs/vue3';
|
||||||
// import { Link } from '@inertiajs/inertia-vue3';
|
|
||||||
|
|
||||||
import { StyleService } from '@/Stores/style.service';
|
import { StyleService } from '@/Stores/style.service';
|
||||||
import { mdiMinus, mdiPlus } from '@mdi/js';
|
import { mdiMinus, mdiPlus } from '@mdi/js';
|
||||||
import { getButtonColor } from '@/colors';
|
import { getButtonColor } from '@/colors';
|
||||||
import BaseIcon from '@/Components/BaseIcon.vue';
|
import BaseIcon from '@/Components/BaseIcon.vue';
|
||||||
// import AsideMenuList from '@/Components/AsideMenuList.vue';
|
|
||||||
import { stardust } from '@eidellev/adonis-stardust/client';
|
import { stardust } from '@eidellev/adonis-stardust/client';
|
||||||
import type { User } from '@/Dataset';
|
import type { User } from '@/Dataset';
|
||||||
|
import { MenuItem } from '@headlessui/vue';
|
||||||
|
|
||||||
const props = defineProps({
|
interface MenuItem {
|
||||||
item: {
|
href?: string;
|
||||||
type: Object,
|
route?: string;
|
||||||
required: true,
|
icon?: string;
|
||||||
},
|
label: string;
|
||||||
parentItem: {
|
target?: string;
|
||||||
type: Object,
|
color?: string;
|
||||||
required: false,
|
children?: MenuItem[];
|
||||||
},
|
isOpen?: boolean;
|
||||||
// isDropdownList: Boolean,
|
roles?: string[];
|
||||||
});
|
}
|
||||||
|
|
||||||
const user: ComputedRef<User> = computed(() => {
|
const props = defineProps<{
|
||||||
return usePage().props.authUser as User;
|
item: MenuItem;
|
||||||
|
parentItem?: MenuItem;
|
||||||
|
// isDropdownList?: boolean;
|
||||||
|
}>();
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(e: 'menu-click', event: Event, item: MenuItem): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// Retrieve authenticated user from page props
|
||||||
|
const user = computed<User>(() => usePage().props.authUser as User);
|
||||||
|
|
||||||
|
// Check if the menu item has children
|
||||||
|
const hasChildren = computed(() => {
|
||||||
|
return Array.isArray(props.item?.children) && props.item.children.length > 0;
|
||||||
});
|
});
|
||||||
|
|
||||||
const itemRoute = computed(() => (props.item && props.item.route ? stardust.route(props.item.route) : ''));
|
const itemRoute = computed(() => (props.item && props.item.route ? stardust.route(props.item.route) : ''));
|
||||||
// const isCurrentRoute = computed(() => (props.item && props.item.route ? stardust.isCurrent(props.item.route): false));
|
// const isCurrentRoute = computed(() => (props.item && props.item.route ? stardust.isCurrent(props.item.route): false));
|
||||||
// const itemHref = computed(() => (props.item && props.item.href ? props.item.href : ''));
|
|
||||||
|
|
||||||
const emit = defineEmits(['menu-click']);
|
// Determine which element to render based on 'href' or 'route'
|
||||||
|
const isComponent = computed(() => {
|
||||||
|
if (props.item.href) {
|
||||||
|
return 'a';
|
||||||
|
}
|
||||||
|
if (props.item.route) {
|
||||||
|
return Link;
|
||||||
|
}
|
||||||
|
return 'div';
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check if any child route is active
|
||||||
|
const isChildActive = computed(() => {
|
||||||
|
if (props.item.children && props.item.children.length > 0) {
|
||||||
|
return props.item.children.some(child => child.route && stardust.isCurrent(child.route));
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Automatically use prop item.isOpen if set from the parent,
|
||||||
|
// or if one of its children is active then force open state.
|
||||||
|
const isOpen = computed(() => {
|
||||||
|
return props.item.isOpen || isChildActive.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
const styleService = StyleService();
|
const styleService = StyleService();
|
||||||
|
|
||||||
const hasColor = computed(() => props.item && props.item.color);
|
const hasColor = computed(() => props.item && props.item.color);
|
||||||
|
|
||||||
// const isDropdownOpen = ref(false);
|
|
||||||
|
|
||||||
// const isChildSelected = computed(() => {
|
|
||||||
// if (props.item.children && props.item.children.length > 0) {
|
// const children = computed(() => {
|
||||||
// return children.value.some(childItem => stardust.isCurrent(childItem.route));
|
// return props.item.children || [];
|
||||||
// }
|
|
||||||
// return false;
|
|
||||||
// });
|
// });
|
||||||
|
|
||||||
|
|
||||||
const hasChildren = computed(() => {
|
|
||||||
// props.item.children?.length > 0
|
|
||||||
if (props.item.children && props.item.children.length > 0) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
const children = computed(() => {
|
|
||||||
return props.item.children || [];
|
|
||||||
});
|
|
||||||
|
|
||||||
const componentClass = computed(() => [
|
const componentClass = computed(() => [
|
||||||
hasChildren ? 'py-3 px-6 text-sm font-semibold' : 'py-3 px-6',
|
hasChildren ? 'py-3 px-6 text-sm font-semibold' : 'py-3 px-6',
|
||||||
hasColor.value ? getButtonColor(props.item.color, false, true) : styleService.asideMenuItemStyle,
|
hasColor.value ? getButtonColor(props.item.color, false, true) : styleService.asideMenuItemStyle,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
||||||
|
const menuClick = (event: Event) => {
|
||||||
// const toggleDropdown = () => {
|
|
||||||
// // emit('menu-click', event, props.item);
|
|
||||||
// // console.log(props.item);
|
|
||||||
// if (hasChildren.value) {
|
|
||||||
// isDropdownOpen.value = !isDropdownOpen.value;
|
|
||||||
// }
|
|
||||||
// // if (props.parentItem?.hasDropdown.value) {
|
|
||||||
// // props.parentItem.isDropdownActive.value = true;
|
|
||||||
// // }
|
|
||||||
// };
|
|
||||||
|
|
||||||
const menuClick = (event) => {
|
|
||||||
emit('menu-click', event, props.item);
|
emit('menu-click', event, props.item);
|
||||||
|
|
||||||
if (hasChildren.value) {
|
if (hasChildren.value) {
|
||||||
// if (isChildSelected.value == false) {
|
// Toggle open state if the menu has children
|
||||||
// isDropdownOpen.value = !isDropdownOpen.value;
|
props.item.isOpen = !props.item.isOpen;
|
||||||
props.item.isOpen = !props.item.isOpen;
|
|
||||||
// }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// const handleChildSelected = () => {
|
const activeStyle = computed(() => {
|
||||||
// isChildSelected.value = true;
|
|
||||||
// };
|
|
||||||
|
|
||||||
|
|
||||||
const activeInactiveStyle = computed(() => {
|
|
||||||
if (props.item.route && stardust.isCurrent(props.item.route)) {
|
if (props.item.route && stardust.isCurrent(props.item.route)) {
|
||||||
// console.log(props.item.route);
|
// console.log(props.item.route);
|
||||||
return styleService.asideMenuItemActiveStyle;
|
return 'text-sky-600 font-bold';
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const is = computed(() => {
|
|
||||||
if (props.item.href) {
|
|
||||||
return 'a';
|
|
||||||
}
|
|
||||||
if (props.item.route) {
|
|
||||||
return Link;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'div';
|
|
||||||
});
|
|
||||||
|
|
||||||
const hasRoles = computed(() => {
|
const hasRoles = computed(() => {
|
||||||
if (props.item.roles) {
|
if (props.item.roles) {
|
||||||
return user.value.roles.some(role => props.item.roles.includes(role.name));
|
return user.value.roles.some(role => props.item.roles?.includes(role.name));
|
||||||
// return test;
|
// return test;
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
});
|
});
|
||||||
|
|
||||||
// props.routeName && stardust.isCurrent(props.routeName) ? props.activeColor : null
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- :target="props.item.target ?? null" -->
|
<!-- :target="props.item.target ?? null" -->
|
||||||
<template>
|
<template>
|
||||||
<li v-if="hasRoles">
|
<li v-if="hasRoles">
|
||||||
<!-- <component :is="itemHref ? 'div' : Link" :href="itemHref ? itemHref : itemRoute" -->
|
<component :is="isComponent" :href="props.item.href ? props.item.href : itemRoute"
|
||||||
<component :is="is" :href="itemRoute ? stardust.route(props.item.route) : props.item.href"
|
class="flex cursor-pointer dark:text-slate-300 dark:hover:text-white menu-item-wrapper"
|
||||||
class="flex cursor-pointer dark:text-slate-300 dark:hover:text-white menu-item-wrapper" :class="componentClass"
|
:class="componentClass" @click="menuClick" :target="props.item.target || null">
|
||||||
@click="menuClick" v-bind:target="props.item.target ?? null">
|
<BaseIcon v-if="props.item.icon" :path="props.item.icon" class="flex-none menu-item-icon"
|
||||||
<BaseIcon v-if="item.icon" :path="item.icon" class="flex-none menu-item-icon" :class="activeInactiveStyle"
|
:class="activeStyle" w="w-16" :size="18" />
|
||||||
w="w-16" :size="18" />
|
|
||||||
<div class="menu-item-label">
|
<div class="menu-item-label">
|
||||||
<span class="grow text-ellipsis line-clamp-1" :class="activeInactiveStyle">
|
<span class="grow text-ellipsis line-clamp-1" :class="[activeStyle]">
|
||||||
{{ item.label }}
|
{{ props.item.label }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<!-- plus icon for expanding sub menu -->
|
<!-- Display plus or minus icon if there are child items -->
|
||||||
<BaseIcon v-if="hasChildren" :path="props.item.isOpen ? mdiMinus : mdiPlus" class="flex-none"
|
<BaseIcon v-if="hasChildren" :path="isOpen ? mdiMinus : mdiPlus" class="flex-none"
|
||||||
:class="[activeInactiveStyle]" w="w-12" />
|
:class="[activeStyle]" w="w-12" />
|
||||||
</component>
|
</component>
|
||||||
|
<!-- Render dropdown -->
|
||||||
<div class="menu-item-dropdown"
|
<div class="menu-item-dropdown"
|
||||||
:class="[styleService.asideMenuDropdownStyle, props.item.isOpen ? 'block dark:bg-slate-800/50' : 'hidden']"
|
:class="[styleService.asideMenuDropdownStyle, isOpen ? 'block dark:bg-slate-800/50' : 'hidden']"
|
||||||
v-if="hasChildren">
|
v-if="props.item.children && props.item.children.length > 0">
|
||||||
<ul>
|
<ul>
|
||||||
<!-- <li v-for="( child, index ) in children " :key="index">
|
|
||||||
<AsideMenuItem :item="child" :key="index"> </AsideMenuItem>
|
<AsideMenuItem v-for="(childItem, index) in (props.item.children as any[])" :key="index" :item="childItem"
|
||||||
</li> -->
|
@menu-click="$emit('menu-click', $event, childItem)" />
|
||||||
<AsideMenuItem v-for="(childItem, index) in children" :key="index" :item="childItem" />
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
<!-- <AsideMenuList v-if="hasChildren" :items="item.children"
|
|
||||||
:class="[styleService.asideMenuDropdownStyle, isDropdownOpen ? 'block dark:bg-slate-800/50' : 'hidden']" /> -->
|
|
||||||
|
|
||||||
|
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -167,17 +148,12 @@ const hasRoles = computed(() => {
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-item-icon {
|
.menu-item-icon {
|
||||||
font-size: 2.5rem;
|
font-size: 2.5rem;
|
||||||
/* margin-right: 10px; */
|
/* margin-right: 10px; */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* .menu-item-label {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
} */
|
|
||||||
|
|
||||||
.menu-item-dropdown {
|
.menu-item-dropdown {
|
||||||
/* margin-left: 10px; */
|
/* margin-left: 10px; */
|
||||||
padding-left: 0.75rem;
|
padding-left: 0.75rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -36,13 +36,24 @@ const logoutItemClick = async () => {
|
||||||
await router.post(stardust.route('logout'));
|
await router.post(stardust.route('logout'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuClick = (event, item) => {
|
interface MenuItem {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
icon: string;
|
||||||
|
color: string;
|
||||||
|
link: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const menuClick = (event: Event, item: MenuItem) => {
|
||||||
emit('menu-click', event, item);
|
emit('menu-click', event, item);
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<aside id="aside" class="lg:py-2 lg:pl-2 w-60 fixed flex z-40 top-0 h-screen transition-position overflow-hidden">
|
<aside
|
||||||
|
id="aside"
|
||||||
|
class="lg:pb-2 lg:pl-2 w-60 fixed flex z-40 top-0 lg:top-16 h-screen lg:h-[calc(100vh-64px)] transition-position overflow-hidden"
|
||||||
|
>
|
||||||
<div :class="styleStore.asideStyle" class="lg:rounded-xl flex-1 flex flex-col overflow-hidden dark:bg-slate-900">
|
<div :class="styleStore.asideStyle" class="lg:rounded-xl flex-1 flex flex-col overflow-hidden dark:bg-slate-900">
|
||||||
<div :class="styleStore.asideBrandStyle" class="flex flex-row h-14 items-center justify-between dark:bg-slate-900">
|
<div :class="styleStore.asideBrandStyle" class="flex flex-row h-14 items-center justify-between dark:bg-slate-900">
|
||||||
<div class="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
|
<div class="text-center flex-1 lg:text-left lg:pl-6 xl:text-center xl:pl-0">
|
||||||
|
|
|
@ -12,6 +12,10 @@ const props = defineProps({
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
showHeaderIcon: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
headerIcon: {
|
headerIcon: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -63,7 +67,7 @@ const submit = (e) => {
|
||||||
<BaseIcon v-if="icon" :path="icon" class="mr-3" />
|
<BaseIcon v-if="icon" :path="icon" class="mr-3" />
|
||||||
{{ title }}
|
{{ title }}
|
||||||
</div>
|
</div>
|
||||||
<button class="flex items-center py-3 px-4 justify-center ring-blue-700 focus:ring" @click="headerIconClick">
|
<button v-if="showHeaderIcon" class="flex items-center py-3 px-4 justify-center ring-blue-700 focus:ring" @click="headerIconClick">
|
||||||
<BaseIcon :path="computedHeaderIcon" />
|
<BaseIcon :path="computedHeaderIcon" />
|
||||||
</button>
|
</button>
|
||||||
</header>
|
</header>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import { mdiTrendingDown, mdiTrendingUp, mdiTrendingNeutral } from '@mdi/js';
|
// import { mdiTrendingDown, mdiTrendingUp, mdiTrendingNeutral } from '@mdi/js';
|
||||||
import CardBox from '@/Components/CardBox.vue';
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
import BaseLevel from '@/Components/BaseLevel.vue';
|
import BaseLevel from '@/Components/BaseLevel.vue';
|
||||||
import PillTag from '@/Components/PillTag.vue';
|
import PillTag from '@/Components/PillTag.vue';
|
||||||
|
@ -27,6 +27,10 @@ const props = defineProps({
|
||||||
type: Number,
|
type: Number,
|
||||||
default: 0,
|
default: 0,
|
||||||
},
|
},
|
||||||
|
count: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
text: {
|
text: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -35,6 +39,10 @@ const props = defineProps({
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
allowEmailContact: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const pillType = computed(() => {
|
const pillType = computed(() => {
|
||||||
|
@ -42,11 +50,11 @@ const pillType = computed(() => {
|
||||||
return props.type;
|
return props.type;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.progress) {
|
if (props.count) {
|
||||||
if (props.progress >= 60) {
|
if (props.count >= 20) {
|
||||||
return 'success';
|
return 'success';
|
||||||
}
|
}
|
||||||
if (props.progress >= 40) {
|
if (props.count >= 5) {
|
||||||
return 'warning';
|
return 'warning';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,17 +64,17 @@ const pillType = computed(() => {
|
||||||
return 'info';
|
return 'info';
|
||||||
});
|
});
|
||||||
|
|
||||||
const pillIcon = computed(() => {
|
// const pillIcon = computed(() => {
|
||||||
return {
|
// return {
|
||||||
success: mdiTrendingUp,
|
// success: mdiTrendingUp,
|
||||||
warning: mdiTrendingNeutral,
|
// warning: mdiTrendingNeutral,
|
||||||
danger: mdiTrendingDown,
|
// danger: mdiTrendingDown,
|
||||||
info: mdiTrendingNeutral,
|
// info: mdiTrendingNeutral,
|
||||||
}[pillType.value];
|
// }[pillType.value];
|
||||||
});
|
// });
|
||||||
|
|
||||||
const pillText = computed(() => props.text ?? `${props.progress}%`);
|
// const pillText = computed(() => props.text ?? `${props.progress}%`);
|
||||||
</script>
|
// </script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CardBox class="mb-6 last:mb-0" hoverable>
|
<CardBox class="mb-6 last:mb-0" hoverable>
|
||||||
|
@ -78,12 +86,21 @@ const pillText = computed(() => props.text ?? `${props.progress}%`);
|
||||||
{{ name }}
|
{{ name }}
|
||||||
</h4>
|
</h4>
|
||||||
<p class="text-gray-500 dark:text-slate-400">
|
<p class="text-gray-500 dark:text-slate-400">
|
||||||
<!-- {{ date }} @ {{ login }} -->
|
<div v-if="props.allowEmailContact"> {{ email }}</div>
|
||||||
{{ email }}
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</BaseLevel>
|
</BaseLevel>
|
||||||
<PillTag :type="pillType" :text="pillText" small :icon="pillIcon" />
|
<!-- <PillTag :type="pillType" :text="text" small :icon="pillIcon" /> -->
|
||||||
|
|
||||||
|
<div class="text-center md:text-right space-y-2">
|
||||||
|
<p class="text-sm text-gray-500">
|
||||||
|
Count
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<PillTag :type="pillType" :text="String(count)" small />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</BaseLevel>
|
</BaseLevel>
|
||||||
</CardBox>
|
</CardBox>
|
||||||
</template>
|
</template>
|
||||||
|
|
107
resources/js/Components/CardBoxDataset.vue
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, PropType } from 'vue';
|
||||||
|
import { mdiChartTimelineVariant, mdiFileDocumentOutline, mdiFileOutline, mdiDatabase } from '@mdi/js';
|
||||||
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
|
import PillTag from '@/Components/PillTag.vue';
|
||||||
|
import IconRounded from '@/Components/IconRounded.vue';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
|
|
||||||
|
// Extend dayjs to support relative times
|
||||||
|
dayjs.extend(relativeTime);
|
||||||
|
|
||||||
|
interface Dataset {
|
||||||
|
account_id: number;
|
||||||
|
created_at: string;
|
||||||
|
creating_corporation: string;
|
||||||
|
editor_id: number;
|
||||||
|
embargo_date: string | null;
|
||||||
|
id: number;
|
||||||
|
language: string;
|
||||||
|
main_abstract: string | null;
|
||||||
|
main_title: string | null;
|
||||||
|
preferred_reviewer: string | null;
|
||||||
|
preferred_reviewer_email: string | null;
|
||||||
|
project_id: number | null;
|
||||||
|
publish_id: number;
|
||||||
|
publisher_name: string;
|
||||||
|
reject_editor_note: string | null;
|
||||||
|
reject_reviewer_note: string | null;
|
||||||
|
remaining_time: number;
|
||||||
|
reviewer_id: number;
|
||||||
|
server_date_modified: string;
|
||||||
|
server_date_published: string;
|
||||||
|
server_state: string;
|
||||||
|
type: string;
|
||||||
|
doi_identifier: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
dataset: {
|
||||||
|
type: Object as PropType<Dataset>,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const icon = computed(() => {
|
||||||
|
switch (props.dataset.type) {
|
||||||
|
case 'analysisdata':
|
||||||
|
return { icon: mdiChartTimelineVariant, type: 'success' };
|
||||||
|
case 'measurementdata':
|
||||||
|
return { icon: mdiFileDocumentOutline, type: 'warning' };
|
||||||
|
case 'monitoring':
|
||||||
|
return { icon: mdiFileOutline, type: 'info' };
|
||||||
|
case 'remotesensing':
|
||||||
|
return { icon: mdiDatabase, type: 'primary' };
|
||||||
|
case 'gis':
|
||||||
|
return { icon: mdiDatabase, type: 'info' };
|
||||||
|
case 'models':
|
||||||
|
return { icon: mdiChartTimelineVariant, type: 'success' };
|
||||||
|
case 'mixedtype':
|
||||||
|
return { icon: mdiFileDocumentOutline, type: 'warning' };
|
||||||
|
case 'vocabulary':
|
||||||
|
return { icon: mdiFileOutline, type: 'info' };
|
||||||
|
default:
|
||||||
|
return { icon: mdiDatabase, type: 'secondary' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const displayTitle = computed(() => props.dataset.main_title || 'Untitled Dataset');
|
||||||
|
|
||||||
|
const doiLink = computed(() => {
|
||||||
|
return `https://doi.tethys.at/10.24341/tethys.${props.dataset.publish_id}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
const relativeDate = computed(() => {
|
||||||
|
const publishedDate = dayjs(props.dataset.server_date_published);
|
||||||
|
if (publishedDate.isValid()) {
|
||||||
|
return publishedDate.fromNow();
|
||||||
|
}
|
||||||
|
return props.dataset.server_date_published;
|
||||||
|
});
|
||||||
|
|
||||||
|
// const displayBusiness = computed(() => props.dataset.publisher_name);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<CardBox class="mb-6 last:mb-0" hoverable>
|
||||||
|
<div class="flex items-start justify-between">
|
||||||
|
<IconRounded :icon="icon.icon" :type="icon.type" class="mr-6" />
|
||||||
|
<div class="flex-grow space-y-1 text-left" style="width: 70%;">
|
||||||
|
<h4 class="text-lg truncate" >
|
||||||
|
{{ displayTitle }}
|
||||||
|
</h4>
|
||||||
|
<p class="text-gray-500 dark:text-slate-400">
|
||||||
|
<b>
|
||||||
|
<a :href="doiLink" target="_blank">View Publication</a>
|
||||||
|
</b>
|
||||||
|
• {{ relativeDate }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col items-end gap-2">
|
||||||
|
<p class="text-sm text-gray-500">{{ props.dataset.type }}</p>
|
||||||
|
<PillTag :type="icon.type" :text="props.dataset.type" small class="inline-flex" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardBox>
|
||||||
|
</template>
|
|
@ -61,7 +61,7 @@ const cancel = () => confirmCancel('cancel');
|
||||||
<CardBox
|
<CardBox
|
||||||
v-show="value"
|
v-show="value"
|
||||||
:title="title"
|
:title="title"
|
||||||
class="shadow-lg max-h-modal w-11/12 md:w-3/5 lg:w-2/5 xl:w-4/12 z-50"
|
class="p-4 shadow-lg max-h-modal w-11/12 md:w-3/5 lg:w-2/5 xl:w-4/12 z-50"
|
||||||
:header-icon="mdiClose"
|
:header-icon="mdiClose"
|
||||||
modal
|
modal
|
||||||
@header-icon-click="cancel"
|
@header-icon-click="cancel"
|
||||||
|
|
75
resources/js/Components/CardBoxSimple.vue
Normal file
|
@ -0,0 +1,75 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
|
||||||
|
import { computed, useSlots } from 'vue';
|
||||||
|
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
icon: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
showHeaderIcon: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
headerIcon: {
|
||||||
|
type: String,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
|
rounded: {
|
||||||
|
type: String,
|
||||||
|
default: 'rounded-xl',
|
||||||
|
},
|
||||||
|
hasFormData: Boolean,
|
||||||
|
empty: Boolean,
|
||||||
|
form: Boolean,
|
||||||
|
hoverable: Boolean,
|
||||||
|
modal: Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['header-icon-click', 'submit']);
|
||||||
|
|
||||||
|
const is = computed(() => (props.form ? 'form' : 'div'));
|
||||||
|
|
||||||
|
const slots = useSlots();
|
||||||
|
|
||||||
|
// const footer = computed(() => slots.footer && !!slots.footer());
|
||||||
|
|
||||||
|
const componentClass = computed(() => {
|
||||||
|
const base = [props.rounded, props.modal ? 'dark:bg-slate-900' : 'dark:bg-slate-900/70'];
|
||||||
|
|
||||||
|
if (props.hoverable) {
|
||||||
|
base.push('hover:shadow-lg transition-shadow duration-500');
|
||||||
|
}
|
||||||
|
|
||||||
|
return base;
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// const headerIconClick = () => {
|
||||||
|
// emit('header-icon-click');
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const submit = (e) => {
|
||||||
|
// emit('submit', e);
|
||||||
|
// };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<component :is="is" :class="componentClass" class="bg-white flex flex-col border border-gray-100 dark:border-slate-800 mb-4">
|
||||||
|
|
||||||
|
<div v-if="empty" class="text-center py-24 text-gray-500 dark:text-slate-400">
|
||||||
|
<p>Nothing's here…</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="flex-1" :class="[!hasFormData && 'p-6']">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</component>
|
||||||
|
</template>
|
|
@ -1,4 +1,4 @@
|
||||||
<script setup>
|
<script lang="ts" setup>
|
||||||
import { mdiCog } from '@mdi/js';
|
import { mdiCog } from '@mdi/js';
|
||||||
import CardBox from '@/Components/CardBox.vue';
|
import CardBox from '@/Components/CardBox.vue';
|
||||||
import NumberDynamic from '@/Components/NumberDynamic.vue';
|
import NumberDynamic from '@/Components/NumberDynamic.vue';
|
||||||
|
@ -49,6 +49,9 @@ defineProps({
|
||||||
<PillTagTrend :trend="trend" :trend-type="trendType" small />
|
<PillTagTrend :trend="trend" :trend-type="trendType" small />
|
||||||
<BaseButton :icon="mdiCog" icon-w="w-4" icon-h="h-4" color="white" small />
|
<BaseButton :icon="mdiCog" icon-w="w-4" icon-h="h-4" color="white" small />
|
||||||
</BaseLevel>
|
</BaseLevel>
|
||||||
|
<BaseLevel v-else class="mb-3" mobile>
|
||||||
|
<BaseIcon v-if="icon" :path="icon" size="48" w="w-4" h="h-4" :class="color" />
|
||||||
|
</BaseLevel>
|
||||||
<BaseLevel mobile>
|
<BaseLevel mobile>
|
||||||
<div>
|
<div>
|
||||||
<h3 class="text-lg leading-tight text-gray-500 dark:text-slate-400">
|
<h3 class="text-lg leading-tight text-gray-500 dark:text-slate-400">
|
||||||
|
|
|
@ -17,6 +17,15 @@
|
||||||
<p class="text-lg text-blue-700">Drop files to upload</p>
|
<p class="text-lg text-blue-700">Drop files to upload</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Loading Spinner when processing big files -->
|
||||||
|
<div v-if="isLoading" class="absolute inset-0 z-60 flex items-center justify-center bg-gray-500 bg-opacity-50">
|
||||||
|
<svg class="animate-spin h-12 w-12 text-white" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
|
viewBox="0 0 24 24">
|
||||||
|
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||||
|
<path class="opacity-75" fill="currentColor" d="M12 2a10 10 0 0110 10h-4a6 6 0 00-6-6V2z"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- scroll area -->
|
<!-- scroll area -->
|
||||||
<div class="h-full p-8 w-full h-full flex flex-col">
|
<div class="h-full p-8 w-full h-full flex flex-col">
|
||||||
<header class="flex items-center justify-center w-full">
|
<header class="flex items-center justify-center w-full">
|
||||||
|
@ -32,9 +41,9 @@
|
||||||
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">
|
||||||
<span class="font-semibold">Click to upload</span> or drag and drop
|
<span class="font-semibold">Click to upload</span> or drag and drop
|
||||||
</p>
|
</p>
|
||||||
<!-- <p class="text-xs text-gray-500 dark:text-gray-400">SVG, PNG, JPG or GIF (MAX. 800x400px)</p> -->
|
|
||||||
</div>
|
</div>
|
||||||
<input id="dropzone-file" type="file" class="hidden" @change="onChangeFile" multiple="true" />
|
<input id="dropzone-file" type="file" class="hidden" @click="showSpinner" @change="onChangeFile"
|
||||||
|
@cancel="cancelSpinner" multiple="true" />
|
||||||
</label>
|
</label>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
@ -182,7 +191,7 @@
|
||||||
|
|
||||||
<!-- sticky footer -->
|
<!-- sticky footer -->
|
||||||
<footer class="flex justify-end px-8 pb-8 pt-4">
|
<footer class="flex justify-end px-8 pb-8 pt-4">
|
||||||
<button id="cancel"
|
<button v-if="showClearButton" id="cancel"
|
||||||
class="ml-3 rounded-sm px-3 py-1 hover:bg-gray-300 focus:shadow-outline focus:outline-none"
|
class="ml-3 rounded-sm px-3 py-1 hover:bg-gray-300 focus:shadow-outline focus:outline-none"
|
||||||
@click="clearAllFiles">
|
@click="clearAllFiles">
|
||||||
Clear
|
Clear
|
||||||
|
@ -198,7 +207,7 @@ import DeleteIcon from '@/Components/Icons/Delete.vue';
|
||||||
import RefreshIcon from '@/Components/Icons/Refresh.vue';
|
import RefreshIcon from '@/Components/Icons/Refresh.vue';
|
||||||
// import { Page, PageProps, Errors, ErrorBag } from '@inertiajs/inertia';
|
// import { Page, PageProps, Errors, ErrorBag } from '@inertiajs/inertia';
|
||||||
import Draggable from 'vuedraggable';
|
import Draggable from 'vuedraggable';
|
||||||
import { Buffer } from 'buffer';
|
// import { Buffer } from 'buffer';
|
||||||
import { TethysFile } from '@/Dataset';
|
import { TethysFile } from '@/Dataset';
|
||||||
|
|
||||||
// lastModified: 1691759507591
|
// lastModified: 1691759507591
|
||||||
|
@ -241,6 +250,8 @@ class FileUploadComponent extends Vue {
|
||||||
|
|
||||||
@Ref('overlay') overlay: HTMLDivElement;
|
@Ref('overlay') overlay: HTMLDivElement;
|
||||||
|
|
||||||
|
|
||||||
|
public isLoading: boolean = false;
|
||||||
private counter: number = 0;
|
private counter: number = 0;
|
||||||
// @Prop() files: Array<TestFile>;
|
// @Prop() files: Array<TestFile>;
|
||||||
|
|
||||||
|
@ -257,6 +268,12 @@ class FileUploadComponent extends Vue {
|
||||||
})
|
})
|
||||||
filesToDelete: Array<TethysFile>;
|
filesToDelete: Array<TethysFile>;
|
||||||
|
|
||||||
|
@Prop({
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
})
|
||||||
|
showClearButton: boolean;
|
||||||
|
|
||||||
// // deletetFiles: Array<TethysFile> = [];
|
// // deletetFiles: Array<TethysFile> = [];
|
||||||
get deletetFiles(): Array<TethysFile> {
|
get deletetFiles(): Array<TethysFile> {
|
||||||
return this.filesToDelete;
|
return this.filesToDelete;
|
||||||
|
@ -342,10 +359,10 @@ class FileUploadComponent extends Vue {
|
||||||
}
|
}
|
||||||
|
|
||||||
// reset counter and append file to gallery when file is dropped
|
// reset counter and append file to gallery when file is dropped
|
||||||
|
|
||||||
public dropHandler(event: DragEvent): void {
|
public dropHandler(event: DragEvent): void {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const dataTransfer = event.dataTransfer;
|
const dataTransfer = event.dataTransfer;
|
||||||
|
// let bigFileFound = false;
|
||||||
if (dataTransfer) {
|
if (dataTransfer) {
|
||||||
for (const file of event.dataTransfer?.files) {
|
for (const file of event.dataTransfer?.files) {
|
||||||
// let fileName = String(file.name.replace(/\.[^/.]+$/, ''));
|
// let fileName = String(file.name.replace(/\.[^/.]+$/, ''));
|
||||||
|
@ -353,28 +370,73 @@ class FileUploadComponent extends Vue {
|
||||||
// if (file.type.match('image.*')) {
|
// if (file.type.match('image.*')) {
|
||||||
// this.generateURL(file);
|
// this.generateURL(file);
|
||||||
// }
|
// }
|
||||||
|
// if (file.size > 62914560) { // 60 MB in bytes
|
||||||
|
// bigFileFound = true;
|
||||||
|
// }
|
||||||
this._addFile(file);
|
this._addFile(file);
|
||||||
}
|
}
|
||||||
this.overlay.classList.remove('draggedover');
|
this.overlay.classList.remove('draggedover');
|
||||||
this.counter = 0;
|
this.counter = 0;
|
||||||
}
|
}
|
||||||
|
// if (bigFileFound) {
|
||||||
|
// this.isLoading = true;
|
||||||
|
// // Assume file processing delay; adjust timeout as needed or rely on async processing completion.
|
||||||
|
// setTimeout(() => {
|
||||||
|
// this.isLoading = false;
|
||||||
|
// }, 1500);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
public showSpinner() {
|
||||||
|
// event.preventDefault();
|
||||||
|
this.isLoading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public cancelSpinner() {
|
||||||
|
// const target = event.target as HTMLInputElement;
|
||||||
|
// // If no files were selected, remove spinner
|
||||||
|
// if (!target.files || target.files.length === 0) {
|
||||||
|
// this.isLoading = false;
|
||||||
|
// }
|
||||||
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public onChangeFile(event: Event) {
|
public onChangeFile(event: Event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
let target = event.target as HTMLInputElement;
|
||||||
// let uploadedFile = event.target.files[0];
|
// let uploadedFile = event.target.files[0];
|
||||||
// let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, ''));
|
// let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, ''));
|
||||||
|
|
||||||
for (const file of event.target.files) {
|
if (target && target.files) {
|
||||||
// let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, ''));
|
for (const file of event.target.files) {
|
||||||
// file.label = fileName;
|
// let fileName = String(event.target.files[0].name.replace(/\.[^/.]+$/, ''));
|
||||||
// if (file.type.match('image.*')) {
|
// file.label = fileName;
|
||||||
// this.generateURL(file);
|
// if (file.type.match('image.*')) {
|
||||||
// }
|
// this.generateURL(file);
|
||||||
this._addFile(file);
|
// }
|
||||||
|
// Immediately set spinner if any file is large (over 100 MB)
|
||||||
|
// for (const file of target.files) {
|
||||||
|
// if (file.size > 62914560) { // 100 MB
|
||||||
|
// bigFileFound = true;
|
||||||
|
// break;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if (bigFileFound) {
|
||||||
|
// this.isLoading = true;
|
||||||
|
// }
|
||||||
|
this._addFile(file);
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// if (bigFileFound) {
|
||||||
|
// this.isLoading = true;
|
||||||
|
// setTimeout(() => {
|
||||||
|
// this.isLoading = false;
|
||||||
|
// }, 1500);
|
||||||
|
// }
|
||||||
// this.overlay.classList.remove('draggedover');
|
// this.overlay.classList.remove('draggedover');
|
||||||
this.counter = 0;
|
this.counter = 0;
|
||||||
|
this.isLoading = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
get errors(): IDictionary {
|
get errors(): IDictionary {
|
||||||
|
@ -396,7 +458,9 @@ class FileUploadComponent extends Vue {
|
||||||
|
|
||||||
public clearAllFiles(event: Event) {
|
public clearAllFiles(event: Event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.items.splice(0);
|
if (this.showClearButton == true) {
|
||||||
|
this.items.splice(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeFile(key: number) {
|
public removeFile(key: number) {
|
||||||
|
@ -445,18 +509,19 @@ class FileUploadComponent extends Vue {
|
||||||
let localUrl: string = '';
|
let localUrl: string = '';
|
||||||
if (file instanceof File) {
|
if (file instanceof File) {
|
||||||
localUrl = URL.createObjectURL(file as Blob);
|
localUrl = URL.createObjectURL(file as Blob);
|
||||||
} else if (file.fileData) {
|
|
||||||
// const blob = new Blob([file.fileData]);
|
|
||||||
// localUrl = URL.createObjectURL(blob);
|
|
||||||
const parsed = JSON.parse(file.fileData);
|
|
||||||
file.fileData = '';
|
|
||||||
// retrieve the original buffer of data
|
|
||||||
const buff = Buffer.from(parsed.blob, 'base64');
|
|
||||||
const blob = new Blob([buff], { type: 'application/octet-stream' });
|
|
||||||
// file.blob = blob;
|
|
||||||
localUrl = URL.createObjectURL(blob);
|
|
||||||
file.fileSrc = localUrl;
|
|
||||||
}
|
}
|
||||||
|
// else if (file.fileData) {
|
||||||
|
// // const blob = new Blob([file.fileData]);
|
||||||
|
// // localUrl = URL.createObjectURL(blob);
|
||||||
|
// const parsed = JSON.parse(file.fileData);
|
||||||
|
// file.fileData = '';
|
||||||
|
// // retrieve the original buffer of data
|
||||||
|
// const buff = Buffer.from(parsed.blob, 'base64');
|
||||||
|
// const blob = new Blob([buff], { type: 'application/octet-stream' });
|
||||||
|
// // file.blob = blob;
|
||||||
|
// localUrl = URL.createObjectURL(blob);
|
||||||
|
// file.fileSrc = localUrl;
|
||||||
|
// }
|
||||||
|
|
||||||
// setTimeout(() => {
|
// setTimeout(() => {
|
||||||
// URL.revokeObjectURL(localUrl);
|
// URL.revokeObjectURL(localUrl);
|
||||||
|
@ -464,17 +529,6 @@ class FileUploadComponent extends Vue {
|
||||||
return localUrl;
|
return localUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
// private async downloadFile(id: number): Promise<string> {
|
|
||||||
// const response = await axios.get<Blob>(`/api/download/${id}`, {
|
|
||||||
// responseType: 'blob',
|
|
||||||
// });
|
|
||||||
// const url = URL.createObjectURL(response.data);
|
|
||||||
// setTimeout(() => {
|
|
||||||
// URL.revokeObjectURL(url);
|
|
||||||
// }, 1000);
|
|
||||||
// return url;
|
|
||||||
// }
|
|
||||||
|
|
||||||
public getFileSize(file: File) {
|
public getFileSize(file: File) {
|
||||||
if (file.size > 1024) {
|
if (file.size > 1024) {
|
||||||
if (file.size > 1048576) {
|
if (file.size > 1048576) {
|
||||||
|
@ -487,17 +541,6 @@ class FileUploadComponent extends Vue {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// private _addFile(file) {
|
|
||||||
// // const isImage = file.type.match('image.*');
|
|
||||||
// // const objectURL = URL.createObjectURL(file);
|
|
||||||
|
|
||||||
// // this.files[objectURL] = file;
|
|
||||||
// // let test: TethysFile = { upload: file, label: "dfdsfs", sorting: 0 };
|
|
||||||
// // file.sorting = this.files.length;
|
|
||||||
// file.sort_order = (this.items.length + 1),
|
|
||||||
// this.files.push(file);
|
|
||||||
// }
|
|
||||||
|
|
||||||
private _addFile(file: File) {
|
private _addFile(file: File) {
|
||||||
// const reader = new FileReader();
|
// const reader = new FileReader();
|
||||||
// reader.onload = (event) => {
|
// reader.onload = (event) => {
|
||||||
|
@ -529,14 +572,11 @@ class FileUploadComponent extends Vue {
|
||||||
// this.items.push(test);
|
// this.items.push(test);
|
||||||
this.items[this.items.length] = test;
|
this.items[this.items.length] = test;
|
||||||
} else {
|
} else {
|
||||||
|
file.sort_order = this.items.length + 1;
|
||||||
this.items.push(file);
|
this.items.push(file);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// use to check if a file is being dragged
|
|
||||||
// private _hasFiles({ types = [] as Array<string> }) {
|
|
||||||
// return types.indexOf('Files') > -1;
|
|
||||||
// }
|
|
||||||
private _hasFiles(dataTransfer: DataTransfer | null): boolean {
|
private _hasFiles(dataTransfer: DataTransfer | null): boolean {
|
||||||
return dataTransfer ? dataTransfer.items.length > 0 : false;
|
return dataTransfer ? dataTransfer.items.length > 0 : false;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,9 +15,10 @@ const year = computed(() => new Date().getFullYear());
|
||||||
<!-- Get more with <a href="https://tailwind-vue.justboil.me/" target="_blank" class="text-blue-600">Premium
|
<!-- Get more with <a href="https://tailwind-vue.justboil.me/" target="_blank" class="text-blue-600">Premium
|
||||||
version</a> -->
|
version</a> -->
|
||||||
</div>
|
</div>
|
||||||
<div class="md:py-3">
|
<div class="md:py-1">
|
||||||
<a href="https://www.tethys.at" target="_blank">
|
<a href="https://www.tethys.at" target="_blank">
|
||||||
<JustboilLogo class="w-auto h-8 md:h-6" />
|
<!-- <JustboilLogo class="w-auto h-8 md:h-6" /> -->
|
||||||
|
<JustboilLogo class="w-auto h-12 md:h-10 dark:invert" />
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</BaseLevel>
|
</BaseLevel>
|
||||||
|
|
|
@ -1,43 +1,59 @@
|
||||||
<script setup>
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
|
|
||||||
const props = defineProps({
|
interface Props {
|
||||||
name: {
|
name: string;
|
||||||
type: String,
|
type?: 'checkbox' | 'radio' | 'switch';
|
||||||
required: true,
|
label?: string | null;
|
||||||
},
|
modelValue: Array<any> | string | number | boolean | null;
|
||||||
type: {
|
inputValue: string | number | boolean;
|
||||||
type: String,
|
}
|
||||||
default: 'checkbox',
|
|
||||||
validator: (value) => ['checkbox', 'radio', 'switch'].includes(value),
|
const props = defineProps<Props>();
|
||||||
},
|
|
||||||
label: {
|
const emit = defineEmits<{ (e: 'update:modelValue', value: Props['modelValue']): void }>();
|
||||||
type: String,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
modelValue: {
|
|
||||||
type: [Array, String, Number, Boolean],
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
inputValue: {
|
|
||||||
type: [String, Number, Boolean],
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const emit = defineEmits(['update:modelValue']);
|
|
||||||
const computedValue = computed({
|
const computedValue = computed({
|
||||||
get: () => props.modelValue,
|
get: () => props.modelValue,
|
||||||
set: (value) => {
|
set: (value) => {
|
||||||
emit('update:modelValue', value);
|
emit('update:modelValue', props.type === 'radio' ? [value] : value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const inputType = computed(() => (props.type === 'radio' ? 'radio' : 'checkbox'));
|
const inputType = computed(() => (props.type === 'radio' ? 'radio' : 'checkbox'));
|
||||||
|
|
||||||
|
// Define isChecked for radio inputs: it's true when the current modelValue equals the inputValue
|
||||||
|
const isChecked = computed(() => {
|
||||||
|
if (Array.isArray(computedValue.value) && computedValue.value.length > 0) {
|
||||||
|
return props.type === 'radio'
|
||||||
|
? computedValue.value[0] === props.inputValue
|
||||||
|
: computedValue.value.includes(props.inputValue);
|
||||||
|
}
|
||||||
|
return computedValue.value === props.inputValue;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<label :class="type" class="mr-6 mb-3 last:mr-0">
|
<label v-if="type === 'radio'" :class="[type]"
|
||||||
<input v-model="computedValue" :type="inputType" :name="name" :value="inputValue" />
|
class="mr-6 mb-3 last:mr-0 inline-flex items-center cursor-pointer relative">
|
||||||
<span class="check" />
|
<input v-model="computedValue" :type="inputType" :name="name" :value="inputValue"
|
||||||
<span class="pl-2">{{ label }}</span>
|
class="absolute left-0 opacity-0 -z-1 focus:outline-none focus:ring-0"
|
||||||
|
:checked="isChecked" />
|
||||||
|
<span class="check border transition-colors duration-200 dark:bg-slate-800 block w-5 h-5 rounded-full" :class="{
|
||||||
|
'border-gray-700': !isChecked,
|
||||||
|
'bg-radio-checked bg-no-repeat bg-center bg-lime-600 border-lime-600 border-4': isChecked
|
||||||
|
}" />
|
||||||
|
<span class="pl-2 control-label">{{ label }}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label v-else-if="type === 'checkbox'" :class="[type]"
|
||||||
|
class="mr-6 mb-3 last:mr-0 inline-flex items-center cursor-pointer relative">
|
||||||
|
<input v-model="computedValue" :type="inputType" :name="name" :value="inputValue"
|
||||||
|
class="absolute left-0 opacity-0 -z-1 focus:outline-none focus:ring-0" />
|
||||||
|
<span class="check border transition-colors duration-200 dark:bg-slate-800 block w-5 h-5 rounded" :class="{
|
||||||
|
'border-gray-700': !isChecked,
|
||||||
|
'bg-checkbox-checked bg-no-repeat bg-center bg-lime-600 border-lime-600 border-4': isChecked
|
||||||
|
}" />
|
||||||
|
<span class="pl-2 control-label">{{ label }}</span>
|
||||||
</label>
|
</label>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -1,17 +1,29 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue';
|
import { computed, ref, PropType } from 'vue';
|
||||||
import FormCheckRadio from '@/Components/FormCheckRadio.vue';
|
import FormCheckRadio from '@/Components/FormCheckRadio.vue';
|
||||||
|
// import BaseButton from '@/Components/BaseButton.vue';
|
||||||
|
// import FormControl from '@/Components/FormControl.vue';
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
default: () => {},
|
default: () => { },
|
||||||
|
},
|
||||||
|
allowManualAdding: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
manualAddingPlaceholder: {
|
||||||
|
type: String,
|
||||||
|
default: 'Add manually',
|
||||||
|
required: false,
|
||||||
},
|
},
|
||||||
name: {
|
name: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String as PropType<'checkbox' | 'radio' | 'switch'>,
|
||||||
default: 'checkbox',
|
default: 'checkbox',
|
||||||
validator: (value: string) => ['checkbox', 'radio', 'switch'].includes(value),
|
validator: (value: string) => ['checkbox', 'radio', 'switch'].includes(value),
|
||||||
},
|
},
|
||||||
|
@ -35,7 +47,7 @@ const computedValue = computed({
|
||||||
if (props.modelValue.every((item) => typeof item === 'number')) {
|
if (props.modelValue.every((item) => typeof item === 'number')) {
|
||||||
return props.modelValue;
|
return props.modelValue;
|
||||||
} else if (props.modelValue.every((item) => hasIdAttribute(item))) {
|
} else if (props.modelValue.every((item) => hasIdAttribute(item))) {
|
||||||
const ids = props.modelValue.map((obj) => obj.id.toString());
|
const ids = props.modelValue.map((obj) => obj.id);
|
||||||
return ids;
|
return ids;
|
||||||
}
|
}
|
||||||
return props.modelValue;
|
return props.modelValue;
|
||||||
|
@ -55,6 +67,29 @@ const computedValue = computed({
|
||||||
const hasIdAttribute = (obj: any): obj is { id: any } => {
|
const hasIdAttribute = (obj: any): obj is { id: any } => {
|
||||||
return typeof obj === 'object' && 'id' in obj;
|
return typeof obj === 'object' && 'id' in obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const newOption = ref<string>('');
|
||||||
|
const addOption = () => {
|
||||||
|
if (newOption.value && !props.options[newOption.value]) {
|
||||||
|
props.options[newOption.value] = newOption.value;
|
||||||
|
newOption.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const inputElClass = computed(() => {
|
||||||
|
const base = [
|
||||||
|
'px-3 py-2 max-w-full border-gray-700 rounded w-full',
|
||||||
|
'dark:placeholder-gray-400',
|
||||||
|
'h-12',
|
||||||
|
'border',
|
||||||
|
'bg-transparent'
|
||||||
|
// props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
|
||||||
|
];
|
||||||
|
// if (props.icon) {
|
||||||
|
// base.push('pl-10');
|
||||||
|
// }
|
||||||
|
return base;
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -63,15 +98,17 @@ const hasIdAttribute = (obj: any): obj is { id: any } => {
|
||||||
<!-- :label="value" -->
|
<!-- :label="value" -->
|
||||||
<!-- :input-value="value.id"
|
<!-- :input-value="value.id"
|
||||||
:label="value.name" -->
|
:label="value.name" -->
|
||||||
<FormCheckRadio
|
<div v-if="allowManualAdding && type === 'checkbox'" class="flex items-center mt-2 mb-2 relative">
|
||||||
v-for="(value, key) in options"
|
<input v-model="newOption" :placeholder="manualAddingPlaceholder" :class="inputElClass"
|
||||||
:key="key"
|
@keydown.prevent.enter="addOption" />
|
||||||
v-model="computedValue"
|
<svg v-show="newOption.length >= 2" @click.prevent="addOption" xmlns="http://www.w3.org/2000/svg"
|
||||||
:type="type"
|
class="w-6 h-6 absolute right-2 top-1/2 transform -translate-y-1/2 cursor-pointer text-gray-500"
|
||||||
:name="name"
|
viewBox="0 0 24 24" fill="currentColor">
|
||||||
:input-value="key"
|
<path
|
||||||
:label="value"
|
d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 17h-2v-6H5v-2h6V5h2v6h6v2h-6v6z" />
|
||||||
:class="componentClass"
|
</svg>
|
||||||
/>
|
</div>
|
||||||
|
<FormCheckRadio v-for="(value, key) in options" :key="key" v-model="computedValue" :type="type"
|
||||||
|
:name="name" :input-value="Number(key)" :label="value" :class="componentClass" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -67,15 +67,28 @@ const computedValue = computed({
|
||||||
emit('update:modelValue', value);
|
emit('update:modelValue', value);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// focus:ring focus:outline-none border-gray-700
|
||||||
const inputElClass = computed(() => {
|
const inputElClass = computed(() => {
|
||||||
const base = [
|
const base = [
|
||||||
'px-3 py-2 max-w-full focus:ring focus:outline-none border-gray-700 rounded w-full',
|
'px-3 py-2 max-w-full rounded w-full',
|
||||||
'dark:placeholder-gray-400',
|
'dark:placeholder-gray-400',
|
||||||
props.extraHigh ? 'h-80' : (computedType.value === 'textarea' ? 'h-44' : 'h-12'),
|
props.extraHigh ? 'h-80' : (computedType.value === 'textarea' ? 'h-44' : 'h-12'),
|
||||||
props.borderless ? 'border-0' : 'border',
|
props.borderless ? 'border-0' : 'border',
|
||||||
// props.transparent && !props.isReadOnly ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
|
// // props.transparent && !props.isReadOnly ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
|
||||||
props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
|
// props.isReadOnly ? 'bg-gray-50 dark:bg-slate-600' : 'bg-white dark:bg-slate-800',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Apply styles based on read-only state.
|
||||||
|
if (props.isReadOnly) {
|
||||||
|
// Read-only: no focus ring, grayed-out text and border, and disabled cursor.
|
||||||
|
base.push('bg-gray-50', 'dark:bg-slate-600', 'border', 'border-gray-300', 'dark:border-slate-600', 'text-gray-500', 'cursor-not-allowed', 'focus:outline-none' ,'focus:ring-0', 'focus:border-gray-300');
|
||||||
|
} else {
|
||||||
|
// Actionable field: focus ring, white/dark background, and darker border.
|
||||||
|
base.push('bg-white dark:bg-slate-800', 'focus:ring focus:outline-none', 'border', 'border-gray-700');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (props.icon) {
|
if (props.icon) {
|
||||||
base.push('pl-10', 'pr-10');
|
base.push('pl-10', 'pr-10');
|
||||||
}
|
}
|
||||||
|
@ -118,6 +131,9 @@ if (props.ctrlKFocus) {
|
||||||
mainService.isFieldFocusRegistered = false;
|
mainService.isFieldFocusRegistered = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
const focus = () => {
|
||||||
|
inputEl?.value.focus();
|
||||||
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
@ -130,7 +146,7 @@ if (props.ctrlKFocus) {
|
||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
<textarea v-else-if="computedType === 'textarea'" :id="id" v-model="computedValue" :class="inputElClass"
|
<textarea v-else-if="computedType === 'textarea'" :id="id" v-model="computedValue" :class="inputElClass"
|
||||||
:name="name" :placeholder="placeholder" :required="required" />
|
:name="name" :placeholder="placeholder" :required="required" :readonly="isReadOnly"/>
|
||||||
<input v-else :id="id" ref="inputEl" v-model="computedValue" :name="name" :inputmode="inputmode"
|
<input v-else :id="id" ref="inputEl" v-model="computedValue" :name="name" :inputmode="inputmode"
|
||||||
:autocomplete="autocomplete" :required="required" :placeholder="placeholder" :type="computedType"
|
:autocomplete="autocomplete" :required="required" :placeholder="placeholder" :type="computedType"
|
||||||
:class="inputElClass" :readonly="isReadOnly" />
|
:class="inputElClass" :readonly="isReadOnly" />
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import { computed, useSlots } from 'vue';
|
import { computed, useSlots } from 'vue';
|
||||||
|
|
||||||
|
|
||||||
defineProps({
|
const props = defineProps({
|
||||||
label: {
|
label: {
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
|
@ -15,6 +15,10 @@ defineProps({
|
||||||
type: String,
|
type: String,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
|
// class: {
|
||||||
|
// type: Object,
|
||||||
|
// default: {},
|
||||||
|
// },
|
||||||
});
|
});
|
||||||
|
|
||||||
const slots = useSlots();
|
const slots = useSlots();
|
||||||
|
@ -36,7 +40,7 @@ const wrapperClass = computed(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="mb-6 last:mb-0">
|
<div :class="['last:mb-0', 'mb-6']">
|
||||||
<!-- <label v-if="label" :for="labelFor" class="block font-bold mb-2">{{ label }}</label> -->
|
<!-- <label v-if="label" :for="labelFor" class="block font-bold mb-2">{{ label }}</label> -->
|
||||||
<label v-if="label" :for="labelFor" class="font-bold h-6 mt-3 text-xs leading-8 uppercase">{{ label }}</label>
|
<label v-if="label" :for="labelFor" class="font-bold h-6 mt-3 text-xs leading-8 uppercase">{{ label }}</label>
|
||||||
<div v-bind:class="wrapperClass">
|
<div v-bind:class="wrapperClass">
|
||||||
|
|
74
resources/js/Components/Icons/IconSvg.vue
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import { mdiLicense } from '@mdi/js';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
path: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
default: 24
|
||||||
|
},
|
||||||
|
viewBox: {
|
||||||
|
type: String,
|
||||||
|
default: '0 0 24 24'
|
||||||
|
},
|
||||||
|
color: {
|
||||||
|
type: String,
|
||||||
|
default: 'currentColor'
|
||||||
|
},
|
||||||
|
className: {
|
||||||
|
type: String,
|
||||||
|
default: ''
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define all the SVG paths we need
|
||||||
|
const svgPaths = {
|
||||||
|
// Document/File icons
|
||||||
|
document: 'M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
||||||
|
documentPlus: 'M9 13h6m-3-3v6m5 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z',
|
||||||
|
|
||||||
|
// Communication icons
|
||||||
|
email: 'M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z',
|
||||||
|
|
||||||
|
// Identity/User icons
|
||||||
|
idCard: '10 2a1 1 0 00-1 1v1a1 1 0 002 0V3a1 1 0 00-1-1zM4 4h3a3 3 0 006 0h3a2 2 0 012 2v9a2 2 0 01-2 2H4a2 2 0 01-2-2V6a2 2 0 012-2zm2.5 7a1.5 1.5 0 100-3 1.5 1.5 0 000 3zm2.45 4a2.5 2.5 0 10-4.9 0h4.9zM12 9a1 1 0 100 2h3a1 1 0 100-2h-3zm-1 4a1 1 0 011-1h2a1 1 0 110 2h-2a1 1 0 01-1-1z',
|
||||||
|
|
||||||
|
// Language/Translation icons
|
||||||
|
// language: 'M7 2a1 1 0 011 1v1h3a1 1 0 110 2H9.578a18.87 18.87 0 01-1.724 4.78c.29.354.596.696.914 1.026a1 1 0 11-1.44 1.389c-.188-.196-.373-.396-.554-.6a19.098 19.098 0 01-3.107 3.567 1 1 0 01-1.334-1.49 17.087 17.087 0 003.13-3.733 18.992 18.992 0 01-1.487-2.494 1 1 0 111.79-.89c.234.47.489.928.764 1.372.417-.934.752-1.913.997-2.927H3a1 1 0 110-2h3V3a1 1 0 011-1zm6 6a1 1 0 01.894.553l2.991 5.982a.869.869 0 01.02.037l.99 1.98a1 1 0 11-1.79.895L15.383 16h-4.764l-.724 1.447a1 1 0 11-1.788-.894l.99-1.98.019-.038 2.99-5.982A1 1 0 0113 8zm-1.382 6h2.764L13 11.236 11.618 14z',
|
||||||
|
language: 'M12 2a10 10 0 1 0 0 20a10 10 0 1 0 0-20zm0 0c2.5 0 4.5 4.5 4.5 10s-2 10-4.5 10-4.5-4.5-4.5-10 2-10 4.5-10zm0 0a10 10 0 0 1 0 20a10 10 0 0 1 0-20z',
|
||||||
|
// License/Legal icons
|
||||||
|
// license: 'M10 2a1 1 0 00-1 1v1.323l-3.954 1.582A1 1 0 004 6.32V16a1 1 0 001.555.832l3-1.2a1 1 0 01.8 0l3 1.2a1 1 0 001.555-.832V6.32a1 1 0 00-1.046-.894L9 4.877V3a1 1 0 00-1-1zm0 14.5a.5.5 0 01-.5-.5v-4a.5.5 0 011 0v4a.5.5 0 01-.5.5zm1.5-10.5a.5.5 0 11-1 0 .5.5 0 011 0z',
|
||||||
|
license: mdiLicense,
|
||||||
|
|
||||||
|
// Building/Organization icons
|
||||||
|
building: 'M4 4a2 2 0 012-2h8a2 2 0 012 2v12a1 1 0 110 2h-3a1 1 0 01-1-1v-2a1 1 0 00-1-1H9a1 1 0 00-1 1v2a1 1 0 01-1 1H4a1 1 0 110-2V4zm3 1h2v2H7V5zm2 4H7v2h2V9zm2-4h2v2h-2V5zm2 4h-2v2h2V9z',
|
||||||
|
|
||||||
|
// Book/Publication icons
|
||||||
|
book: 'M9 4.804A7.968 7.968 0 005.5 4c-1.255 0-2.443.29-3.5.804v10A7.969 7.969 0 015.5 14c1.669 0 3.218.51 4.5 1.385A7.962 7.962 0 0114.5 14c1.255 0 2.443.29 3.5.804v-10A7.968 7.968 0 0014.5 4c-1.255 0-2.443.29-3.5.804V12a1 1 0 11-2 0V4.804z',
|
||||||
|
|
||||||
|
// Download icon
|
||||||
|
download: 'M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4'
|
||||||
|
};
|
||||||
|
|
||||||
|
const pathData = computed(() => {
|
||||||
|
return svgPaths[props.path] || props.path;
|
||||||
|
});
|
||||||
|
|
||||||
|
const sizeStyle = computed(() => {
|
||||||
|
return {
|
||||||
|
width: `${props.size}px`,
|
||||||
|
height: `${props.size}px`
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg :style="sizeStyle" :class="className" :viewBox="viewBox" xmlns="http://www.w3.org/2000/svg" fill="none"
|
||||||
|
:stroke="color" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path :d="pathData" />
|
||||||
|
</svg>
|
||||||
|
</template>
|
124
resources/js/Components/Map/MapComponentView.vue
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, ref, watch } from 'vue';
|
||||||
|
import L from 'leaflet';
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
|
||||||
|
const DEFAULT_BASE_LAYER_NAME = 'BaseLayer';
|
||||||
|
const DEFAULT_BASE_LAYER_ATTRIBUTION = '© <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
coverage: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: String,
|
||||||
|
default: '250px'
|
||||||
|
},
|
||||||
|
mapId: {
|
||||||
|
type: String,
|
||||||
|
default: 'view-map'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const map = ref(null);
|
||||||
|
const mapContainer = ref(null);
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
initializeMap();
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => props.coverage, (newCoverage) => {
|
||||||
|
if (map.value && newCoverage) {
|
||||||
|
updateBounds();
|
||||||
|
}
|
||||||
|
}, { deep: true });
|
||||||
|
|
||||||
|
const initializeMap = () => {
|
||||||
|
// Create the map with minimal controls
|
||||||
|
map.value = L.map(mapContainer.value, {
|
||||||
|
zoomControl: false,
|
||||||
|
attributionControl: false,
|
||||||
|
dragging: false,
|
||||||
|
scrollWheelZoom: false,
|
||||||
|
doubleClickZoom: false,
|
||||||
|
boxZoom: false,
|
||||||
|
tap: false,
|
||||||
|
keyboard: false,
|
||||||
|
touchZoom: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// // Add a simple tile layer (OpenStreetMap)
|
||||||
|
let osmGgray = new L.TileLayer.WMS('https://ows.terrestris.de/osm-gray/service', {
|
||||||
|
format: 'image/png',
|
||||||
|
attribution: DEFAULT_BASE_LAYER_ATTRIBUTION,
|
||||||
|
layers: 'OSM-WMS',
|
||||||
|
});
|
||||||
|
let layerOptions = {
|
||||||
|
label: DEFAULT_BASE_LAYER_NAME,
|
||||||
|
visible: true,
|
||||||
|
layer: osmGgray,
|
||||||
|
};
|
||||||
|
layerOptions.layer.addTo(map.value);
|
||||||
|
|
||||||
|
// Add a light-colored rectangle for the coverage area
|
||||||
|
updateBounds();
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateBounds = () => {
|
||||||
|
if (!props.coverage || !map.value) return;
|
||||||
|
|
||||||
|
// Clear any existing layers except the base tile layer
|
||||||
|
map.value.eachLayer(layer => {
|
||||||
|
if (layer instanceof L.Rectangle) {
|
||||||
|
map.value.removeLayer(layer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create bounds from the coverage coordinates
|
||||||
|
const bounds = L.latLngBounds(
|
||||||
|
[props.coverage.y_min, props.coverage.x_min],
|
||||||
|
[props.coverage.y_max, props.coverage.x_max]
|
||||||
|
);
|
||||||
|
|
||||||
|
// Add a rectangle with emerald styling
|
||||||
|
L.rectangle(bounds, {
|
||||||
|
color: '#10b981', // emerald-500
|
||||||
|
weight: 2,
|
||||||
|
fillColor: '#d1fae5', // emerald-100
|
||||||
|
fillOpacity: 0.5
|
||||||
|
}).addTo(map.value);
|
||||||
|
|
||||||
|
// Fit the map to the bounds with some padding
|
||||||
|
map.value.fitBounds(bounds, {
|
||||||
|
padding: [20, 20]
|
||||||
|
});
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="map-container bg-emerald-50 dark:bg-emerald-900/30
|
||||||
|
rounded-lg shadow-sm overflow-hidden">
|
||||||
|
<div :id="mapId" ref="mapContainer" :style="{ height: height }" class="w-full"></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
/* Ensure the Leaflet container has proper styling */
|
||||||
|
:deep(.leaflet-container) {
|
||||||
|
background-color: #f0fdf4;
|
||||||
|
/* emerald-50 */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark mode adjustments */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:deep(.leaflet-container) {
|
||||||
|
background-color: rgba(6, 78, 59, 0.3);
|
||||||
|
/* emerald-900/30 */
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.leaflet-tile) {
|
||||||
|
filter: brightness(0.8) contrast(1.2) grayscale(0.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -282,7 +282,7 @@ const handleDrawEventCreated = async (event) => {
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
||||||
<style lang="css">
|
<style scoped lang="css">
|
||||||
/* .leaflet-container {
|
/* .leaflet-container {
|
||||||
height: 600px;
|
height: 600px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
|
@ -268,7 +268,7 @@ export default class DrawControlComponent extends Vue {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
top: 100px;
|
top: 100px;
|
||||||
z-index: 999;
|
z-index: 40;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-group-vertical button {
|
.btn-group-vertical button {
|
||||||
|
|
|
@ -372,6 +372,9 @@ export default class MapComponent extends Vue {
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.leaflet-container .leaflet-pane {
|
||||||
|
z-index: 30!important;
|
||||||
|
}
|
||||||
/* .leaflet-pane {
|
/* .leaflet-pane {
|
||||||
z-index: 30;
|
z-index: 30;
|
||||||
} */
|
} */
|
||||||
|
|
|
@ -100,7 +100,7 @@ export default class ZoomControlComponent extends Vue {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 10px;
|
left: 10px;
|
||||||
top: 10px;
|
top: 10px;
|
||||||
z-index: 999;
|
z-index: 40;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-group-vertical button {
|
.btn-group-vertical button {
|
||||||
|
|
138
resources/js/Components/MimetypeInput.vue
Normal file
|
@ -0,0 +1,138 @@
|
||||||
|
<template>
|
||||||
|
<div class="relative mb-4">
|
||||||
|
<!-- <label for="mimetype-input" class="block text-sm font-medium text-gray-700">Search for Mimetypes</label> -->
|
||||||
|
<input id="mimetype-input" v-model="newExtension" type="text" placeholder="Enter Mimetype Name"
|
||||||
|
class="block w-full border border-gray-300 rounded-lg px-4 py-2 focus:ring-blue-500 focus:border-blue-500"
|
||||||
|
:class="inputElClass" @input="handleInputChange" @keydown.down="onArrowDown" @keydown.up="onArrowUp"
|
||||||
|
@keydown.prevent.enter="onEnter">
|
||||||
|
</input>
|
||||||
|
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="newExtension.length < 2"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||||
|
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
||||||
|
</svg>
|
||||||
|
<svg class="w-4 h-4 absolute left-2.5 top-3.5" v-show="newExtension.length >= 2"
|
||||||
|
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||||
|
@click="clearInput">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<ul v-if="showDropdown && filteredMimetypes.length"
|
||||||
|
class="bg-white dark:bg-slate-800 w-full mt-2 max-h-28 overflow-y-auto scroll-smooth">
|
||||||
|
<li v-for="(mimeType, index) in filteredMimetypes" :key="index" @click="selectResult(mimeType)"
|
||||||
|
class="pl-8 pr-2 py-1 border-b-2 border-gray-100 relative cursor-pointer hover:bg-blue-500 hover:text-white"
|
||||||
|
:class="{
|
||||||
|
'bg-blue-500 text-white': selectedIndex === index,
|
||||||
|
'bg-white text-gray-900': selectedIndex !== index
|
||||||
|
}" :ref="(el) => {
|
||||||
|
if (ul) {
|
||||||
|
ul[index] = el as HTMLLIElement;
|
||||||
|
}
|
||||||
|
}">
|
||||||
|
<svg class="absolute w-4 h-4 left-2 top-2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"
|
||||||
|
fill="currentColor">
|
||||||
|
<path fill-rule="evenodd"
|
||||||
|
d="M12.293 5.293a1 1 0 011.414 0l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414-1.414L14.586 11H3a1 1 0 110-2h11.586l-2.293-2.293a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd" />
|
||||||
|
</svg>
|
||||||
|
<span>{{ mimeType }}</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { ref, Ref, watch, computed } from 'vue';
|
||||||
|
// import mime from 'mime';
|
||||||
|
|
||||||
|
const emit = defineEmits(['onSelectResult', 'onClearInput'])
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
borderless: Boolean,
|
||||||
|
transparent: Boolean,
|
||||||
|
mimeTypes: {
|
||||||
|
type: Array as () => string[],
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
// form: Object,
|
||||||
|
// isValidMimeType: Function,
|
||||||
|
});
|
||||||
|
|
||||||
|
const newExtension = ref('');
|
||||||
|
const showDropdown = ref(false);
|
||||||
|
const filteredMimetypes = ref<string[]>([]);
|
||||||
|
const selectedIndex: Ref<number> = ref(0);
|
||||||
|
const ul: Ref<HTMLLIElement[] | null> = ref<HTMLLIElement[]>([]);
|
||||||
|
|
||||||
|
watch(selectedIndex, (selectedIndex: number) => {
|
||||||
|
if (selectedIndex != null && ul.value != null) {
|
||||||
|
const currentElement: HTMLLIElement = ul.value[selectedIndex];
|
||||||
|
currentElement &&
|
||||||
|
currentElement?.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'nearest',
|
||||||
|
inline: 'start',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const inputElClass = computed(() => {
|
||||||
|
const base = [
|
||||||
|
'block p-2.5 w-full z-20 text-sm text-gray-900 bg-gray-50 rounded-r-lg',
|
||||||
|
'dark:placeholder-gray-400 dark:text-white dark:focus:border-blue-500',
|
||||||
|
'h-12',
|
||||||
|
props.borderless ? 'border-0' : 'border',
|
||||||
|
props.transparent ? 'bg-transparent' : 'bg-white dark:bg-slate-800',
|
||||||
|
];
|
||||||
|
base.push('pl-10');
|
||||||
|
return base;
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInputChange = (e: Event) => {
|
||||||
|
const target = <HTMLInputElement>e.target;
|
||||||
|
newExtension.value = target.value;
|
||||||
|
|
||||||
|
if (newExtension.value.length >= 2) {
|
||||||
|
showDropdown.value = true;
|
||||||
|
filteredMimetypes.value = props.mimeTypes.filter(mimeType =>
|
||||||
|
mimeType.toLowerCase().includes(newExtension.value.toLowerCase())
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
showDropdown.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectResult = (mimeType: string) => {
|
||||||
|
showDropdown.value = false;
|
||||||
|
newExtension.value = '';
|
||||||
|
selectedIndex.value = -1;
|
||||||
|
emit('onSelectResult', mimeType);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearInput = () => {
|
||||||
|
newExtension.value = '';
|
||||||
|
showDropdown.value = false;
|
||||||
|
// props.form.name = '';
|
||||||
|
// props.resetFileExtensions();
|
||||||
|
emit('onClearInput');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onArrowDown = () => {
|
||||||
|
if (filteredMimetypes.value.length > 0) {
|
||||||
|
selectedIndex.value = selectedIndex.value === filteredMimetypes.value.length - 1 ? 0 : selectedIndex.value + 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onArrowUp = () => {
|
||||||
|
if (filteredMimetypes.value.length > 0) {
|
||||||
|
selectedIndex.value = selectedIndex.value == 0 || selectedIndex.value == -1 ? filteredMimetypes.value.length - 1 : selectedIndex.value - 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const onEnter = () => {
|
||||||
|
if (Array.isArray(filteredMimetypes.value) && filteredMimetypes.value.length && selectedIndex.value !== -1 && selectedIndex.value < filteredMimetypes.value.length) {
|
||||||
|
const mimeType = filteredMimetypes.value[selectedIndex.value];
|
||||||
|
selectResult(mimeType);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
|
@ -21,12 +21,11 @@ import {
|
||||||
mdiFormatListGroup,
|
mdiFormatListGroup,
|
||||||
mdiFormatListNumbered,
|
mdiFormatListNumbered,
|
||||||
mdiLogout,
|
mdiLogout,
|
||||||
mdiGithub,
|
|
||||||
mdiThemeLightDark,
|
mdiThemeLightDark,
|
||||||
mdiViewDashboard,
|
mdiViewDashboard,
|
||||||
mdiMapSearch,
|
|
||||||
mdiInformationVariant,
|
mdiInformationVariant,
|
||||||
mdiGlasses,
|
mdiGlasses,
|
||||||
|
mdiXml
|
||||||
} from '@mdi/js';
|
} from '@mdi/js';
|
||||||
import NavBarItem from '@/Components/NavBarItem.vue';
|
import NavBarItem from '@/Components/NavBarItem.vue';
|
||||||
import NavBarItemLabel from '@/Components/NavBarItemLabel.vue';
|
import NavBarItemLabel from '@/Components/NavBarItemLabel.vue';
|
||||||
|
@ -72,7 +71,7 @@ const menuNavBarToggle = () => {
|
||||||
const menuOpenLg = () => {
|
const menuOpenLg = () => {
|
||||||
layoutStore.isAsideLgActive = true;
|
layoutStore.isAsideLgActive = true;
|
||||||
};
|
};
|
||||||
const userHasRoles = (roleNames): boolean => {
|
const userHasRoles = (roleNames: Array<string>): boolean => {
|
||||||
return user.value.roles.some(role => roleNames.includes(role.name));
|
return user.value.roles.some(role => roleNames.includes(role.name));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -95,12 +94,13 @@ const showAbout = async () => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<nav class="text-base top-0 left-0 right-0 fixed bg-gray-50 h-14 z-40 w-screen transition-position lg:w-auto dark:bg-slate-800"
|
<nav class="text-base top-0 left-0 right-0 fixed bg-lime h-14 z-50 w-screen transition-position lg:w-auto dark:bg-slate-800"
|
||||||
:class="{ 'xl:pl-60': props.showBurger == true }">
|
:class="{ 'xl:pl-60': props.showBurger == true }">
|
||||||
<FirstrunWizard ref="about"></FirstrunWizard>
|
<FirstrunWizard ref="about"></FirstrunWizard>
|
||||||
<div class="flex lg:items-stretch" :class="containerMaxW">
|
<div class="flex lg:items-stretch" :class="containerMaxW">
|
||||||
<div class="flex-1 items-stretch flex h-14">
|
<div class="flex-1 items-stretch flex h-14">
|
||||||
<NavBarItem type="flex lg:hidden" @click.prevent="layoutStore.asideMobileToggle()" v-if="props.showBurger">
|
<NavBarItem type="flex lg:hidden" @click.prevent="layoutStore.asideMobileToggle()"
|
||||||
|
v-if="props.showBurger">
|
||||||
<BaseIcon :path="layoutStore.isAsideMobileExpanded ? mdiBackburger : mdiForwardburger" size="24" />
|
<BaseIcon :path="layoutStore.isAsideMobileExpanded ? mdiBackburger : mdiForwardburger" size="24" />
|
||||||
</NavBarItem>
|
</NavBarItem>
|
||||||
<NavBarItem type="hidden lg:flex xl:hidden" @click.prevent="menuOpenLg" v-if="props.showBurger">
|
<NavBarItem type="hidden lg:flex xl:hidden" @click.prevent="menuOpenLg" v-if="props.showBurger">
|
||||||
|
@ -110,9 +110,9 @@ const showAbout = async () => {
|
||||||
<NavBarItemLabel :icon="mdiViewDashboard" label="Dashboard" size="22" is-hover-label-only
|
<NavBarItemLabel :icon="mdiViewDashboard" label="Dashboard" size="22" is-hover-label-only
|
||||||
route-name="apps.dashboard" />
|
route-name="apps.dashboard" />
|
||||||
</NavBarItem>
|
</NavBarItem>
|
||||||
<NavBarItem route-name="apps.map">
|
<!-- <NavBarItem route-name="apps.map">
|
||||||
<NavBarItemLabel :icon="mdiMapSearch" label="Map" size="22" is-hover-label-only route-name="apps.map" />
|
<NavBarItemLabel :icon="mdiMapSearch" label="Map" size="22" is-hover-label-only route-name="apps.map" />
|
||||||
</NavBarItem>
|
</NavBarItem> -->
|
||||||
<!-- <NavBarItem>
|
<!-- <NavBarItem>
|
||||||
<NavBarSearch />
|
<NavBarSearch />
|
||||||
</NavBarItem> -->
|
</NavBarItem> -->
|
||||||
|
@ -122,10 +122,10 @@ const showAbout = async () => {
|
||||||
<BaseIcon :path="isMenuNavBarActive ? mdiClose : mdiDotsVertical" size="24" />
|
<BaseIcon :path="isMenuNavBarActive ? mdiClose : mdiDotsVertical" size="24" />
|
||||||
</NavBarItem>
|
</NavBarItem>
|
||||||
</div>
|
</div>
|
||||||
<div class="absolute w-screen top-14 left-0 bg-gray-50 shadow lg:w-auto lg:items-stretch lg:flex lg:grow lg:static lg:border-b-0 lg:overflow-visible lg:shadow-none dark:bg-slate-800"
|
<div class="fixed w-screen top-14 left-0 shadow lg:w-auto lg:items-stretch lg:flex lg:grow lg:static lg:border-b-0 lg:overflow-visible lg:shadow-none dark:bg-slate-800"
|
||||||
:class="[isMenuNavBarActive ? 'block' : 'hidden']">
|
:class="[isMenuNavBarActive ? 'block' : 'hidden']">
|
||||||
<div
|
<div
|
||||||
class="max-h-screen-menu overflow-y-auto lg:overflow-visible lg:flex lg:items-stretch lg:justify-end lg:ml-auto">
|
class="bg-white lg:bg-lime dark:bg-transparent max-h-screen-menu overflow-y-auto lg:overflow-visible lg:flex lg:items-stretch lg:justify-end lg:ml-auto">
|
||||||
|
|
||||||
<!-- help menu -->
|
<!-- help menu -->
|
||||||
<NavBarMenu>
|
<NavBarMenu>
|
||||||
|
@ -150,7 +150,7 @@ const showAbout = async () => {
|
||||||
<!-- personal menu -->
|
<!-- personal menu -->
|
||||||
<NavBarMenu>
|
<NavBarMenu>
|
||||||
<NavBarItemLabel v-bind:label="`hello ${user.login}`">
|
<NavBarItemLabel v-bind:label="`hello ${user.login}`">
|
||||||
<UserAvatarCurrentUser class="w-6 h-6 mr-3 inline-flex" />
|
<UserAvatarCurrentUser :user="user" class="w-6 h-6 mr-3 inline-flex" />
|
||||||
</NavBarItemLabel>
|
</NavBarItemLabel>
|
||||||
<template #dropdown>
|
<template #dropdown>
|
||||||
<!-- <NavBarItem> -->
|
<!-- <NavBarItem> -->
|
||||||
|
@ -170,12 +170,9 @@ const showAbout = async () => {
|
||||||
<NavBarItem v-if="userHasRoles(['reviewer'])" :route-name="'reviewer.dataset.list'">
|
<NavBarItem v-if="userHasRoles(['reviewer'])" :route-name="'reviewer.dataset.list'">
|
||||||
<NavBarItemLabel :icon="mdiGlasses" label="Reviewer Menu" />
|
<NavBarItemLabel :icon="mdiGlasses" label="Reviewer Menu" />
|
||||||
</NavBarItem>
|
</NavBarItem>
|
||||||
<!-- <NavBarItem>
|
<!-- <NavBarItem @click="showAbout">
|
||||||
<NavBarItemLabel :icon="mdiEmail" label="Messages" />
|
|
||||||
</NavBarItem> -->
|
|
||||||
<NavBarItem @click="showAbout">
|
|
||||||
<NavBarItemLabel :icon="mdiInformationVariant" label="About" />
|
<NavBarItemLabel :icon="mdiInformationVariant" label="About" />
|
||||||
</NavBarItem>
|
</NavBarItem> -->
|
||||||
<BaseDivider nav-bar />
|
<BaseDivider nav-bar />
|
||||||
<NavBarItem @click="logout">
|
<NavBarItem @click="logout">
|
||||||
<NavBarItemLabel :icon="mdiLogout" label="Log Out" />
|
<NavBarItemLabel :icon="mdiLogout" label="Log Out" />
|
||||||
|
@ -186,12 +183,15 @@ const showAbout = async () => {
|
||||||
<NavBarItem is-desktop-icon-only @click.prevent="toggleLightDark">
|
<NavBarItem is-desktop-icon-only @click.prevent="toggleLightDark">
|
||||||
<NavBarItemLabel v-bind:icon="mdiThemeLightDark" label="Light/Dark" is-desktop-icon-only />
|
<NavBarItemLabel v-bind:icon="mdiThemeLightDark" label="Light/Dark" is-desktop-icon-only />
|
||||||
</NavBarItem>
|
</NavBarItem>
|
||||||
<NavBarItem href="https://gitea.geologie.ac.at/geolba/tethys" target="_blank" is-desktop-icon-only>
|
<!-- <NavBarItem href="" target="_blank" is-desktop-icon-only>
|
||||||
<NavBarItemLabel v-bind:icon="mdiGithub" label="GitHub" is-desktop-icon-only />
|
<NavBarItemLabel v-bind:icon="mdiGithub" label="GitHub" is-desktop-icon-only />
|
||||||
|
</NavBarItem> -->
|
||||||
|
<NavBarItem href="/oai" target="_blank" is-desktop-icon-only>
|
||||||
|
<NavBarItemLabel v-bind:icon="mdiXml" label="OAI Interface" is-desktop-icon-only />
|
||||||
</NavBarItem>
|
</NavBarItem>
|
||||||
<NavBarItem is-desktop-icon-only @click="showAbout">
|
<!-- <NavBarItem is-desktop-icon-only @click="showAbout">
|
||||||
<NavBarItemLabel v-bind:icon="mdiInformationVariant" label="About" is-desktop-icon-only />
|
<NavBarItemLabel v-bind:icon="mdiInformationVariant" label="About" is-desktop-icon-only />
|
||||||
</NavBarItem>
|
</NavBarItem> -->
|
||||||
<NavBarItem is-desktop-icon-only @click="logout">
|
<NavBarItem is-desktop-icon-only @click="logout">
|
||||||
<NavBarItemLabel v-bind:icon="mdiLogout" label="Log out" is-desktop-icon-only />
|
<NavBarItemLabel v-bind:icon="mdiLogout" label="Log out" is-desktop-icon-only />
|
||||||
</NavBarItem>
|
</NavBarItem>
|
||||||
|
|