Merge branch 'development'
This commit is contained in:
commit
842b3a10b9
46 changed files with 1236 additions and 1201 deletions
4
Services/client/.gitignore
vendored
4
Services/client/.gitignore
vendored
|
|
@ -26,3 +26,7 @@ coverage
|
|||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
/test-results/
|
||||
/playwright-report/
|
||||
/blob-report/
|
||||
/playwright/.cache/
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
"singleQuote": true,
|
||||
"trailingComma": "all"
|
||||
}
|
||||
|
|
@ -1,16 +1,17 @@
|
|||
# EDM Frontend
|
||||
Enterprise Document Management (EDM) ส่วน frontend
|
||||
|
||||
Enterprise Document Management (EDM) ส่วน frontend
|
||||
|
||||
# ส่วนประกอบ
|
||||
|
||||
- Vue.js (TypeScript) ใช้ Quasar Framework เป็น UI Framework หลักสำหรับการพัฒนา
|
||||
- pnpm เป็น package manager
|
||||
- Library Document ใช้ typedoc, typedoc-plugin-vue
|
||||
|
||||
```
|
||||
pnpm i -D typedoc typedoc-plugin-vue
|
||||
```
|
||||
|
||||
|
||||
สำหรับ User
|
||||
|
||||
## โครงสร้างโฟลเดอร์
|
||||
|
|
@ -91,20 +92,8 @@ npm run build
|
|||
npm run test:unit
|
||||
```
|
||||
|
||||
### Run End-to-End Tests with [Cypress](https://www.cypress.io/)
|
||||
|
||||
```sh
|
||||
npm run test:e2e:dev
|
||||
```
|
||||
|
||||
This runs the end-to-end tests against the Vite development server.
|
||||
It is much faster than the production build.
|
||||
|
||||
But it's still recommended to test the production build with `test:e2e` before deploying (e.g. in CI environments):
|
||||
|
||||
```sh
|
||||
npm run build
|
||||
npm run test:e2e
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
import { defineConfig } from 'cypress'
|
||||
|
||||
export default defineConfig({
|
||||
e2e: {
|
||||
specPattern: 'cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}',
|
||||
baseUrl: 'http://localhost:4173',
|
||||
},
|
||||
})
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
// https://docs.cypress.io/api/introduction/api.html
|
||||
|
||||
describe('My First Test', () => {
|
||||
it('visits the app root url', () => {
|
||||
cy.visit('/')
|
||||
cy.contains('h1', 'You did it!')
|
||||
})
|
||||
})
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.web.json",
|
||||
"include": ["./**/*", "../support/**/*"],
|
||||
"compilerOptions": {
|
||||
"isolatedModules": false,
|
||||
"target": "es5",
|
||||
"lib": ["es5", "dom"],
|
||||
"types": ["cypress"]
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +0,0 @@
|
|||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
|
|
@ -1,39 +0,0 @@
|
|||
/// <reference types="cypress" />
|
||||
// ***********************************************
|
||||
// This example commands.ts shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add('login', (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... })
|
||||
//
|
||||
// declare global {
|
||||
// namespace Cypress {
|
||||
// interface Chainable {
|
||||
// login(email: string, password: string): Chainable<void>
|
||||
// drag(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// dismiss(subject: string, options?: Partial<TypeOptions>): Chainable<Element>
|
||||
// visit(originalFn: CommandOriginalFn, url: string, options: Partial<VisitOptions>): Chainable<Element>
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
export {}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
<!DOCTYPE html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
|
|
|
|||
|
|
@ -8,8 +8,6 @@
|
|||
"build": "run-p type-check build-only",
|
||||
"preview": "vite preview",
|
||||
"test:unit": "vitest --environment jsdom --root src/",
|
||||
"test:e2e": "start-server-and-test preview :4173 'cypress run --e2e'",
|
||||
"test:e2e:dev": "start-server-and-test 'vite dev --port 4173' :4173 'cypress open --e2e'",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
|
||||
|
|
@ -22,6 +20,7 @@
|
|||
"@tsconfig/node18": "^18.2.2",
|
||||
"axios": "^1.6.2",
|
||||
"keycloak-js": "^23.0.0",
|
||||
"nanoid": "^5.0.4",
|
||||
"pinia": "^2.1.7",
|
||||
"quasar": "^2.14.0",
|
||||
"vite-plugin-pwa": "^0.17.2",
|
||||
|
|
@ -29,6 +28,7 @@
|
|||
"vue-router": "^4.2.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@playwright/test": "^1.40.1",
|
||||
"@quasar/vite-plugin": "^1.6.0",
|
||||
"@rushstack/eslint-patch": "^1.6.0",
|
||||
"@types/jsdom": "^21.1.6",
|
||||
|
|
@ -39,15 +39,12 @@
|
|||
"@vue/eslint-config-typescript": "^12.0.0",
|
||||
"@vue/test-utils": "^2.4.2",
|
||||
"@vue/tsconfig": "^0.4.0",
|
||||
"cypress": "^13.6.0",
|
||||
"eslint": "^8.54.0",
|
||||
"eslint-plugin-cypress": "^2.15.1",
|
||||
"eslint-plugin-vue": "^9.18.1",
|
||||
"jsdom": "^23.0.0",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"prettier": "^3.1.0",
|
||||
"sass": "^1.69.5",
|
||||
"start-server-and-test": "^2.0.3",
|
||||
"typedoc": "^0.25.4",
|
||||
"typedoc-plugin-vue": "^1.1.0",
|
||||
"typescript": "~5.3.2",
|
||||
|
|
|
|||
77
Services/client/playwright.config.ts
Normal file
77
Services/client/playwright.config.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
import { defineConfig, devices } from '@playwright/test';
|
||||
|
||||
/**
|
||||
* Read environment variables from file.
|
||||
* https://github.com/motdotla/dotenv
|
||||
*/
|
||||
// require('dotenv').config();
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
*/
|
||||
export default defineConfig({
|
||||
testDir: './tests',
|
||||
/* Run tests in files in parallel */
|
||||
fullyParallel: true,
|
||||
/* Fail the build on CI if you accidentally left test.only in the source code. */
|
||||
forbidOnly: !!process.env.CI,
|
||||
/* Retry on CI only */
|
||||
retries: process.env.CI ? 2 : 0,
|
||||
/* Opt out of parallel tests on CI. */
|
||||
workers: process.env.CI ? 1 : undefined,
|
||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||
reporter: 'html',
|
||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||
use: {
|
||||
/* Base URL to use in actions like `await page.goto('/')`. */
|
||||
// baseURL: 'http://127.0.0.1:3000',
|
||||
|
||||
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
|
||||
trace: 'on-first-retry',
|
||||
},
|
||||
|
||||
/* Configure projects for major browsers */
|
||||
projects: [
|
||||
{
|
||||
name: 'chromium',
|
||||
use: { ...devices['Desktop Chrome'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'firefox',
|
||||
use: { ...devices['Desktop Firefox'] },
|
||||
},
|
||||
|
||||
{
|
||||
name: 'webkit',
|
||||
use: { ...devices['Desktop Safari'] },
|
||||
},
|
||||
|
||||
/* Test against mobile viewports. */
|
||||
// {
|
||||
// name: 'Mobile Chrome',
|
||||
// use: { ...devices['Pixel 5'] },
|
||||
// },
|
||||
// {
|
||||
// name: 'Mobile Safari',
|
||||
// use: { ...devices['iPhone 12'] },
|
||||
// },
|
||||
|
||||
/* Test against branded browsers. */
|
||||
// {
|
||||
// name: 'Microsoft Edge',
|
||||
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
||||
// },
|
||||
// {
|
||||
// name: 'Google Chrome',
|
||||
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
||||
// },
|
||||
],
|
||||
|
||||
/* Run your local dev server before starting the tests */
|
||||
// webServer: {
|
||||
// command: 'npm run start',
|
||||
// url: 'http://127.0.0.1:3000',
|
||||
// reuseExistingServer: !process.env.CI,
|
||||
// },
|
||||
});
|
||||
933
Services/client/pnpm-lock.yaml
generated
933
Services/client/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,8 +1,8 @@
|
|||
{
|
||||
"realm": "EDM",
|
||||
"auth-server-url": "https://edm-id.frappet.synology.me/",
|
||||
"ssl-required": "external",
|
||||
"resource": "EDM-V1",
|
||||
"public-client": true,
|
||||
"confidential-port": 0
|
||||
}
|
||||
"realm": "EDM",
|
||||
"auth-server-url": "https://edm-id.frappet.synology.me/",
|
||||
"ssl-required": "external",
|
||||
"resource": "EDM-V1",
|
||||
"public-client": true,
|
||||
"confidential-port": 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,12 +12,12 @@ const props = withDefaults(
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<q-dialog
|
||||
persistent
|
||||
transition-show="scale"
|
||||
transition-hide="scale"
|
||||
:model-value="props.open"
|
||||
@update:model-value="(v) => $emit('update:open', v)"
|
||||
<q-dialog
|
||||
persistent
|
||||
transition-show="scale"
|
||||
transition-hide="scale"
|
||||
:model-value="props.open"
|
||||
@update:model-value="(v) => $emit('update:open', v)"
|
||||
>
|
||||
<q-card style="width: 400px">
|
||||
<q-card-section>
|
||||
|
|
@ -36,7 +36,7 @@ const props = withDefaults(
|
|||
label="ยกเลิก"
|
||||
flat
|
||||
v-close-popup
|
||||
@click="() => ($emit('update:open', !open))"
|
||||
@click="() => $emit('update:open', !open)"
|
||||
id="dialogDeleteClose"
|
||||
/>
|
||||
|
||||
|
|
|
|||
|
|
@ -190,7 +190,9 @@ const file = ref<File | undefined>()
|
|||
<div class="q-mt-md">
|
||||
<q-select
|
||||
outlined
|
||||
dense
|
||||
:model-value="category"
|
||||
label="กดปุ่มEnterเพื่อเพิ่ม"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
|
|
@ -200,7 +202,7 @@ const file = ref<File | undefined>()
|
|||
@filter="filterCategory"
|
||||
style="width: 250px"
|
||||
@update:model-value="(v) => $emit('update:category', v)"
|
||||
id="inputCategory"
|
||||
data-testid="filterDataCategory"
|
||||
>
|
||||
<template v-slot:no-option>
|
||||
<q-item>
|
||||
|
|
@ -216,6 +218,8 @@ const file = ref<File | undefined>()
|
|||
<div class="q-mt-md">
|
||||
<q-select
|
||||
outlined
|
||||
dense
|
||||
label="กดปุ่มEnterเพื่อเพิ่ม"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
|
|
@ -225,7 +229,7 @@ const file = ref<File | undefined>()
|
|||
:model-value="props.keyword"
|
||||
@update:model-value="(v) => $emit('update:keyword', v)"
|
||||
@new-value="createKeyword"
|
||||
id="inputKeyword"
|
||||
data-testid="filterDataKeyWord"
|
||||
>
|
||||
</q-select>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ const props = withDefaults(
|
|||
},
|
||||
)
|
||||
const DEPT_NAME = ['ตู้เอกสาร', 'ลิ้นชัก', 'แฟ้ม', 'แฟ้มย่อย'] as const
|
||||
const { getFileInfo, getFileNameFormat } = useFileInfoStore()
|
||||
const { getFileInfo } = useFileInfoStore()
|
||||
const { currentFolder, currentFile, currentDept, currentPath } =
|
||||
storeToRefs(useTreeDataStore())
|
||||
const {
|
||||
|
|
@ -183,7 +183,10 @@ async function submitFileForm(
|
|||
{{ DEPT_NAME[currentDept] }}
|
||||
</div>
|
||||
<div class="grid q-mt-md">
|
||||
<div v-for="value in currentFolder" key="value.name">
|
||||
<div
|
||||
v-for="(value, index) in currentFolder"
|
||||
:data-pathname="value.pathname"
|
||||
>
|
||||
<div
|
||||
:style="{
|
||||
position: 'relative',
|
||||
|
|
@ -215,6 +218,7 @@ async function submitFileForm(
|
|||
v-if="props.action"
|
||||
>
|
||||
<file-item-action
|
||||
:nameId="value.pathname"
|
||||
@delete="() => triggerFolderDelete(value.pathname)"
|
||||
@edit="() => triggerFolderEdit(value.name, value.pathname)"
|
||||
/>
|
||||
|
|
@ -243,6 +247,7 @@ async function submitFileForm(
|
|||
padding: currentDept > 2 ? '.5rem 0' : '.5rem',
|
||||
}"
|
||||
@click="() => triggerFolderCreate()"
|
||||
id="triggerFolderCreateFileItem"
|
||||
>
|
||||
<div
|
||||
class="q-px-md flex items-center justify-center"
|
||||
|
|
@ -286,7 +291,7 @@ async function submitFileForm(
|
|||
เอกสาร
|
||||
</div>
|
||||
<div class="grid q-mt-md">
|
||||
<div v-for="value in currentFile">
|
||||
<div v-for="(value, index) in currentFile" :data-pathname="value.pathname">
|
||||
<div
|
||||
:style="{
|
||||
position: 'relative',
|
||||
|
|
@ -299,6 +304,7 @@ async function submitFileForm(
|
|||
}"
|
||||
class="box"
|
||||
@click="() => getFileInfo(value)"
|
||||
:id="`getFileInfoFileItem${index}`"
|
||||
>
|
||||
<div class="q-px-md flex items-center justify-center">
|
||||
<file-icon
|
||||
|
|
@ -312,6 +318,7 @@ async function submitFileForm(
|
|||
v-if="props.action"
|
||||
>
|
||||
<file-item-action
|
||||
:nameId="value.pathname"
|
||||
@edit="
|
||||
() =>
|
||||
triggerFileEdit(
|
||||
|
|
@ -347,6 +354,7 @@ async function submitFileForm(
|
|||
}"
|
||||
class="dashed"
|
||||
@click="() => triggerFileCreate()"
|
||||
id="triggerFileCreateFileItem"
|
||||
>
|
||||
<div
|
||||
class="q-px-md flex items-center justify-center"
|
||||
|
|
|
|||
|
|
@ -1,22 +1,34 @@
|
|||
<script lang="ts" setup>
|
||||
defineEmits(['edit', 'delete'])
|
||||
|
||||
const props =
|
||||
defineProps<{
|
||||
nameId: string
|
||||
|
||||
}>()
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<q-btn @click.stop icon="more_vert" color="grey" flat dense>
|
||||
<q-btn @click.stop icon="more_vert" color="grey" flat dense :data-testid="`action${props.nameId}`">
|
||||
<q-menu auto-close>
|
||||
<q-list dense>
|
||||
<q-item clickable @click="() => $emit('edit')">
|
||||
<q-item clickable @click="() => $emit('edit')" id="FileltemActionEdit">
|
||||
<q-item-section>
|
||||
<div class="row items-center white ">
|
||||
<div class="row items-center white">
|
||||
<q-icon name="edit" color="positive" />
|
||||
<span class="q-ml-sm">แก้ไข</span>
|
||||
</div>
|
||||
</q-item-section>
|
||||
</q-item>
|
||||
<q-item clickable @click="() => $emit('delete')">
|
||||
<q-item
|
||||
clickable
|
||||
@click="() => $emit('delete')"
|
||||
id="FileltemActiondelete"
|
||||
>
|
||||
<q-item-section>
|
||||
<div class="row items-center white ">
|
||||
<div class="row items-center white">
|
||||
<q-icon name="delete" color="negative" />
|
||||
<span class="q-ml-sm">ลบ</span>
|
||||
</div>
|
||||
|
|
@ -27,15 +39,11 @@ defineEmits(['edit', 'delete'])
|
|||
</q-btn>
|
||||
</template>
|
||||
|
||||
|
||||
<style scoped>
|
||||
.white {
|
||||
.white {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
|
||||
display: block;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,21 +2,52 @@
|
|||
import { storeToRefs } from 'pinia'
|
||||
import { useSearchDataStore } from '@/stores/searched-data'
|
||||
import { useFileInfoStore } from '@/stores/file-info-data'
|
||||
import { useTreeDataStore } from '@/stores/tree-data'
|
||||
|
||||
import DialogDelete from './DialogDelete.vue'
|
||||
import UploadExistDialog from './UploadExistDialog.vue'
|
||||
import FileForm from './FileForm.vue'
|
||||
import FileItemAction from '@/components/FileItemAction.vue'
|
||||
import FileIcon from '@/components/FileIcon.vue'
|
||||
|
||||
import type { QTableProps } from 'quasar'
|
||||
import { onMounted, ref, watch } from 'vue'
|
||||
|
||||
defineProps<{
|
||||
viewMode: 'view_list' | 'view_module'
|
||||
}>()
|
||||
|
||||
const { foundFile } = storeToRefs(useSearchDataStore())
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
action: boolean
|
||||
viewMode: 'view_list' | 'view_module'
|
||||
}>(),
|
||||
{
|
||||
action: false,
|
||||
},
|
||||
)
|
||||
const { foundFile, isActFoundFile } = storeToRefs(useSearchDataStore())
|
||||
const { getFileInfo, getSize, getType } = useFileInfoStore()
|
||||
const { updateFile, deleteFile, checkFile } = useTreeDataStore()
|
||||
const keywordList = ref<string[]>([])
|
||||
const categoryList = ref<string[]>([])
|
||||
const selectKeyword = ref<string[]>([])
|
||||
const selectCategory = ref<string[]>([])
|
||||
const filterFoundFile = ref<any>()
|
||||
|
||||
const fileExistNotification = ref<boolean>(false)
|
||||
const fileFormError = ref<{ fileExist?: boolean }>({})
|
||||
const deleteFormType = ref<'deleteFile'>('deleteFile')
|
||||
const dialogDeleteState = ref<boolean>(false)
|
||||
const deleteFormPath = ref<string>('')
|
||||
|
||||
const fileFormType = ref<'edit'>('edit')
|
||||
const fileFormState = ref<boolean>(false)
|
||||
const fileFormPath = ref<string>('')
|
||||
const fileFormData = ref<{
|
||||
file?: File
|
||||
title?: string
|
||||
description?: string
|
||||
keyword?: string[]
|
||||
category?: string[]
|
||||
}>({})
|
||||
|
||||
const columns: QTableProps['columns'] = [
|
||||
{
|
||||
name: 'name',
|
||||
|
|
@ -53,6 +84,82 @@ const columns: QTableProps['columns'] = [
|
|||
},
|
||||
]
|
||||
|
||||
const currentParam = ref<Parameters<typeof submitFileForm>[0]>()
|
||||
|
||||
async function submitFileForm(
|
||||
value: {
|
||||
mode: 'create' | 'edit'
|
||||
file?: File
|
||||
title: string
|
||||
description: string
|
||||
keyword: string[]
|
||||
category: string[]
|
||||
},
|
||||
force = false,
|
||||
) {
|
||||
currentParam.value = value
|
||||
if (value.file && checkFile(value.file.name) && !force) {
|
||||
fileExistNotification.value = true
|
||||
return
|
||||
}
|
||||
|
||||
if (value.mode === 'edit') {
|
||||
await updateFile(
|
||||
fileFormPath.value,
|
||||
{
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
keyword: value.keyword,
|
||||
category: value.category,
|
||||
},
|
||||
value.file,
|
||||
)
|
||||
setTimeout(() => {
|
||||
isActFoundFile.value = true
|
||||
}, 300)
|
||||
}
|
||||
|
||||
fileFormData.value = {}
|
||||
fileFormState.value = false
|
||||
currentParam.value = undefined
|
||||
}
|
||||
|
||||
function triggerFileEdit(
|
||||
value: {
|
||||
title: string
|
||||
description: string
|
||||
keyword: string[]
|
||||
category: string[]
|
||||
},
|
||||
pathname: string,
|
||||
) {
|
||||
fileFormState.value = true
|
||||
fileFormType.value = 'edit'
|
||||
fileFormPath.value = pathname
|
||||
fileFormData.value = {
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
keyword: value.keyword,
|
||||
category: value.category,
|
||||
}
|
||||
}
|
||||
|
||||
function triggerFileDelete(pathname: string) {
|
||||
deleteFormType.value = 'deleteFile'
|
||||
deleteFormPath.value = pathname
|
||||
dialogDeleteState.value = !dialogDeleteState.value
|
||||
}
|
||||
|
||||
function confirmDelete() {
|
||||
if (deleteFormType) {
|
||||
deleteFile(deleteFormPath.value)
|
||||
|
||||
setTimeout(() => {
|
||||
isActFoundFile.value = true
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
function filterSearch() {
|
||||
function updateList() {
|
||||
keywordList.value = []
|
||||
|
|
@ -101,7 +208,7 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="row grid q-pt-md q-gutter-sm">
|
||||
<div class="row grid q-py-md q-gutter-sm">
|
||||
<q-select
|
||||
outlined
|
||||
dense
|
||||
|
|
@ -111,6 +218,7 @@ onMounted(() => {
|
|||
:options="keywordList"
|
||||
style="width: 100%"
|
||||
label="คำสำคัญ"
|
||||
class="custom-selection"
|
||||
/>
|
||||
<q-select
|
||||
outlined
|
||||
|
|
@ -124,7 +232,7 @@ onMounted(() => {
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="viewMode === 'view_list' && foundFile.length > 0">
|
||||
<div v-if="props.viewMode === 'view_list' && foundFile.length > 0">
|
||||
<div class="grid q-mt-md">
|
||||
<div v-for="(value, index) in filterFoundFile" :key="value.title">
|
||||
<div
|
||||
|
|
@ -138,7 +246,8 @@ onMounted(() => {
|
|||
maxWidth: '100%',
|
||||
}"
|
||||
class="box"
|
||||
@click="() => getFileInfo(filterFoundFile[index])"
|
||||
@click="() => getFileInfo(foundFile[index])"
|
||||
:id="`getFileInfoFileSearched${index}`"
|
||||
>
|
||||
<div class="q-px-md flex items-center justify-center">
|
||||
<file-icon
|
||||
|
|
@ -146,6 +255,28 @@ onMounted(() => {
|
|||
:fileMimeType="value.fileType ? value.fileType : 'unknow'"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="absolute"
|
||||
style="top: 0.5rem; right: 0.5rem"
|
||||
v-if="props.action"
|
||||
>
|
||||
<file-item-action
|
||||
:nameId="value.pathname"
|
||||
@edit="
|
||||
() =>
|
||||
triggerFileEdit(
|
||||
{
|
||||
title: value.title,
|
||||
description: value.description,
|
||||
keyword: value.keyword,
|
||||
category: value.category,
|
||||
},
|
||||
value.pathname,
|
||||
)
|
||||
"
|
||||
@delete="() => triggerFileDelete(value.pathname)"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="text-overflow-handle block q-px-md text-center"
|
||||
style="max-width: 100%"
|
||||
|
|
@ -157,7 +288,10 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="viewMode === 'view_module' && foundFile.length > 0">
|
||||
<div
|
||||
v-if="props.viewMode === 'view_module' && foundFile.length > 0"
|
||||
class="q-gutter-sm"
|
||||
>
|
||||
<q-table
|
||||
flat
|
||||
bordered
|
||||
|
|
@ -183,7 +317,7 @@ onMounted(() => {
|
|||
</q-td>
|
||||
</template>
|
||||
|
||||
<template v-slot:body-cell-actions="sizeData">
|
||||
<template v-slot:body-cell-actions="actionData">
|
||||
<q-td class="justify-center">
|
||||
<div>
|
||||
<q-icon class="q-ma-sm" name="info" size="2em" color="primary" />
|
||||
|
|
@ -192,9 +326,29 @@ onMounted(() => {
|
|||
self="center right"
|
||||
:offset="[5, 1]"
|
||||
>
|
||||
{{ getSize(sizeData.row.fileSize) }}
|
||||
{{ getSize(actionData.row.fileSize) }}
|
||||
</q-tooltip>
|
||||
</div>
|
||||
<div v-if="props.action">
|
||||
<q-btn
|
||||
flat
|
||||
color="positive"
|
||||
dense
|
||||
icon="edit"
|
||||
@click="
|
||||
() => triggerFileEdit(actionData.row, actionData.row.pathname)
|
||||
"
|
||||
id="listViewFileEdit"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
color="negative"
|
||||
dense
|
||||
icon="delete"
|
||||
@click="() => triggerFileDelete(actionData.row.pathname)"
|
||||
id="listViewFileDelete"
|
||||
/>
|
||||
</div>
|
||||
</q-td>
|
||||
</template>
|
||||
</q-table>
|
||||
|
|
@ -203,6 +357,26 @@ onMounted(() => {
|
|||
<div class="q-mt-md" v-if="foundFile.length == 0">
|
||||
<span>ไม่พบรายการที่ค้นหา</span>
|
||||
</div>
|
||||
|
||||
<file-form
|
||||
:mode="fileFormType"
|
||||
:error="fileFormError"
|
||||
v-model:open="fileFormState"
|
||||
v-model:title="fileFormData.title"
|
||||
v-model:description="fileFormData.description"
|
||||
v-model:keyword="fileFormData.keyword"
|
||||
v-model:category="fileFormData.category"
|
||||
@filechange="(name: string) => (fileFormError.fileExist = checkFile(name))"
|
||||
@submit="submitFileForm"
|
||||
/>
|
||||
|
||||
<upload-exist-dialog
|
||||
v-model:notification="fileExistNotification"
|
||||
@confirm="() => currentParam && submitFileForm(currentParam, true)"
|
||||
@cancel="() => (currentParam = undefined)"
|
||||
/>
|
||||
|
||||
<dialog-delete v-model:open="dialogDeleteState" @confirm="confirmDelete" />
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
|
@ -238,4 +412,10 @@ onMounted(() => {
|
|||
.grid .box {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.justify-center {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ onUnmounted(() => window.addEventListener('keydown', keydown))
|
|||
$emit('update:open', !open)
|
||||
}
|
||||
"
|
||||
id="folderFormIconClose"
|
||||
/>
|
||||
</q-toolbar>
|
||||
|
||||
|
|
@ -98,11 +99,12 @@ onUnmounted(() => window.addEventListener('keydown', keydown))
|
|||
offensiveWord = checkOffensiveWord(v as string)
|
||||
}
|
||||
"
|
||||
id="folderNameInput"
|
||||
/>
|
||||
</section>
|
||||
|
||||
<section :style="{ display: 'flex', gap: '.5rem' }">
|
||||
<q-btn label="บันทึก" type="submit" color="primary" />
|
||||
<q-btn label="บันทึก" type="submit" color="primary" id="FoldeSubmit" />
|
||||
<q-btn
|
||||
label="ยกเลิก"
|
||||
type="reset"
|
||||
|
|
@ -114,6 +116,7 @@ onUnmounted(() => window.addEventListener('keydown', keydown))
|
|||
$emit('update:open', false), reset()
|
||||
}
|
||||
"
|
||||
id="folderBtnCancel"
|
||||
/>
|
||||
</section>
|
||||
</q-form>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@ watch(visible, () => {
|
|||
v-close-popup
|
||||
label="ปิด"
|
||||
@click="() => (visible = !visible)"
|
||||
id="globalErrorClose"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
|
|
|
|||
|
|
@ -6,13 +6,13 @@ import { useTreeDataStore, type TreeDataFolder } from '@/stores/tree-data'
|
|||
import { useFileInfoStore } from '@/stores/file-info-data'
|
||||
import FileIcon from '@/components/FileIcon.vue'
|
||||
import DialogDelete from '@/components/DialogDelete.vue'
|
||||
import UploadExistDialog from './UploadExistDialog.vue'
|
||||
import FileForm from './FileForm.vue'
|
||||
import FolderForm from './FolderForm.vue'
|
||||
|
||||
const { getFormatDate, getSize, getType, getFileInfo } = useFileInfoStore()
|
||||
const { listDataFile, listDataFolder, currentDept, currentPath } = storeToRefs(
|
||||
useTreeDataStore()
|
||||
)
|
||||
const { listDataFile, listDataFolder, currentDept, currentPath } =
|
||||
storeToRefs(useTreeDataStore())
|
||||
const {
|
||||
createFolder,
|
||||
editFolder,
|
||||
|
|
@ -34,18 +34,18 @@ const currentLevel = computed(() =>
|
|||
currentDept.value === 0
|
||||
? 'ตู้จัดเก็บเอกสาร'
|
||||
: currentDept.value === 1
|
||||
? 'ลิ้นชัก'
|
||||
: currentDept.value === 2
|
||||
? 'แฟ้ม'
|
||||
: 'แฟ้มย่อย'
|
||||
? 'ลิ้นชัก'
|
||||
: currentDept.value === 2
|
||||
? 'แฟ้ม'
|
||||
: 'แฟ้มย่อย',
|
||||
)
|
||||
|
||||
const currentIcon = computed(() =>
|
||||
currentDept.value === 0
|
||||
? 'mdi-file-cabinet'
|
||||
: currentDept.value === 1
|
||||
? 'inbox'
|
||||
: 'o_folder_open'
|
||||
? 'inbox'
|
||||
: 'o_folder_open',
|
||||
)
|
||||
|
||||
const folderFormState = ref<boolean>(false)
|
||||
|
|
@ -121,7 +121,7 @@ function triggerFileEdit(
|
|||
keyword: string[]
|
||||
category: string[]
|
||||
},
|
||||
pathname: string
|
||||
pathname: string,
|
||||
) {
|
||||
fileFormState.value = true
|
||||
fileFormType.value = 'edit'
|
||||
|
|
@ -145,7 +145,7 @@ async function submitFileForm(
|
|||
keyword: string[]
|
||||
category: string[]
|
||||
},
|
||||
force = false
|
||||
force = false,
|
||||
) {
|
||||
currentParam.value = value
|
||||
|
||||
|
|
@ -170,7 +170,7 @@ async function submitFileForm(
|
|||
keyword: value.keyword,
|
||||
category: value.category,
|
||||
},
|
||||
value.file
|
||||
value.file,
|
||||
)
|
||||
}
|
||||
fileFormData.value = {}
|
||||
|
|
@ -263,14 +263,12 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => {
|
|||
<div
|
||||
class="flex flex-break d justify-between space-between"
|
||||
v-if="currentDept >= 1 && props.mode == 'admin' && currentDept != 4"
|
||||
|
||||
>
|
||||
<div>
|
||||
<span class="text-h6">{{ currentLevel }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<q-btn
|
||||
|
||||
outline
|
||||
push
|
||||
class="q-px-md q-ml-md q-py-sm"
|
||||
|
|
@ -280,6 +278,7 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => {
|
|||
dense
|
||||
icon="add"
|
||||
@click="() => triggerFolderCreate()"
|
||||
id="listViewFolderCreate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -294,7 +293,7 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => {
|
|||
:rows-per-page-options="[0]"
|
||||
@row-click="onRowClick"
|
||||
class="cursor"
|
||||
v-if=" currentDept != 4 "
|
||||
v-if="currentDept != 4"
|
||||
>
|
||||
<template v-slot:body-cell-name="nameRow">
|
||||
<q-td style="width: 50%">
|
||||
|
|
@ -330,9 +329,10 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => {
|
|||
@click.stop="
|
||||
triggerFolderEdit(
|
||||
actionsRow.row.name,
|
||||
actionsRow.row.pathname
|
||||
actionsRow.row.pathname,
|
||||
)
|
||||
"
|
||||
id="listViewFolderEdit"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
|
|
@ -340,6 +340,7 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => {
|
|||
dense
|
||||
icon="delete"
|
||||
@click.stop="triggerFolderDelete(actionsRow.row.pathname)"
|
||||
id="listViewFolderDelete"
|
||||
/>
|
||||
</div>
|
||||
</q-td>
|
||||
|
|
@ -366,6 +367,7 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => {
|
|||
dense
|
||||
icon="add"
|
||||
@click="() => triggerFileCreate()"
|
||||
id="listViewFileCreate"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -389,6 +391,7 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => {
|
|||
: getFolder(nameRow.row.pathname)
|
||||
}
|
||||
"
|
||||
id="listViewGetFileInfo"
|
||||
>
|
||||
<file-icon size="list" :fileMimeType="nameRow.row.fileType" />
|
||||
{{ nameRow.row.fileName }}
|
||||
|
|
@ -424,6 +427,7 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => {
|
|||
@click="
|
||||
() => triggerFileEdit(actionsRow.row, actionsRow.row.pathname)
|
||||
"
|
||||
id="listViewFileEdit"
|
||||
/>
|
||||
<q-btn
|
||||
flat
|
||||
|
|
@ -431,6 +435,7 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => {
|
|||
dense
|
||||
icon="delete"
|
||||
@click="() => triggerFileDelete(actionsRow.row.pathname)"
|
||||
id="listViewFileDelete"
|
||||
/>
|
||||
</div>
|
||||
</q-td>
|
||||
|
|
@ -460,6 +465,12 @@ const onRowClick = (evt: Event, row: TreeDataFolder, index: number) => {
|
|||
@submit="submitFolderForm"
|
||||
/>
|
||||
|
||||
<upload-exist-dialog
|
||||
v-model:notification="fileExistNotification"
|
||||
@confirm="() => currentParam && submitFileForm(currentParam, true)"
|
||||
@cancel="() => (currentParam = undefined)"
|
||||
/>
|
||||
|
||||
<dialog-delete
|
||||
v-model:open="dialogDeleteState"
|
||||
@confirm="
|
||||
|
|
|
|||
|
|
@ -23,7 +23,6 @@ const { data, currentDept, currentPath } = storeToRefs(useTreeDataStore())
|
|||
const { createFolder, getCabinet, gotoParent, getFolder } = useTreeDataStore()
|
||||
|
||||
const viewMode = ref<'view_list' | 'view_module'>('view_list')
|
||||
const inputSearch = ref<string>()
|
||||
const props = defineProps<{
|
||||
mode: 'admin' | 'user'
|
||||
}>()
|
||||
|
|
@ -69,8 +68,14 @@ onMounted(getCabinet)
|
|||
<div class="col-12 col-md-3">
|
||||
<div class="bg-white rounded-borders shadow-5">
|
||||
<div
|
||||
class="q-px-md q-py-sm text-primary bg-grey-1"
|
||||
class="q-px-md q-py-sm text-primary bg-grey-1 pointer"
|
||||
id="container-header"
|
||||
@click="
|
||||
() => {
|
||||
currentPath = ''
|
||||
getFolder(currentPath)
|
||||
}
|
||||
"
|
||||
>
|
||||
<span class="block q-my-sm text-weight-bold">ตู้จัดเก็บเอกสาร</span>
|
||||
</div>
|
||||
|
|
@ -95,29 +100,45 @@ onMounted(getCabinet)
|
|||
flat
|
||||
dense
|
||||
class="q-mr-sm q-px-sm"
|
||||
v-if="currentDept > 0 && isSearch === false"
|
||||
v-if="isSearch == true || currentDept > 0"
|
||||
@click="
|
||||
() => {
|
||||
folderFormState = false
|
||||
gotoParent()
|
||||
isSearch
|
||||
? (isSearch = false)
|
||||
: ((folderFormState = false), gotoParent())
|
||||
}
|
||||
"
|
||||
>
|
||||
<q-icon name="arrow_back" size="1rem" color="primary" />
|
||||
</q-btn>
|
||||
<span v-if="isSearch === true">ผลการค้นหา</span>
|
||||
<q-breadcrumbs v-if="isSearch === false" active-color="primary">
|
||||
<q-breadcrumbs-el
|
||||
v-if="currentPath === '/' || !currentPath"
|
||||
label="ตู้เอกสารทั้งหมด"
|
||||
/>
|
||||
<q-breadcrumbs-el
|
||||
class="text-black"
|
||||
v-for="fragments in currentPath.split('/').filter(Boolean)"
|
||||
class="text-primary pointer"
|
||||
v-for="(fragments, index) in currentPath
|
||||
.split('/')
|
||||
.filter(Boolean)"
|
||||
:label="fragments"
|
||||
@click="
|
||||
() => {
|
||||
currentPath =
|
||||
currentPath
|
||||
.split('/')
|
||||
.filter(Boolean)
|
||||
.slice(0, index + 1)
|
||||
.join('/') + '/'
|
||||
|
||||
getFolder(currentPath)
|
||||
}
|
||||
"
|
||||
/>
|
||||
</q-breadcrumbs>
|
||||
</div>
|
||||
<span v-if="isSearch === true">ผลการค้นหา</span>
|
||||
|
||||
<q-btn
|
||||
v-if="
|
||||
mode === 'admin' &&
|
||||
|
|
@ -160,7 +181,11 @@ onMounted(getCabinet)
|
|||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<file-searched :viewMode="viewMode" v-if="isSearch === true" />
|
||||
<file-searched
|
||||
:viewMode="viewMode"
|
||||
:action="props.mode === 'admin'"
|
||||
v-if="isSearch === true"
|
||||
/>
|
||||
<file-item
|
||||
:viewMode="viewMode"
|
||||
:action="props.mode === 'admin'"
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
divdivdivdivdivdiv
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import { useSearchDataStore } from '@/stores/searched-data'
|
||||
|
||||
const { isAdvSearchCall } = storeToRefs(useSearchDataStore())
|
||||
const { isAdvSearchCall, advSearchDataRow, advSearchDataField } =
|
||||
storeToRefs(useSearchDataStore())
|
||||
const optionsField = [
|
||||
{ label: 'ชื่อเรื่อง (title)', value: 'title' },
|
||||
{ label: 'คำสำคัญ (keyword)', value: 'keyword' },
|
||||
|
|
@ -16,26 +16,6 @@ const optionsOp = [
|
|||
{ label: 'และ', value: 'AND' },
|
||||
{ label: 'หรือ', value: 'OR' },
|
||||
]
|
||||
const advSearchDataRow = ref<
|
||||
{
|
||||
op: 'AND' | 'OR'
|
||||
field: 'title' | 'keyword'
|
||||
value: string
|
||||
}[]
|
||||
>([
|
||||
{
|
||||
op: 'AND',
|
||||
field: 'title',
|
||||
value: '',
|
||||
},
|
||||
])
|
||||
const advSearchDataField = ref<{
|
||||
keyword: string
|
||||
description: string
|
||||
}>({
|
||||
keyword: '',
|
||||
description: '',
|
||||
})
|
||||
const props = defineProps<{
|
||||
searchSubmit: Function
|
||||
submitSearchData: {
|
||||
|
|
@ -50,11 +30,6 @@ const props = defineProps<{
|
|||
}
|
||||
}>()
|
||||
|
||||
defineExpose({
|
||||
advSearchDataRow,
|
||||
advSearchDataField,
|
||||
})
|
||||
|
||||
function addAdvSearchData() {
|
||||
advSearchDataRow.value.push({
|
||||
op: 'AND',
|
||||
|
|
@ -75,7 +50,7 @@ function clearAdvSearchData() {
|
|||
},
|
||||
]
|
||||
advSearchDataField.value = {
|
||||
keyword: '',
|
||||
keyword: [],
|
||||
description: '',
|
||||
}
|
||||
}
|
||||
|
|
@ -108,6 +83,7 @@ function clearAdvSearchData() {
|
|||
color="red"
|
||||
icon="close"
|
||||
@click="clearAdvSearchData"
|
||||
id="clearAdvSearchData"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
@ -124,6 +100,7 @@ function clearAdvSearchData() {
|
|||
icon="mdi-plus"
|
||||
v-if="index === advSearchDataRow.length - 1"
|
||||
@click="addAdvSearchData"
|
||||
id="addAdvSearchData"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-4 col-md-2">
|
||||
|
|
@ -172,6 +149,7 @@ function clearAdvSearchData() {
|
|||
icon="mdi-trash-can-outline"
|
||||
color="red"
|
||||
@click="() => delAdvSearchData(index)"
|
||||
id="delAdvSearchData"
|
||||
>
|
||||
<q-tooltip
|
||||
class="bg-red"
|
||||
|
|
@ -190,13 +168,17 @@ function clearAdvSearchData() {
|
|||
|
||||
<div class="row q-col-gutter-md q-pb-md">
|
||||
<div class="col-12 col-md-5">
|
||||
<q-input
|
||||
id="advSearchKeyword"
|
||||
dense
|
||||
<q-select
|
||||
outlined
|
||||
placeholder="คำสำคัญ"
|
||||
@keydown.enter.prevent="searchSubmit()"
|
||||
dense
|
||||
v-model="advSearchDataField.keyword"
|
||||
placeholder="คำสำคัญ"
|
||||
use-input
|
||||
use-chips
|
||||
multiple
|
||||
hide-dropdown-icon
|
||||
input-debounce="0"
|
||||
new-value-mode="add"
|
||||
/>
|
||||
</div>
|
||||
<div class="col-12 col-md-grow">
|
||||
|
|
@ -220,6 +202,7 @@ function clearAdvSearchData() {
|
|||
label="ค้นหา"
|
||||
icon="mdi-magnify"
|
||||
@click="() => props.searchSubmit()"
|
||||
id="advSearchSubmit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,30 +9,39 @@ import { useFileInfoStore } from '@/stores/file-info-data'
|
|||
import FileIcon from '@/components/FileIcon.vue'
|
||||
|
||||
const { isFilePreview, fileInfo } = storeToRefs(useFileInfoStore())
|
||||
const { getType, getFormatDate, getSize, getFileNameFormat } =
|
||||
useFileInfoStore()
|
||||
const { getType, getFormatDate, getSize } = useFileInfoStore()
|
||||
|
||||
async function downloadSubmit(path: any) {
|
||||
const [cabinet, drawer, folder, file] = path.split('/')
|
||||
const formatPath = `/cabinet/${cabinet}/drawer/${drawer}/folder/${folder}/file/${file}`
|
||||
const res = await axiosClient.get<EhrFile & { download: string }>(
|
||||
`${import.meta.env.VITE_API_ENDPOINT}${formatPath}`,
|
||||
)
|
||||
await axios
|
||||
.get(res.data.download, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: res.data.fileType,
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
const a = document.createElement('a')
|
||||
a.href = window.URL.createObjectURL(r.data)
|
||||
a.download = res.data.fileName
|
||||
a.click()
|
||||
})
|
||||
async function downloadSubmit(path: string | undefined) {
|
||||
if (path) {
|
||||
let formatPath: string
|
||||
|
||||
if (path.split('/').length - 1 === 3) {
|
||||
const [cabinet, drawer, folder, file] = path.split('/')
|
||||
formatPath = `cabinet/${cabinet}/drawer/${drawer}/folder/${folder}/file/${file}`
|
||||
} else {
|
||||
const [cabinet, drawer, folder, subfolder, file] = path.split('/')
|
||||
formatPath = `cabinet/${cabinet}/drawer/${drawer}/folder/${folder}/subfolder/${subfolder}/file/${file}`
|
||||
}
|
||||
|
||||
const res = await axiosClient.get<EhrFile & { download: string }>(
|
||||
`${import.meta.env.VITE_API_ENDPOINT}${formatPath}`,
|
||||
)
|
||||
await axios
|
||||
.get(res.data.download, {
|
||||
method: 'GET',
|
||||
responseType: 'blob',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Accept: res.data.fileType,
|
||||
},
|
||||
})
|
||||
.then((r) => {
|
||||
const a = document.createElement('a')
|
||||
a.href = window.URL.createObjectURL(r.data)
|
||||
a.download = res.data.fileName
|
||||
a.click()
|
||||
})
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
@ -46,6 +55,7 @@ async function downloadSubmit(path: any) {
|
|||
dense
|
||||
class="q-mr-sm q-px-sm"
|
||||
@click="() => (isFilePreview = false)"
|
||||
id="goBackInfo"
|
||||
>
|
||||
<q-icon
|
||||
class="pointer"
|
||||
|
|
@ -95,6 +105,7 @@ async function downloadSubmit(path: any) {
|
|||
icon="mdi-download"
|
||||
class="q-py-sm"
|
||||
@click="() => downloadSubmit(fileInfo?.pathname)"
|
||||
id="downloadSubmit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import axiosClient from '@/services/HttpService'
|
||||
|
||||
|
|
@ -10,22 +10,21 @@ import { useLoader } from '@/stores/loader'
|
|||
import AdvancedSearch from '@/modules/01_user/components/AdvancedSearch.vue'
|
||||
|
||||
const loaderStore = useLoader()
|
||||
const { isSearch, isAdvSearchCall } = storeToRefs(useSearchDataStore())
|
||||
const {
|
||||
isSearch,
|
||||
isAdvSearchCall,
|
||||
isActFoundFile,
|
||||
searchData,
|
||||
advSearchDataField,
|
||||
advSearchDataRow,
|
||||
} = storeToRefs(useSearchDataStore())
|
||||
const { getFoundFile } = useSearchDataStore()
|
||||
const advSearchComp = ref<InstanceType<typeof AdvancedSearch>>()
|
||||
const optionsField = [
|
||||
{ label: 'ชื่อเรื่อง (title)', value: 'title' },
|
||||
{ label: 'คำสำคัญ (keyword)', value: 'keyword' },
|
||||
{ label: 'หมวดหมู่ (category)', value: 'category' },
|
||||
{ label: 'เนื้อหาในไฟล์ (content)', value: 'attachment.content' },
|
||||
]
|
||||
const searchData = ref<{
|
||||
field: string
|
||||
value: string
|
||||
}>({
|
||||
field: 'title',
|
||||
value: '',
|
||||
})
|
||||
const submitSearchData = ref<{
|
||||
AND: { field: string; value: string }[]
|
||||
OR: { field: string; value: string }[]
|
||||
|
|
@ -53,9 +52,9 @@ async function searchSubmit() {
|
|||
value: searchData.value.value,
|
||||
})
|
||||
|
||||
if (isAdvSearchCall.value && advSearchComp.value) {
|
||||
const advField = advSearchComp.value.advSearchDataField
|
||||
const advRow = advSearchComp.value.advSearchDataRow
|
||||
if (isAdvSearchCall.value) {
|
||||
let advField = advSearchDataField.value
|
||||
let advRow = advSearchDataRow.value
|
||||
|
||||
advRow.forEach((d: { field: string; value: string; op: string }) => {
|
||||
if (d.field && d.value.trim() !== '') {
|
||||
|
|
@ -63,11 +62,13 @@ async function searchSubmit() {
|
|||
submitSearchData.value[op].push({ field: d.field, value: d.value })
|
||||
}
|
||||
})
|
||||
if (advField.keyword.trim() !== '') {
|
||||
submitSearchData.value.AND.push({
|
||||
field: 'keyword',
|
||||
value: advField.keyword,
|
||||
})
|
||||
if (advField.keyword.length > 0) {
|
||||
for (let i = 0; i < advField.keyword.length; i++) {
|
||||
submitSearchData.value.AND.push({
|
||||
field: 'keyword',
|
||||
value: advField.keyword[i],
|
||||
})
|
||||
}
|
||||
}
|
||||
if (advField.description.trim() !== '') {
|
||||
submitSearchData.value.AND.push({
|
||||
|
|
@ -93,6 +94,18 @@ async function searchSubmit() {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => isActFoundFile.value,
|
||||
(edited) => {
|
||||
if (edited === true) {
|
||||
searchSubmit()
|
||||
setTimeout(() => {
|
||||
isActFoundFile.value = false
|
||||
}, 300)
|
||||
}
|
||||
},
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
@ -107,7 +120,9 @@ async function searchSubmit() {
|
|||
id="inputSearch"
|
||||
@keydown.enter.prevent="searchSubmit"
|
||||
>
|
||||
<template v-slot:append><q-icon name="search" /></template>
|
||||
<template v-slot:append
|
||||
><q-icon name="search" class="pointer" @click="searchSubmit"
|
||||
/></template>
|
||||
</q-input>
|
||||
</div>
|
||||
|
||||
|
|
@ -141,6 +156,7 @@ async function searchSubmit() {
|
|||
name="close"
|
||||
@click="() => ((searchData.value = ''), (isSearch = false))"
|
||||
class="cursor-pointer"
|
||||
id="clearSearchData"
|
||||
/>
|
||||
</template>
|
||||
</q-input>
|
||||
|
|
@ -151,7 +167,6 @@ async function searchSubmit() {
|
|||
<div class="row items-center justify-between q-gutter-y-md q-pt-sm">
|
||||
<div class="column col-grow">
|
||||
<advanced-search
|
||||
ref="advSearchComp"
|
||||
:searchSubmit="searchSubmit"
|
||||
:submit-search-data="submitSearchData"
|
||||
/>
|
||||
|
|
@ -163,6 +178,7 @@ async function searchSubmit() {
|
|||
label="ค้นหา"
|
||||
icon="mdi-magnify"
|
||||
@click="searchSubmit"
|
||||
id="searchSubmit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -171,3 +187,9 @@ async function searchSubmit() {
|
|||
<q-separator />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@ export default [
|
|||
{
|
||||
path: '/',
|
||||
name: 'UserHomePage',
|
||||
component: () => import('@/modules/01_user/views/homePage.vue')
|
||||
}
|
||||
component: () => import('@/modules/01_user/views/homePage.vue'),
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -4,6 +4,6 @@ export default [
|
|||
{
|
||||
path: '/admin',
|
||||
name: 'AdminHomePage',
|
||||
component: () => import('@/modules/02_admin/views/homePage.vue')
|
||||
}
|
||||
component: () => import('@/modules/02_admin/views/homePage.vue'),
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -2,10 +2,42 @@ import { ref } from 'vue'
|
|||
import { defineStore } from 'pinia'
|
||||
import type { EhrFile } from '@/stores/tree-data'
|
||||
|
||||
export interface searchData {
|
||||
field: string
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface advSearchDataRow {
|
||||
op: 'AND' | 'OR'
|
||||
field: 'title' | 'keyword'
|
||||
value: string
|
||||
}
|
||||
|
||||
export interface advSearchDataField {
|
||||
keyword: string[]
|
||||
description: string
|
||||
}
|
||||
|
||||
export const useSearchDataStore = defineStore('searched', () => {
|
||||
const foundFile = ref<EhrFile[]>([])
|
||||
const isAdvSearchCall = ref<boolean>(false)
|
||||
const isSearch = ref<Boolean>(false)
|
||||
const isActFoundFile = ref<Boolean>(false)
|
||||
const searchData = ref<searchData>({
|
||||
field: 'title',
|
||||
value: '',
|
||||
})
|
||||
const advSearchDataRow = ref<advSearchDataRow[]>([
|
||||
{
|
||||
op: 'AND',
|
||||
field: 'title',
|
||||
value: '',
|
||||
},
|
||||
])
|
||||
const advSearchDataField = ref<advSearchDataField>({
|
||||
keyword: [],
|
||||
description: '',
|
||||
})
|
||||
|
||||
async function getFoundFile(data: EhrFile[]) {
|
||||
foundFile.value = data
|
||||
|
|
@ -15,6 +47,10 @@ export const useSearchDataStore = defineStore('searched', () => {
|
|||
foundFile,
|
||||
isSearch,
|
||||
isAdvSearchCall,
|
||||
isActFoundFile,
|
||||
searchData,
|
||||
advSearchDataRow,
|
||||
advSearchDataField,
|
||||
getFoundFile,
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export const useTreeDataStore = defineStore('changeCabinet', () => {
|
|||
currentPath.value = pathname
|
||||
|
||||
const res = await axiosClient.get<EhrFolder[]>(
|
||||
`${apiEndpoint}${requestPath}`
|
||||
`${apiEndpoint}${requestPath}`,
|
||||
)
|
||||
|
||||
const list = res.data.map((v) => ({
|
||||
|
|
@ -245,7 +245,7 @@ export const useTreeDataStore = defineStore('changeCabinet', () => {
|
|||
description: string
|
||||
keyword: string[]
|
||||
category: string[]
|
||||
}
|
||||
},
|
||||
) {
|
||||
loader.show()
|
||||
|
||||
|
|
@ -265,7 +265,7 @@ export const useTreeDataStore = defineStore('changeCabinet', () => {
|
|||
{
|
||||
file: file.name,
|
||||
...metadata,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
if (res && res.data.upload) {
|
||||
|
|
@ -294,7 +294,7 @@ export const useTreeDataStore = defineStore('changeCabinet', () => {
|
|||
keyword: string[]
|
||||
category: string[]
|
||||
},
|
||||
file?: File
|
||||
file?: File,
|
||||
) {
|
||||
loader.show()
|
||||
|
||||
|
|
@ -309,7 +309,7 @@ export const useTreeDataStore = defineStore('changeCabinet', () => {
|
|||
|
||||
const res = await axiosClient.patch<{ upload: string }>(
|
||||
`${apiEndpoint}${requestPath}`,
|
||||
{ file: file?.name, ...metadata }
|
||||
{ file: file?.name, ...metadata },
|
||||
)
|
||||
|
||||
if (res && res.data.upload) {
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
<script lang="ts">
|
||||
import { defineComponent } from "vue";
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: "Error404NotFound",
|
||||
});
|
||||
name: 'Error404NotFound',
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
|
|
|||
|
|
@ -2,7 +2,9 @@
|
|||
import { useLoader } from '@/stores/loader'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import profile from '@/components/Profile.vue'
|
||||
|
||||
import { useTreeDataStore } from '@/stores/tree-data'
|
||||
const { currentPath } = storeToRefs(useTreeDataStore())
|
||||
const { getFolder } = useTreeDataStore()
|
||||
const loaderStore = useLoader()
|
||||
const { loader } = storeToRefs(loaderStore)
|
||||
</script>
|
||||
|
|
@ -12,11 +14,27 @@ const { loader } = storeToRefs(loaderStore)
|
|||
<q-header class="bg-white text-black" bordered>
|
||||
<q-toolbar class="q-py-sm">
|
||||
<q-img
|
||||
class="pointer"
|
||||
src="@/assets/logo.png"
|
||||
spinner-color="white"
|
||||
style="height: 32px; max-width: 32px"
|
||||
@click="
|
||||
() => {
|
||||
currentPath = ''
|
||||
getFolder(currentPath)
|
||||
}
|
||||
"
|
||||
/>
|
||||
<div class="column q-px-md" id="app-toolbar-title">
|
||||
<div
|
||||
class="column q-px-md pointer"
|
||||
id="app-toolbar-title"
|
||||
@click="
|
||||
() => {
|
||||
currentPath = ''
|
||||
getFolder(currentPath)
|
||||
}
|
||||
"
|
||||
>
|
||||
<span class="text-body1">ระบบทรัพยากรบุคคล</span>
|
||||
<span class="text-caption text-grey">
|
||||
จัดเก็บข้อมูลผลการประเมิน
|
||||
|
|
@ -41,4 +59,8 @@ const { loader } = storeToRefs(loaderStore)
|
|||
.q-layout {
|
||||
background: var(--q-secondary);
|
||||
}
|
||||
|
||||
.pointer {
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
437
Services/client/tests-examples/demo-todo-app.spec.ts
Normal file
437
Services/client/tests-examples/demo-todo-app.spec.ts
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
import { test, expect, type Page } from '@playwright/test';
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto('https://demo.playwright.dev/todomvc');
|
||||
});
|
||||
|
||||
const TODO_ITEMS = [
|
||||
'buy some cheese',
|
||||
'feed the cat',
|
||||
'book a doctors appointment'
|
||||
];
|
||||
|
||||
test.describe('New Todo', () => {
|
||||
test('should allow me to add todo items', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
// Create 1st todo.
|
||||
await newTodo.fill(TODO_ITEMS[0]);
|
||||
await newTodo.press('Enter');
|
||||
|
||||
// Make sure the list only has one todo item.
|
||||
await expect(page.getByTestId('todo-title')).toHaveText([
|
||||
TODO_ITEMS[0]
|
||||
]);
|
||||
|
||||
// Create 2nd todo.
|
||||
await newTodo.fill(TODO_ITEMS[1]);
|
||||
await newTodo.press('Enter');
|
||||
|
||||
// Make sure the list now has two todo items.
|
||||
await expect(page.getByTestId('todo-title')).toHaveText([
|
||||
TODO_ITEMS[0],
|
||||
TODO_ITEMS[1]
|
||||
]);
|
||||
|
||||
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||
});
|
||||
|
||||
test('should clear text input field when an item is added', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
// Create one todo item.
|
||||
await newTodo.fill(TODO_ITEMS[0]);
|
||||
await newTodo.press('Enter');
|
||||
|
||||
// Check that input is empty.
|
||||
await expect(newTodo).toBeEmpty();
|
||||
await checkNumberOfTodosInLocalStorage(page, 1);
|
||||
});
|
||||
|
||||
test('should append new items to the bottom of the list', async ({ page }) => {
|
||||
// Create 3 items.
|
||||
await createDefaultTodos(page);
|
||||
|
||||
// create a todo count locator
|
||||
const todoCount = page.getByTestId('todo-count')
|
||||
|
||||
// Check test using different methods.
|
||||
await expect(page.getByText('3 items left')).toBeVisible();
|
||||
await expect(todoCount).toHaveText('3 items left');
|
||||
await expect(todoCount).toContainText('3');
|
||||
await expect(todoCount).toHaveText(/3/);
|
||||
|
||||
// Check all items in one call.
|
||||
await expect(page.getByTestId('todo-title')).toHaveText(TODO_ITEMS);
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Mark all as completed', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test.afterEach(async ({ page }) => {
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test('should allow me to mark all items as completed', async ({ page }) => {
|
||||
// Complete all todos.
|
||||
await page.getByLabel('Mark all as complete').check();
|
||||
|
||||
// Ensure all todos have 'completed' class.
|
||||
await expect(page.getByTestId('todo-item')).toHaveClass(['completed', 'completed', 'completed']);
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test('should allow me to clear the complete state of all items', async ({ page }) => {
|
||||
const toggleAll = page.getByLabel('Mark all as complete');
|
||||
// Check and then immediately uncheck.
|
||||
await toggleAll.check();
|
||||
await toggleAll.uncheck();
|
||||
|
||||
// Should be no completed classes.
|
||||
await expect(page.getByTestId('todo-item')).toHaveClass(['', '', '']);
|
||||
});
|
||||
|
||||
test('complete all checkbox should update state when items are completed / cleared', async ({ page }) => {
|
||||
const toggleAll = page.getByLabel('Mark all as complete');
|
||||
await toggleAll.check();
|
||||
await expect(toggleAll).toBeChecked();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||
|
||||
// Uncheck first todo.
|
||||
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||
await firstTodo.getByRole('checkbox').uncheck();
|
||||
|
||||
// Reuse toggleAll locator and make sure its not checked.
|
||||
await expect(toggleAll).not.toBeChecked();
|
||||
|
||||
await firstTodo.getByRole('checkbox').check();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 3);
|
||||
|
||||
// Assert the toggle all is checked again.
|
||||
await expect(toggleAll).toBeChecked();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Item', () => {
|
||||
|
||||
test('should allow me to mark items as complete', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
// Create two items.
|
||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press('Enter');
|
||||
}
|
||||
|
||||
// Check first item.
|
||||
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||
await firstTodo.getByRole('checkbox').check();
|
||||
await expect(firstTodo).toHaveClass('completed');
|
||||
|
||||
// Check second item.
|
||||
const secondTodo = page.getByTestId('todo-item').nth(1);
|
||||
await expect(secondTodo).not.toHaveClass('completed');
|
||||
await secondTodo.getByRole('checkbox').check();
|
||||
|
||||
// Assert completed class.
|
||||
await expect(firstTodo).toHaveClass('completed');
|
||||
await expect(secondTodo).toHaveClass('completed');
|
||||
});
|
||||
|
||||
test('should allow me to un-mark items as complete', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
// Create two items.
|
||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press('Enter');
|
||||
}
|
||||
|
||||
const firstTodo = page.getByTestId('todo-item').nth(0);
|
||||
const secondTodo = page.getByTestId('todo-item').nth(1);
|
||||
const firstTodoCheckbox = firstTodo.getByRole('checkbox');
|
||||
|
||||
await firstTodoCheckbox.check();
|
||||
await expect(firstTodo).toHaveClass('completed');
|
||||
await expect(secondTodo).not.toHaveClass('completed');
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
|
||||
await firstTodoCheckbox.uncheck();
|
||||
await expect(firstTodo).not.toHaveClass('completed');
|
||||
await expect(secondTodo).not.toHaveClass('completed');
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 0);
|
||||
});
|
||||
|
||||
test('should allow me to edit an item', async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
const secondTodo = todoItems.nth(1);
|
||||
await secondTodo.dblclick();
|
||||
await expect(secondTodo.getByRole('textbox', { name: 'Edit' })).toHaveValue(TODO_ITEMS[1]);
|
||||
await secondTodo.getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||
await secondTodo.getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||
|
||||
// Explicitly assert the new text value.
|
||||
await expect(todoItems).toHaveText([
|
||||
TODO_ITEMS[0],
|
||||
'buy some sausages',
|
||||
TODO_ITEMS[2]
|
||||
]);
|
||||
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Editing', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test('should hide other controls when editing', async ({ page }) => {
|
||||
const todoItem = page.getByTestId('todo-item').nth(1);
|
||||
await todoItem.dblclick();
|
||||
await expect(todoItem.getByRole('checkbox')).not.toBeVisible();
|
||||
await expect(todoItem.locator('label', {
|
||||
hasText: TODO_ITEMS[1],
|
||||
})).not.toBeVisible();
|
||||
await checkNumberOfTodosInLocalStorage(page, 3);
|
||||
});
|
||||
|
||||
test('should save edits on blur', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).dispatchEvent('blur');
|
||||
|
||||
await expect(todoItems).toHaveText([
|
||||
TODO_ITEMS[0],
|
||||
'buy some sausages',
|
||||
TODO_ITEMS[2],
|
||||
]);
|
||||
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||
});
|
||||
|
||||
test('should trim entered text', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill(' buy some sausages ');
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||
|
||||
await expect(todoItems).toHaveText([
|
||||
TODO_ITEMS[0],
|
||||
'buy some sausages',
|
||||
TODO_ITEMS[2],
|
||||
]);
|
||||
await checkTodosInLocalStorage(page, 'buy some sausages');
|
||||
});
|
||||
|
||||
test('should remove the item if an empty text string was entered', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('');
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Enter');
|
||||
|
||||
await expect(todoItems).toHaveText([
|
||||
TODO_ITEMS[0],
|
||||
TODO_ITEMS[2],
|
||||
]);
|
||||
});
|
||||
|
||||
test('should cancel edits on escape', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).dblclick();
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).fill('buy some sausages');
|
||||
await todoItems.nth(1).getByRole('textbox', { name: 'Edit' }).press('Escape');
|
||||
await expect(todoItems).toHaveText(TODO_ITEMS);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Counter', () => {
|
||||
test('should display the current number of todo items', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
// create a todo count locator
|
||||
const todoCount = page.getByTestId('todo-count')
|
||||
|
||||
await newTodo.fill(TODO_ITEMS[0]);
|
||||
await newTodo.press('Enter');
|
||||
|
||||
await expect(todoCount).toContainText('1');
|
||||
|
||||
await newTodo.fill(TODO_ITEMS[1]);
|
||||
await newTodo.press('Enter');
|
||||
await expect(todoCount).toContainText('2');
|
||||
|
||||
await checkNumberOfTodosInLocalStorage(page, 2);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Clear completed button', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
});
|
||||
|
||||
test('should display the correct text', async ({ page }) => {
|
||||
await page.locator('.todo-list li .toggle').first().check();
|
||||
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('should remove completed items when clicked', async ({ page }) => {
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
await todoItems.nth(1).getByRole('checkbox').check();
|
||||
await page.getByRole('button', { name: 'Clear completed' }).click();
|
||||
await expect(todoItems).toHaveCount(2);
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||
});
|
||||
|
||||
test('should be hidden when there are no items that are completed', async ({ page }) => {
|
||||
await page.locator('.todo-list li .toggle').first().check();
|
||||
await page.getByRole('button', { name: 'Clear completed' }).click();
|
||||
await expect(page.getByRole('button', { name: 'Clear completed' })).toBeHidden();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Persistence', () => {
|
||||
test('should persist its data', async ({ page }) => {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
for (const item of TODO_ITEMS.slice(0, 2)) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press('Enter');
|
||||
}
|
||||
|
||||
const todoItems = page.getByTestId('todo-item');
|
||||
const firstTodoCheck = todoItems.nth(0).getByRole('checkbox');
|
||||
await firstTodoCheck.check();
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||
await expect(firstTodoCheck).toBeChecked();
|
||||
await expect(todoItems).toHaveClass(['completed', '']);
|
||||
|
||||
// Ensure there is 1 completed item.
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
|
||||
// Now reload.
|
||||
await page.reload();
|
||||
await expect(todoItems).toHaveText([TODO_ITEMS[0], TODO_ITEMS[1]]);
|
||||
await expect(firstTodoCheck).toBeChecked();
|
||||
await expect(todoItems).toHaveClass(['completed', '']);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe('Routing', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await createDefaultTodos(page);
|
||||
// make sure the app had a chance to save updated todos in storage
|
||||
// before navigating to a new view, otherwise the items can get lost :(
|
||||
// in some frameworks like Durandal
|
||||
await checkTodosInLocalStorage(page, TODO_ITEMS[0]);
|
||||
});
|
||||
|
||||
test('should allow me to display active items', async ({ page }) => {
|
||||
const todoItem = page.getByTestId('todo-item');
|
||||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
await page.getByRole('link', { name: 'Active' }).click();
|
||||
await expect(todoItem).toHaveCount(2);
|
||||
await expect(todoItem).toHaveText([TODO_ITEMS[0], TODO_ITEMS[2]]);
|
||||
});
|
||||
|
||||
test('should respect the back button', async ({ page }) => {
|
||||
const todoItem = page.getByTestId('todo-item');
|
||||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
|
||||
await test.step('Showing all items', async () => {
|
||||
await page.getByRole('link', { name: 'All' }).click();
|
||||
await expect(todoItem).toHaveCount(3);
|
||||
});
|
||||
|
||||
await test.step('Showing active items', async () => {
|
||||
await page.getByRole('link', { name: 'Active' }).click();
|
||||
});
|
||||
|
||||
await test.step('Showing completed items', async () => {
|
||||
await page.getByRole('link', { name: 'Completed' }).click();
|
||||
});
|
||||
|
||||
await expect(todoItem).toHaveCount(1);
|
||||
await page.goBack();
|
||||
await expect(todoItem).toHaveCount(2);
|
||||
await page.goBack();
|
||||
await expect(todoItem).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('should allow me to display completed items', async ({ page }) => {
|
||||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
await page.getByRole('link', { name: 'Completed' }).click();
|
||||
await expect(page.getByTestId('todo-item')).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('should allow me to display all items', async ({ page }) => {
|
||||
await page.getByTestId('todo-item').nth(1).getByRole('checkbox').check();
|
||||
await checkNumberOfCompletedTodosInLocalStorage(page, 1);
|
||||
await page.getByRole('link', { name: 'Active' }).click();
|
||||
await page.getByRole('link', { name: 'Completed' }).click();
|
||||
await page.getByRole('link', { name: 'All' }).click();
|
||||
await expect(page.getByTestId('todo-item')).toHaveCount(3);
|
||||
});
|
||||
|
||||
test('should highlight the currently applied filter', async ({ page }) => {
|
||||
await expect(page.getByRole('link', { name: 'All' })).toHaveClass('selected');
|
||||
|
||||
//create locators for active and completed links
|
||||
const activeLink = page.getByRole('link', { name: 'Active' });
|
||||
const completedLink = page.getByRole('link', { name: 'Completed' });
|
||||
await activeLink.click();
|
||||
|
||||
// Page change - active items.
|
||||
await expect(activeLink).toHaveClass('selected');
|
||||
await completedLink.click();
|
||||
|
||||
// Page change - completed items.
|
||||
await expect(completedLink).toHaveClass('selected');
|
||||
});
|
||||
});
|
||||
|
||||
async function createDefaultTodos(page: Page) {
|
||||
// create a new todo locator
|
||||
const newTodo = page.getByPlaceholder('What needs to be done?');
|
||||
|
||||
for (const item of TODO_ITEMS) {
|
||||
await newTodo.fill(item);
|
||||
await newTodo.press('Enter');
|
||||
}
|
||||
}
|
||||
|
||||
async function checkNumberOfTodosInLocalStorage(page: Page, expected: number) {
|
||||
return await page.waitForFunction(e => {
|
||||
return JSON.parse(localStorage['react-todos']).length === e;
|
||||
}, expected);
|
||||
}
|
||||
|
||||
async function checkNumberOfCompletedTodosInLocalStorage(page: Page, expected: number) {
|
||||
return await page.waitForFunction(e => {
|
||||
return JSON.parse(localStorage['react-todos']).filter((todo: any) => todo.completed).length === e;
|
||||
}, expected);
|
||||
}
|
||||
|
||||
async function checkTodosInLocalStorage(page: Page, title: string) {
|
||||
return await page.waitForFunction(t => {
|
||||
return JSON.parse(localStorage['react-todos']).map((todo: any) => todo.title).includes(t);
|
||||
}, title);
|
||||
}
|
||||
18
Services/client/tests/example.spec.ts
Normal file
18
Services/client/tests/example.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { test, expect } from '@playwright/test';
|
||||
|
||||
test('has title', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev/');
|
||||
|
||||
// Expect a title "to contain" a substring.
|
||||
await expect(page).toHaveTitle(/Playwright/);
|
||||
});
|
||||
|
||||
test('get started link', async ({ page }) => {
|
||||
await page.goto('https://playwright.dev/');
|
||||
|
||||
// Click the get started link.
|
||||
await page.getByRole('link', { name: 'Get started' }).click();
|
||||
|
||||
// Expects page to have a heading with the name of Installation.
|
||||
await expect(page.getByRole('heading', { name: 'Installation' })).toBeVisible();
|
||||
});
|
||||
|
|
@ -1,11 +1,6 @@
|
|||
{
|
||||
"extends": ["@tsconfig/node18/tsconfig.json", "@vue/tsconfig/tsconfig.json"],
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"playwright.config.*"
|
||||
],
|
||||
"include": ["vite.config.*", "vitest.config.*", "playwright.config.*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"ignoreDeprecations": "5.0",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"plugin": ["typedoc-plugin-vue"],
|
||||
"entryPoints": ["src/main.ts","src/**/*.ts"],
|
||||
"out": "fe-typedoc"
|
||||
"plugin": ["typedoc-plugin-vue"],
|
||||
"entryPoints": ["src/main.ts", "src/**/*.ts"],
|
||||
"out": "fe-typedoc"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,12 @@ Enterprise Document Management (EDM) ส่วน backend
|
|||
- Attachment Processor สำหรับดึงข้อความเพื่อทำ Index สำหรับ Elasticsearch
|
||||
- RabbitMQ ใช้ Bucket Notification ของ MiniO ส่งแจ้งเตือนเมื่อมีเอกสารใหม่จะต้องทำ Index ทำงานเป็น คิวเพื่อทำให้รับโหลดเอกสารได้มากยิ่งขึ้น เอกสารที่ทำผ่าน MiniO ก็จะถูกทำ Index โดยที่ Application ไม่ต้องเขียนการจัดการพิเศษ
|
||||
|
||||
# การเรียกใช้ผ่าน Service Account
|
||||
จำเป็นต้องตั้งค่า Keycloak เพื่อให้สามารถขอ Token โดยใช้ Secret Key ที่ได้รับ ผ่านทาง `https://edm-id.frappet.synology.me/realms/EDM/protocol/openid-connect/token` ดังนี้
|
||||

|
||||
|
||||
# สิทธิในจัดการ
|
||||
การจัดการสามารถตั้งค่าได้โดยใช้ ENV ที่มีชื่อว่า MANAGEMENT_ROLE ให้ตรงกับที่ตั้งค่าใน Keycloak อีกที
|
||||
|
||||
## Note
|
||||
- ELK (Elasticsearch+Kibana) ใช้แบบ unsecure อยู่หลัง firewall น่าจะเพียงพอ อาจจะ[ปรับเพิ่มเรื่องความปลอดภัย](https://vorapoap.medium.com/%E0%B8%95%E0%B8%AD%E0%B8%99%E0%B8%97%E0%B8%B5%E0%B9%88-3-%E0%B8%95%E0%B8%B4%E0%B8%94%E0%B8%95%E0%B8%B1%E0%B9%89%E0%B8%87-security-%E0%B9%83%E0%B8%AB%E0%B9%89%E0%B8%81%E0%B8%B1%E0%B8%9A-elasticsearch-aa26a71b87ff)ในอนาคน
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ export class CabinetController extends Controller {
|
|||
|
||||
@Post("/")
|
||||
@Tags("ตู้เอกสาร")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์")
|
||||
@SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
|
||||
public async createCabinet(
|
||||
|
|
@ -94,7 +94,7 @@ export class CabinetController extends Controller {
|
|||
*/
|
||||
@Put("/{cabinetName}")
|
||||
@Tags("ตู้เอกสาร")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้")
|
||||
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
|
||||
public async editCabinet(
|
||||
|
|
@ -163,7 +163,7 @@ export class CabinetController extends Controller {
|
|||
*/
|
||||
@Delete("/{cabinetName}")
|
||||
@Tags("ตู้เอกสาร")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาด ไม่สามารถลบไฟล์ได้")
|
||||
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
|
||||
public async deleteCabinet(@Path() cabinetName: string) {
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ export class DrawerController extends Controller {
|
|||
*/
|
||||
@Post("/")
|
||||
@Tags("ลิ้นชัก")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@Response(HttpStatusCode.NOT_FOUND, "ไม่พบลิ้นชัก")
|
||||
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์")
|
||||
@SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
|
||||
|
|
@ -110,7 +110,7 @@ export class DrawerController extends Controller {
|
|||
*/
|
||||
@Put("/{drawerName}")
|
||||
@Tags("ลิ้นชัก")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้")
|
||||
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
|
||||
public async editDrawer(
|
||||
|
|
@ -181,7 +181,7 @@ export class DrawerController extends Controller {
|
|||
*/
|
||||
@Delete("/{drawerName}")
|
||||
@Tags("ลิ้นชัก")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
|
||||
public async deleteDrawer(@Path() cabinetName: string, @Path() drawerName: string) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ export class FileController extends Controller {
|
|||
*/
|
||||
@Post("/")
|
||||
@Tags("ไฟล์")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@Response(
|
||||
HttpStatusCode.NOT_FOUND,
|
||||
"ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ",
|
||||
|
|
@ -180,7 +180,7 @@ export class FileController extends Controller {
|
|||
const metadata: Partial<StorageFile> = {
|
||||
pathname,
|
||||
path: basePath,
|
||||
fileName: replaceIllegalChars( body.file ),
|
||||
fileName: replaceIllegalChars(body.file),
|
||||
fileSize: 0,
|
||||
fileType: "",
|
||||
title: body.title ?? "",
|
||||
|
|
@ -218,7 +218,7 @@ export class FileController extends Controller {
|
|||
*/
|
||||
@Patch("/{fileName}")
|
||||
@Tags("ไฟล์")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม")
|
||||
@Response(HttpStatusCode.NO_CONTENT, "สำเร็จ")
|
||||
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
|
||||
|
|
@ -270,9 +270,11 @@ export class FileController extends Controller {
|
|||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์");
|
||||
}
|
||||
|
||||
const { file, ...metadata } = body;
|
||||
|
||||
// assume user will probably replace file by re-upload but maybe just rename
|
||||
if (body.file) {
|
||||
const destination = `${basePath}${replaceIllegalChars(body.file)}`;
|
||||
if (file) {
|
||||
const destination = `${basePath}${replaceIllegalChars(file)}`;
|
||||
const source = `/${DEFAULT_BUCKET}/${basePath}${fileName}`;
|
||||
const copy = await minioClient.copyObject(DEFAULT_BUCKET!, destination, source, copyCond);
|
||||
|
||||
|
|
@ -291,9 +293,10 @@ export class FileController extends Controller {
|
|||
index,
|
||||
id,
|
||||
doc: {
|
||||
...metadata,
|
||||
pathname: destination,
|
||||
path: basePath,
|
||||
fileName: replaceIllegalChars(body.file),
|
||||
fileName: replaceIllegalChars(file),
|
||||
updatedAt: new Date().toISOString(),
|
||||
updatedBy: request.user.preferred_username ?? "n/a",
|
||||
},
|
||||
|
|
@ -318,7 +321,7 @@ export class FileController extends Controller {
|
|||
index,
|
||||
id,
|
||||
doc: {
|
||||
...body,
|
||||
...metadata,
|
||||
updatedAt: new Date().toISOString(),
|
||||
updatedBy: request.user.preferred_username ?? "n/a",
|
||||
},
|
||||
|
|
@ -345,7 +348,7 @@ export class FileController extends Controller {
|
|||
*/
|
||||
@Delete("/{fileName}")
|
||||
@Tags("ไฟล์")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
|
||||
public async deleteFile(
|
||||
@Path() cabinetName: string,
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ export class FolderController extends Controller {
|
|||
*/
|
||||
@Post("/")
|
||||
@Tags("แฟ้ม")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม")
|
||||
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์")
|
||||
@SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
|
||||
|
|
@ -116,7 +116,7 @@ export class FolderController extends Controller {
|
|||
*/
|
||||
@Put("/{folderName}")
|
||||
@Tags("แฟ้ม")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้")
|
||||
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
|
||||
public async editFolder(
|
||||
|
|
@ -189,7 +189,7 @@ export class FolderController extends Controller {
|
|||
*/
|
||||
@Delete("/{folderName}")
|
||||
@Tags("แฟ้ม")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
|
||||
public async deleteFolder(
|
||||
@Path() cabinetName: string,
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export class SubFolderController extends Controller {
|
|||
*/
|
||||
@Post("/")
|
||||
@Tags("แฟ้มย่อย")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@Response(HttpStatusCode.NOT_FOUND, "ไม่พบของแฟ้ม")
|
||||
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดกับระบบจัดการไฟล์")
|
||||
@SuccessResponse(HttpStatusCode.CREATED, "สำเร็จ")
|
||||
|
|
@ -116,7 +116,7 @@ export class SubFolderController extends Controller {
|
|||
*/
|
||||
@Put("/{subFolderName}")
|
||||
@Tags("แฟ้มย่อย")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@Response(HttpStatusCode.INTERNAL_SERVER_ERROR, "เกิดข้อผิดพลาดไม่สามารถย้ายไฟล์ได้")
|
||||
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
|
||||
public async editFolder(
|
||||
|
|
@ -193,7 +193,7 @@ export class SubFolderController extends Controller {
|
|||
*/
|
||||
@Delete("/{subFolderName}")
|
||||
@Tags("แฟ้มย่อย")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@SuccessResponse(HttpStatusCode.NO_CONTENT, "สำเร็จ")
|
||||
public async deleteFolder(
|
||||
@Path() cabinetName: string,
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ export class SubFolderFileController extends Controller {
|
|||
*/
|
||||
@Post("/")
|
||||
@Tags("ไฟล์")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@Response(
|
||||
HttpStatusCode.NOT_FOUND,
|
||||
"ตำแหน่งที่ระบุไม่พบ กรุณาเตรียมตำแหน่งที่ต้องการก่อนดำเนินการ",
|
||||
|
|
@ -225,7 +225,7 @@ export class SubFolderFileController extends Controller {
|
|||
*/
|
||||
@Patch("/{fileName}")
|
||||
@Tags("ไฟล์")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@Response(HttpStatusCode.NOT_FOUND, "ไม่พบตำแหน่งที่ต้องการสร้างแฟ้ม")
|
||||
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
|
||||
public async updateFile(
|
||||
|
|
@ -277,9 +277,11 @@ export class SubFolderFileController extends Controller {
|
|||
throw new HttpError(HttpStatusCode.NOT_FOUND, "ไม่พบไฟล์");
|
||||
}
|
||||
|
||||
const { file, ...metadata } = body;
|
||||
|
||||
// assume user will probably replace file by re-upload but maybe just rename
|
||||
if (body.file) {
|
||||
const destination = `${basePath}${replaceIllegalChars(body.file)}`;
|
||||
if (file) {
|
||||
const destination = `${basePath}${replaceIllegalChars(file)}`;
|
||||
const source = `/${DEFAULT_BUCKET}/${basePath}${fileName}`;
|
||||
const copy = await minioClient.copyObject(DEFAULT_BUCKET!, destination, source, copyCond);
|
||||
|
||||
|
|
@ -298,8 +300,10 @@ export class SubFolderFileController extends Controller {
|
|||
index,
|
||||
id,
|
||||
doc: {
|
||||
...metadata,
|
||||
pathname: destination,
|
||||
fileName: replaceIllegalChars(body.file),
|
||||
path: basePath,
|
||||
fileName: replaceIllegalChars(file),
|
||||
updatedAt: new Date().toISOString(),
|
||||
updatedBy: request.user.preferred_username ?? "n/a",
|
||||
},
|
||||
|
|
@ -324,7 +328,7 @@ export class SubFolderFileController extends Controller {
|
|||
index,
|
||||
id,
|
||||
doc: {
|
||||
...body,
|
||||
...metadata,
|
||||
updatedAt: new Date().toISOString(),
|
||||
updatedBy: request.user.preferred_username ?? "n/a",
|
||||
},
|
||||
|
|
@ -352,7 +356,7 @@ export class SubFolderFileController extends Controller {
|
|||
*/
|
||||
@Delete("/{fileName}")
|
||||
@Tags("ไฟล์")
|
||||
@Security("bearerAuth", ["admin"])
|
||||
@Security("bearerAuth", ["admin", "management-role"])
|
||||
@SuccessResponse(HttpStatusCode.OK, "สำเร็จ")
|
||||
public async deleteFile(
|
||||
@Path() cabinetName: string,
|
||||
|
|
@ -405,7 +409,7 @@ export class SubFolderFileController extends Controller {
|
|||
...rest,
|
||||
download: await minioClient.presignedGetObject(
|
||||
DEFAULT_BUCKET!,
|
||||
`${cabinetName}/${drawerName}/${folderName}/${fileName}`,
|
||||
`${cabinetName}/${drawerName}/${folderName}/${subFolderName}/${fileName}`,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/cabinet',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(CabinetController)),
|
||||
...(fetchMiddlewares<RequestHandler>(CabinetController.prototype.createCabinet)),
|
||||
|
||||
|
|
@ -128,7 +128,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.put('/cabinet/:cabinetName',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(CabinetController)),
|
||||
...(fetchMiddlewares<RequestHandler>(CabinetController.prototype.editCabinet)),
|
||||
|
||||
|
|
@ -155,7 +155,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.delete('/cabinet/:cabinetName',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(CabinetController)),
|
||||
...(fetchMiddlewares<RequestHandler>(CabinetController.prototype.deleteCabinet)),
|
||||
|
||||
|
|
@ -207,7 +207,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/cabinet/:cabinetName/drawer',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(DrawerController)),
|
||||
...(fetchMiddlewares<RequestHandler>(DrawerController.prototype.createDrawer)),
|
||||
|
||||
|
|
@ -235,7 +235,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.put('/cabinet/:cabinetName/drawer/:drawerName',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(DrawerController)),
|
||||
...(fetchMiddlewares<RequestHandler>(DrawerController.prototype.editDrawer)),
|
||||
|
||||
|
|
@ -263,7 +263,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.delete('/cabinet/:cabinetName/drawer/:drawerName',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(DrawerController)),
|
||||
...(fetchMiddlewares<RequestHandler>(DrawerController.prototype.deleteDrawer)),
|
||||
|
||||
|
|
@ -318,7 +318,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(FileController)),
|
||||
...(fetchMiddlewares<RequestHandler>(FileController.prototype.uploadFile)),
|
||||
|
||||
|
|
@ -348,7 +348,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.patch('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(FileController)),
|
||||
...(fetchMiddlewares<RequestHandler>(FileController.prototype.updateFile)),
|
||||
|
||||
|
|
@ -379,7 +379,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/file/:fileName',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(FileController)),
|
||||
...(fetchMiddlewares<RequestHandler>(FileController.prototype.deleteFile)),
|
||||
|
||||
|
|
@ -464,7 +464,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/cabinet/:cabinetName/drawer/:drawerName/folder',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(FolderController)),
|
||||
...(fetchMiddlewares<RequestHandler>(FolderController.prototype.createFolder)),
|
||||
|
||||
|
|
@ -493,7 +493,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.put('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(FolderController)),
|
||||
...(fetchMiddlewares<RequestHandler>(FolderController.prototype.editFolder)),
|
||||
|
||||
|
|
@ -522,7 +522,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(FolderController)),
|
||||
...(fetchMiddlewares<RequestHandler>(FolderController.prototype.deleteFolder)),
|
||||
|
||||
|
|
@ -604,7 +604,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(SubFolderController)),
|
||||
...(fetchMiddlewares<RequestHandler>(SubFolderController.prototype.createFolder)),
|
||||
|
||||
|
|
@ -634,7 +634,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.put('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(SubFolderController)),
|
||||
...(fetchMiddlewares<RequestHandler>(SubFolderController.prototype.editFolder)),
|
||||
|
||||
|
|
@ -664,7 +664,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(SubFolderController)),
|
||||
...(fetchMiddlewares<RequestHandler>(SubFolderController.prototype.deleteFolder)),
|
||||
|
||||
|
|
@ -722,7 +722,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.post('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(SubFolderFileController)),
|
||||
...(fetchMiddlewares<RequestHandler>(SubFolderFileController.prototype.uploadFile)),
|
||||
|
||||
|
|
@ -753,7 +753,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.patch('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file/:fileName',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(SubFolderFileController)),
|
||||
...(fetchMiddlewares<RequestHandler>(SubFolderFileController.prototype.updateFile)),
|
||||
|
||||
|
|
@ -785,7 +785,7 @@ export function RegisterRoutes(app: Router) {
|
|||
});
|
||||
// WARNING: This file was auto-generated with tsoa. Please do not modify it. Re-run tsoa to re-generate this file: https://github.com/lukeautry/tsoa
|
||||
app.delete('/cabinet/:cabinetName/drawer/:drawerName/folder/:folderName/subfolder/:subFolderName/file/:fileName',
|
||||
authenticateMiddleware([{"bearerAuth":["admin"]}]),
|
||||
authenticateMiddleware([{"bearerAuth":["admin","management-role"]}]),
|
||||
...(fetchMiddlewares<RequestHandler>(SubFolderFileController)),
|
||||
...(fetchMiddlewares<RequestHandler>(SubFolderFileController.prototype.deleteFile)),
|
||||
|
||||
|
|
|
|||
|
|
@ -261,7 +261,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -304,7 +305,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -355,7 +357,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -450,7 +453,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -503,7 +507,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -560,7 +565,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -783,7 +789,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -906,7 +913,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -1007,7 +1015,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -1286,7 +1295,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -1348,7 +1358,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -1414,7 +1425,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -1584,7 +1596,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -1654,7 +1667,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -1729,7 +1743,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -1979,7 +1994,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -2108,7 +2124,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
@ -2218,7 +2235,8 @@
|
|||
"security": [
|
||||
{
|
||||
"bearerAuth": [
|
||||
"admin"
|
||||
"admin",
|
||||
"management-role"
|
||||
]
|
||||
}
|
||||
],
|
||||
|
|
|
|||
|
|
@ -1,12 +1,19 @@
|
|||
import * as express from "express";
|
||||
import { createVerifier } from "fast-jwt";
|
||||
import { createDecoder, createVerifier } from "fast-jwt";
|
||||
|
||||
import HttpError from "../interfaces/http-error";
|
||||
import HttpStatusCode from "../interfaces/http-status";
|
||||
import { JwtPayload } from "jsonwebtoken";
|
||||
|
||||
if (!process.env.PUBLIC_KEY && !process.env.REALM_URL) {
|
||||
throw new Error("Require public key or realm url.");
|
||||
}
|
||||
if (process.env.PUBLIC_KEY && process.env.REALM_URL && !process.env.PREFERRED_AUTH) {
|
||||
throw new Error("Preferred auth type must be specified if public key and realm url is provided.");
|
||||
}
|
||||
if (!process.env.MANAGEMENT_ROLE) {
|
||||
throw new Error("Management role env is required.");
|
||||
}
|
||||
|
||||
const jwtVerify = createVerifier({
|
||||
key: async () => {
|
||||
|
|
@ -14,6 +21,8 @@ const jwtVerify = createVerifier({
|
|||
},
|
||||
});
|
||||
|
||||
const jwtDecode = createDecoder();
|
||||
|
||||
export async function expressAuthentication(
|
||||
request: express.Request,
|
||||
securityName: string,
|
||||
|
|
@ -29,19 +38,47 @@ export async function expressAuthentication(
|
|||
|
||||
if (!token) throw new HttpError(HttpStatusCode.UNAUTHORIZED, "No token provided.");
|
||||
|
||||
const payload = await jwtVerify(token).catch((_) => null);
|
||||
let payload: JwtPayload = {};
|
||||
|
||||
if (!payload) {
|
||||
throw new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided.");
|
||||
switch (process.env.PREFERRED_AUTH) {
|
||||
case "online":
|
||||
payload = await verifyOnline(token);
|
||||
break;
|
||||
case "offline":
|
||||
payload = await verifyOffline(token);
|
||||
break;
|
||||
default:
|
||||
if (process.env.REALM_URL) payload = await verifyOnline(token);
|
||||
if (process.env.PUBLIC_KEY) payload = await verifyOffline(token);
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
scopes &&
|
||||
scopes.length > 0 &&
|
||||
scopes.some((v) => !payload.resource_access[payload.azp].roles.includes(v))
|
||||
scopes
|
||||
.map((v) => (v === "management-role" ? process.env.MANAGEMENT_ROLE : v))
|
||||
.every((v) => !payload.resource_access[payload.azp].roles.includes(v))
|
||||
) {
|
||||
throw new HttpError(HttpStatusCode.FORBIDDEN, "You are not allowed to perform this action.");
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function verifyOffline(token: string) {
|
||||
const payload = await jwtVerify(token).catch((_) => null);
|
||||
if (!payload) throw new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided.");
|
||||
return payload;
|
||||
}
|
||||
|
||||
async function verifyOnline(token: string) {
|
||||
const res = await fetch(`${process.env.REALM_URL}/protocol/openid-connect/userinfo`, {
|
||||
headers: { authorization: `Bearer ${token}` },
|
||||
}).catch((e) => console.error(e));
|
||||
|
||||
if (!res) throw new Error("Cannot connect to auth service.");
|
||||
if (!res.ok) throw new HttpError(HttpStatusCode.UNAUTHORIZED, "Invalid token provided.");
|
||||
|
||||
return await jwtDecode(token);
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue