diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml new file mode 100644 index 0000000000000000000000000000000000000000..4450c01a894a1f0f4f582edb67019753499d9332 --- /dev/null +++ b/.gitlab-ci.yml @@ -0,0 +1,20 @@ +stages: + - install + - build + +cache: + paths: + - node_modules/ + key: "$CI_BUILD_REF_NAME" # Separate cache for each branch + +install_dependencies: + stage: install + image: node:latest + script: + - npm install + +build_project: + stage: build + image: node:latest + script: + - npm run build \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 969552f258803cb182fd8f4a5cff36ae102f2fcb..380793cab4d9aec8226d36377a5a0f5f9ef907f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,14 @@ "version": "0.0.0", "dependencies": { "@popperjs/core": "^2.11.8", + "axios": "^1.6.8", "bootstrap": "^5.3.3", + "js-cookie": "^3.0.5", + "oh-vue-icons": "^1.0.0-rc3", "pinia": "^2.1.7", "vue": "^3.4.21", - "vue-router": "^4.3.0" + "vue-router": "^4.3.0", + "xml2js": "^0.6.2" }, "devDependencies": { "@rushstack/eslint-patch": "^1.8.0", @@ -2657,8 +2661,7 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" }, "node_modules/at-least-node": { "version": "1.0.0", @@ -2688,7 +2691,6 @@ "version": "1.6.8", "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", - "dev": true, "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -2699,7 +2701,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -2712,8 +2713,7 @@ "node_modules/axios/node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/balanced-match": { "version": "1.0.2", @@ -3116,7 +3116,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -3539,7 +3538,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "engines": { "node": ">=0.4.0" } @@ -4486,7 +4484,6 @@ "version": "1.15.6", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", - "dev": true, "funding": [ { "type": "individual", @@ -5248,7 +5245,6 @@ "version": "3.0.5", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", "integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==", - "dev": true, "engines": { "node": ">=14" } @@ -5843,7 +5839,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "engines": { "node": ">= 0.6" } @@ -5852,7 +5847,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -6070,6 +6064,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/oh-vue-icons": { + "version": "1.0.0-rc3", + "resolved": "https://registry.npmjs.org/oh-vue-icons/-/oh-vue-icons-1.0.0-rc3.tgz", + "integrity": "sha512-+k2YC6piK7sEZnwbkQF3UokFPMmgqpiLP6f/H0ovQFLl20QA5V4U8EcI6EclD2Lt5NMQ3k6ilLGo8XyXqdVSvg==", + "dependencies": { + "vue-demi": "^0.12.5" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^2.0.0 || >=3.0.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/oh-vue-icons/node_modules/vue-demi": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.12.5.tgz", + "integrity": "sha512-BREuTgTYlUr0zw0EZn3hnhC3I6gPWv+Kwh4MCih6QcAeaTlaIX0DwOVN0wHej7hSvDPecz4jygy/idsgKfW58Q==", + "hasInstallScript": true, + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -6868,6 +6904,11 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, + "node_modules/sax": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", + "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==" + }, "node_modules/saxes": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", @@ -8567,6 +8608,26 @@ "node": ">=12" } }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", diff --git a/package.json b/package.json index 8e38aa925053d6df7ce85891ec58823811d25d33..abf7692db221f6ba8f6bcb90e1978cafe97b4ee2 100644 --- a/package.json +++ b/package.json @@ -17,10 +17,14 @@ }, "dependencies": { "@popperjs/core": "^2.11.8", + "axios": "^1.6.8", "bootstrap": "^5.3.3", + "js-cookie": "^3.0.5", + "oh-vue-icons": "^1.0.0-rc3", "pinia": "^2.1.7", "vue": "^3.4.21", - "vue-router": "^4.3.0" + "vue-router": "^4.3.0", + "xml2js": "^0.6.2" }, "devDependencies": { "@rushstack/eslint-patch": "^1.8.0", diff --git a/src/App.vue b/src/App.vue index 461e3d52db193794b9b516bce015595a8eeb985d..1a140ee58a2d942ddacb457b5fbd6f5b5ad23e0b 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,9 +1,18 @@ <script setup lang="ts"> import { RouterView } from 'vue-router' +import ErrorBoundaryCatcher from '@/components/Exceptions/ErrorBoundaryCatcher.vue'; </script> <template> <main> - <RouterView /> + <error-boundary-catcher> + <RouterView /> + </error-boundary-catcher> </main> -</template> \ No newline at end of file +</template> + +<style> + main { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + } +</style> \ No newline at end of file diff --git a/src/api/core/ApiError.ts b/src/api/core/ApiError.ts new file mode 100644 index 0000000000000000000000000000000000000000..ec7b16af6f41b1323a8e3aa3d529bf2324959e66 --- /dev/null +++ b/src/api/core/ApiError.ts @@ -0,0 +1,25 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; + +export class ApiError extends Error { + public readonly url: string; + public readonly status: number; + public readonly statusText: string; + public readonly body: any; + public readonly request: ApiRequestOptions; + + constructor(request: ApiRequestOptions, response: ApiResult, message: string) { + super(message); + + this.name = 'ApiError'; + this.url = response.url; + this.status = response.status; + this.statusText = response.statusText; + this.body = response.body; + this.request = request; + } +} diff --git a/src/api/core/ApiRequestOptions.ts b/src/api/core/ApiRequestOptions.ts new file mode 100644 index 0000000000000000000000000000000000000000..93143c3ce1ba5323894d4ac10299f62493f030f6 --- /dev/null +++ b/src/api/core/ApiRequestOptions.ts @@ -0,0 +1,17 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ApiRequestOptions = { + readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH'; + readonly url: string; + readonly path?: Record<string, any>; + readonly cookies?: Record<string, any>; + readonly headers?: Record<string, any>; + readonly query?: Record<string, any>; + readonly formData?: Record<string, any>; + readonly body?: any; + readonly mediaType?: string; + readonly responseHeader?: string; + readonly errors?: Record<number, string>; +}; diff --git a/src/api/core/ApiResult.ts b/src/api/core/ApiResult.ts new file mode 100644 index 0000000000000000000000000000000000000000..ee1126e2ccd1e37dba97511c38c56a282ceac4dc --- /dev/null +++ b/src/api/core/ApiResult.ts @@ -0,0 +1,11 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export type ApiResult = { + readonly url: string; + readonly ok: boolean; + readonly status: number; + readonly statusText: string; + readonly body: any; +}; diff --git a/src/api/core/CancelablePromise.ts b/src/api/core/CancelablePromise.ts new file mode 100644 index 0000000000000000000000000000000000000000..d70de92946d977e9da7970871375117a8b04770a --- /dev/null +++ b/src/api/core/CancelablePromise.ts @@ -0,0 +1,131 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export class CancelError extends Error { + + constructor(message: string) { + super(message); + this.name = 'CancelError'; + } + + public get isCancelled(): boolean { + return true; + } +} + +export interface OnCancel { + readonly isResolved: boolean; + readonly isRejected: boolean; + readonly isCancelled: boolean; + + (cancelHandler: () => void): void; +} + +export class CancelablePromise<T> implements Promise<T> { + #isResolved: boolean; + #isRejected: boolean; + #isCancelled: boolean; + readonly #cancelHandlers: (() => void)[]; + readonly #promise: Promise<T>; + #resolve?: (value: T | PromiseLike<T>) => void; + #reject?: (reason?: any) => void; + + constructor( + executor: ( + resolve: (value: T | PromiseLike<T>) => void, + reject: (reason?: any) => void, + onCancel: OnCancel + ) => void + ) { + this.#isResolved = false; + this.#isRejected = false; + this.#isCancelled = false; + this.#cancelHandlers = []; + this.#promise = new Promise<T>((resolve, reject) => { + this.#resolve = resolve; + this.#reject = reject; + + const onResolve = (value: T | PromiseLike<T>): void => { + if (this.#isResolved || this.#isRejected || this.#isCancelled) { + return; + } + this.#isResolved = true; + if (this.#resolve) this.#resolve(value); + }; + + const onReject = (reason?: any): void => { + if (this.#isResolved || this.#isRejected || this.#isCancelled) { + return; + } + this.#isRejected = true; + if (this.#reject) this.#reject(reason); + }; + + const onCancel = (cancelHandler: () => void): void => { + if (this.#isResolved || this.#isRejected || this.#isCancelled) { + return; + } + this.#cancelHandlers.push(cancelHandler); + }; + + Object.defineProperty(onCancel, 'isResolved', { + get: (): boolean => this.#isResolved, + }); + + Object.defineProperty(onCancel, 'isRejected', { + get: (): boolean => this.#isRejected, + }); + + Object.defineProperty(onCancel, 'isCancelled', { + get: (): boolean => this.#isCancelled, + }); + + return executor(onResolve, onReject, onCancel as OnCancel); + }); + } + + get [Symbol.toStringTag]() { + return "Cancellable Promise"; + } + + public then<TResult1 = T, TResult2 = never>( + onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null, + onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null + ): Promise<TResult1 | TResult2> { + return this.#promise.then(onFulfilled, onRejected); + } + + public catch<TResult = never>( + onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null + ): Promise<T | TResult> { + return this.#promise.catch(onRejected); + } + + public finally(onFinally?: (() => void) | null): Promise<T> { + return this.#promise.finally(onFinally); + } + + public cancel(): void { + if (this.#isResolved || this.#isRejected || this.#isCancelled) { + return; + } + this.#isCancelled = true; + if (this.#cancelHandlers.length) { + try { + for (const cancelHandler of this.#cancelHandlers) { + cancelHandler(); + } + } catch (error) { + console.warn('Cancellation threw an error', error); + return; + } + } + this.#cancelHandlers.length = 0; + if (this.#reject) this.#reject(new CancelError('Request aborted')); + } + + public get isCancelled(): boolean { + return this.#isCancelled; + } +} diff --git a/src/api/core/OpenAPI.ts b/src/api/core/OpenAPI.ts new file mode 100644 index 0000000000000000000000000000000000000000..bae5eb773e38a6fa187a68f3ac9f3dbecc88eaf5 --- /dev/null +++ b/src/api/core/OpenAPI.ts @@ -0,0 +1,32 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import type { ApiRequestOptions } from './ApiRequestOptions'; + +type Resolver<T> = (options: ApiRequestOptions) => Promise<T>; +type Headers = Record<string, string>; + +export type OpenAPIConfig = { + BASE: string; + VERSION: string; + WITH_CREDENTIALS: boolean; + CREDENTIALS: 'include' | 'omit' | 'same-origin'; + TOKEN?: string | Resolver<string> | undefined; + USERNAME?: string | Resolver<string> | undefined; + PASSWORD?: string | Resolver<string> | undefined; + HEADERS?: Headers | Resolver<Headers> | undefined; + ENCODE_PATH?: ((path: string) => string) | undefined; +}; + +export const OpenAPI: OpenAPIConfig = { + BASE: 'http://localhost:8080', + VERSION: '0', + WITH_CREDENTIALS: false, + CREDENTIALS: 'include', + TOKEN: undefined, + USERNAME: undefined, + PASSWORD: undefined, + HEADERS: undefined, + ENCODE_PATH: undefined, +}; diff --git a/src/api/core/request.ts b/src/api/core/request.ts new file mode 100644 index 0000000000000000000000000000000000000000..1dc6fef4aab4086ff57b48d26b20ec26bb8fa472 --- /dev/null +++ b/src/api/core/request.ts @@ -0,0 +1,323 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +import axios from 'axios'; +import type { AxiosError, AxiosRequestConfig, AxiosResponse, AxiosInstance } from 'axios'; +import FormData from 'form-data'; + +import { ApiError } from './ApiError'; +import type { ApiRequestOptions } from './ApiRequestOptions'; +import type { ApiResult } from './ApiResult'; +import { CancelablePromise } from './CancelablePromise'; +import type { OnCancel } from './CancelablePromise'; +import type { OpenAPIConfig } from './OpenAPI'; + +export const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => { + return value !== undefined && value !== null; +}; + +export const isString = (value: any): value is string => { + return typeof value === 'string'; +}; + +export const isStringWithValue = (value: any): value is string => { + return isString(value) && value !== ''; +}; + +export const isBlob = (value: any): value is Blob => { + return ( + typeof value === 'object' && + typeof value.type === 'string' && + typeof value.stream === 'function' && + typeof value.arrayBuffer === 'function' && + typeof value.constructor === 'function' && + typeof value.constructor.name === 'string' && + /^(Blob|File)$/.test(value.constructor.name) && + /^(Blob|File)$/.test(value[Symbol.toStringTag]) + ); +}; + +export const isFormData = (value: any): value is FormData => { + return value instanceof FormData; +}; + +export const isSuccess = (status: number): boolean => { + return status >= 200 && status < 300; +}; + +export const base64 = (str: string): string => { + try { + return btoa(str); + } catch (err) { + // @ts-ignore + return Buffer.from(str).toString('base64'); + } +}; + +export const getQueryString = (params: Record<string, any>): string => { + const qs: string[] = []; + + const append = (key: string, value: any) => { + qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + }; + + const process = (key: string, value: any) => { + if (isDefined(value)) { + if (Array.isArray(value)) { + value.forEach(v => { + process(key, v); + }); + } else if (typeof value === 'object') { + Object.entries(value).forEach(([k, v]) => { + process(`${key}[${k}]`, v); + }); + } else { + append(key, value); + } + } + }; + + Object.entries(params).forEach(([key, value]) => { + process(key, value); + }); + + if (qs.length > 0) { + return `?${qs.join('&')}`; + } + + return ''; +}; + +const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => { + const encoder = config.ENCODE_PATH || encodeURI; + + const path = options.url + .replace('{api-version}', config.VERSION) + .replace(/{(.*?)}/g, (substring: string, group: string) => { + if (options.path?.hasOwnProperty(group)) { + return encoder(String(options.path[group])); + } + return substring; + }); + + const url = `${config.BASE}${path}`; + if (options.query) { + return `${url}${getQueryString(options.query)}`; + } + return url; +}; + +export const getFormData = (options: ApiRequestOptions): FormData | undefined => { + if (options.formData) { + const formData = new FormData(); + + const process = (key: string, value: any) => { + if (isString(value) || isBlob(value)) { + formData.append(key, value); + } else { + formData.append(key, JSON.stringify(value)); + } + }; + + Object.entries(options.formData) + .filter(([_, value]) => isDefined(value)) + .forEach(([key, value]) => { + if (Array.isArray(value)) { + value.forEach(v => process(key, v)); + } else { + process(key, value); + } + }); + + return formData; + } + return undefined; +}; + +type Resolver<T> = (options: ApiRequestOptions) => Promise<T>; + +export const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => { + if (typeof resolver === 'function') { + return (resolver as Resolver<T>)(options); + } + return resolver; +}; + +export const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions, formData?: FormData): Promise<Record<string, string>> => { + const [token, username, password, additionalHeaders] = await Promise.all([ + resolve(options, config.TOKEN), + resolve(options, config.USERNAME), + resolve(options, config.PASSWORD), + resolve(options, config.HEADERS), + ]); + + const formHeaders = typeof formData?.getHeaders === 'function' && formData?.getHeaders() || {} + + const headers = Object.entries({ + Accept: 'application/json', + ...additionalHeaders, + ...options.headers, + ...formHeaders, + }) + .filter(([_, value]) => isDefined(value)) + .reduce((headers, [key, value]) => ({ + ...headers, + [key]: String(value), + }), {} as Record<string, string>); + + if (isStringWithValue(token)) { + headers['Authorization'] = `Bearer ${token}`; + } + + if (isStringWithValue(username) && isStringWithValue(password)) { + const credentials = base64(`${username}:${password}`); + headers['Authorization'] = `Basic ${credentials}`; + } + + if (options.body !== undefined) { + if (options.mediaType) { + headers['Content-Type'] = options.mediaType; + } else if (isBlob(options.body)) { + headers['Content-Type'] = options.body.type || 'application/octet-stream'; + } else if (isString(options.body)) { + headers['Content-Type'] = 'text/plain'; + } else if (!isFormData(options.body)) { + headers['Content-Type'] = 'application/json'; + } + } + + return headers; +}; + +export const getRequestBody = (options: ApiRequestOptions): any => { + if (options.body) { + return options.body; + } + return undefined; +}; + +export const sendRequest = async <T>( + config: OpenAPIConfig, + options: ApiRequestOptions, + url: string, + body: any, + formData: FormData | undefined, + headers: Record<string, string>, + onCancel: OnCancel, + axiosClient: AxiosInstance +): Promise<AxiosResponse<T>> => { + const source = axios.CancelToken.source(); + + const requestConfig: AxiosRequestConfig = { + url, + headers, + data: body ?? formData, + method: options.method, + withCredentials: config.WITH_CREDENTIALS, + withXSRFToken: config.CREDENTIALS === 'include' ? config.WITH_CREDENTIALS : false, + cancelToken: source.token, + }; + + onCancel(() => source.cancel('The user aborted a request.')); + + try { + return await axiosClient.request(requestConfig); + } catch (error) { + const axiosError = error as AxiosError<T>; + if (axiosError.response) { + return axiosError.response; + } + throw error; + } +}; + +export const getResponseHeader = (response: AxiosResponse<any>, responseHeader?: string): string | undefined => { + if (responseHeader) { + const content = response.headers[responseHeader]; + if (isString(content)) { + return content; + } + } + return undefined; +}; + +export const getResponseBody = (response: AxiosResponse<any>): any => { + if (response.status !== 204) { + return response.data; + } + return undefined; +}; + +export const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => { + const errors: Record<number, string> = { + 400: 'Bad Request', + 401: 'Unauthorized', + 403: 'Forbidden', + 404: 'Not Found', + 500: 'Internal Server Error', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + ...options.errors, + } + + const error = errors[result.status]; + if (error) { + throw new ApiError(options, result, error); + } + + if (!result.ok) { + const errorStatus = result.status ?? 'unknown'; + const errorStatusText = result.statusText ?? 'unknown'; + const errorBody = (() => { + try { + return JSON.stringify(result.body, null, 2); + } catch (e) { + return undefined; + } + })(); + + throw new ApiError(options, result, + `Generic Error: status: ${errorStatus}; status text: ${errorStatusText}; body: ${errorBody}` + ); + } +}; + +/** + * Request method + * @param config The OpenAPI configuration object + * @param options The request options from the service + * @param axiosClient The axios client instance to use + * @returns CancelablePromise<T> + * @throws ApiError + */ +export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions, axiosClient: AxiosInstance = axios): CancelablePromise<T> => { + return new CancelablePromise(async (resolve, reject, onCancel) => { + try { + const url = getUrl(config, options); + const formData = getFormData(options); + const body = getRequestBody(options); + const headers = await getHeaders(config, options, formData); + + if (!onCancel.isCancelled) { + const response = await sendRequest<T>(config, options, url, body, formData, headers, onCancel, axiosClient); + const responseBody = getResponseBody(response); + const responseHeader = getResponseHeader(response, options.responseHeader); + + const result: ApiResult = { + url, + ok: isSuccess(response.status), + status: response.status, + statusText: response.statusText, + body: responseHeader ?? responseBody, + }; + + catchErrorCodes(options, result); + + resolve(result.body); + } + } catch (error) { + reject(error); + } + }); +}; diff --git a/src/api/index.ts b/src/api/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..7bfbd7a82dcdde4a855e2ee242508c2268633a83 --- /dev/null +++ b/src/api/index.ts @@ -0,0 +1,8 @@ +/* generated using openapi-typescript-codegen -- do not edit */ +/* istanbul ignore file */ +/* tslint:disable */ +/* eslint-disable */ +export { ApiError } from './core/ApiError'; +export { CancelablePromise, CancelError } from './core/CancelablePromise'; +export { OpenAPI } from './core/OpenAPI'; +export type { OpenAPIConfig } from './core/OpenAPI'; diff --git a/src/assets/icons/danger.svg b/src/assets/icons/danger.svg new file mode 100644 index 0000000000000000000000000000000000000000..609fd17d0541b7258e1374e6c7422b8ee3e696d9 --- /dev/null +++ b/src/assets/icons/danger.svg @@ -0,0 +1 @@ +<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" width="48"><path d="m40-120 440-760 440 760H40Zm104-60h672L480-760 144-180Zm340.175-57q12.825 0 21.325-8.675 8.5-8.676 8.5-21.5 0-12.825-8.675-21.325-8.676-8.5-21.5-8.5-12.825 0-21.325 8.675-8.5 8.676-8.5 21.5 0 12.825 8.675 21.325 8.676 8.5 21.5 8.5ZM454-348h60v-224h-60v224Zm26-122Z" fill="#ffffff"/></svg> \ No newline at end of file diff --git a/src/components/Buttons/Button1.vue b/src/components/Buttons/Button1.vue new file mode 100644 index 0000000000000000000000000000000000000000..33b300d7139220c9388160810541bafdf579332e --- /dev/null +++ b/src/components/Buttons/Button1.vue @@ -0,0 +1,17 @@ +<template> + <button type="button" class="btn btn-success" id="buttonStyle">{{ buttonText }}</button> +</template> + +<script> +export default { + props: ['buttonText'] +} +</script> + +<style scoped> + #buttonStyle { + padding: 0.5rem 4rem; + font-size: 1.5rem; + font-weight: 600; + } +</style> diff --git a/src/components/Exceptions/ErrorBoundaryCatcher.vue b/src/components/Exceptions/ErrorBoundaryCatcher.vue new file mode 100644 index 0000000000000000000000000000000000000000..3bc046fb271f7d3b3ce3acee559329d18dce53bb --- /dev/null +++ b/src/components/Exceptions/ErrorBoundaryCatcher.vue @@ -0,0 +1,19 @@ +<template> + <error-box :error-message="errorStore.getFirstError" @update:errorMessage="errorStore.removeCurrentError" /> + <slot /> + </template> + + <script setup lang="ts"> + import { onErrorCaptured } from 'vue'; + import { useErrorStore } from '@/stores/ErrorStore'; + import ErrorBox from '@/components/Exceptions/ErrorBox.vue'; + import handleUnknownError from '@/components/Exceptions/unkownErrorHandler'; + + const errorStore = useErrorStore(); + + onErrorCaptured((err, _vm, _info): boolean => { + const message = handleUnknownError(err.message); + errorStore.addError(message); //If no openAPI axios error, use err.message + return false; + }); + </script> \ No newline at end of file diff --git a/src/components/Exceptions/ErrorBox.vue b/src/components/Exceptions/ErrorBox.vue new file mode 100644 index 0000000000000000000000000000000000000000..f6dbe0d4adfb07e7ae1b4720ecf322b08fa3de71 --- /dev/null +++ b/src/components/Exceptions/ErrorBox.vue @@ -0,0 +1,90 @@ +<template> + <div class="error-box" v-if="errorMessage" data-testid="error-box"> + <span> + <v-icon name="bi-exclamation-triangle" /> + <button @click="wrapText = !wrapText" class="error-message-button"> + <h4>{{ errorMessage }}</h4> + </button> + <button class="error-remove-button" @click="$emit('update:errorMessage', '')" data-testid="hide-button"> + <v-icon scale="2" name="bi-dash" /> + </button> + </span> + </div> + </template> + + <script lang="ts"> + import { defineComponent } from 'vue'; + import { OhVueIcon, addIcons } from 'oh-vue-icons'; + import { BiDash, BiExclamationTriangle } from 'oh-vue-icons/icons'; + + addIcons(BiDash, BiExclamationTriangle); + + export default defineComponent({ + components: { + 'v-icon': OhVueIcon, + }, + props: { + errorMessage: { + type: String, + default: '', + }, + }, + data() { + return { + wrapText: false, + }; + }, + }); + </script> + + <style scoped> + .error-box { + position: fixed; + top: 2em; + left: 50%; + transform: translate(-50%, 0); + width: min(100%, 700px); + background-color: var(--red-color); + padding: 7px; + border-radius: 5px; + z-index: 1000; + } + + .error-box span { + display: flex; + justify-content: space-between; + align-items: center; + } + + .error-message-button { + white-space: v-bind('wrapText ? "normal" : "nowrap"'); + overflow: hidden; + border: none; + } + + .error-message-button h4 { + overflow: hidden; + text-overflow: ellipsis; + } + + .error-box * { + margin: 2px; + } + + .error-box button { + background-color: transparent; + box-shadow: none; + color: black; + cursor: pointer; + } + + .error-remove-button { + color: rgb(79, 77, 77); + border: solid black 0.5px; + padding: 0.3em; + } + + .error-box button:hover { + color: rgb(40, 38, 38); + } + </style> \ No newline at end of file diff --git a/src/components/Exceptions/unkownErrorHandler.ts b/src/components/Exceptions/unkownErrorHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..6f7b6d914edc88b03029a68b907f7ef45f05ee7e --- /dev/null +++ b/src/components/Exceptions/unkownErrorHandler.ts @@ -0,0 +1,19 @@ +import { ApiError as BackendApiError } from '@/api'; +import { AxiosError } from 'axios'; + +/** + * Finds the correct error message for the given error + * The message is then put into the error store + * which an error component can then display + * @param error The unknown error to handle + */ +const handleUnknownError = (error: any): string => { + if (error instanceof AxiosError) { + return error.code!!; + } else if (error instanceof BackendApiError) { + return error.body.detail ?? error.body; + } + return 'ContextErrorMessage'; +}; + +export default handleUnknownError; \ No newline at end of file diff --git a/src/components/InputFields/BaseInput.vue b/src/components/InputFields/BaseInput.vue new file mode 100644 index 0000000000000000000000000000000000000000..5558323b9a95e53825a222ac444c8a5ee5f8f23a --- /dev/null +++ b/src/components/InputFields/BaseInput.vue @@ -0,0 +1,37 @@ +<script setup lang="ts"> + +const props = defineProps({ + label: { + type: String, + default: "" + }, + type: { + type: String, + default: "text" + }, + placeholder: { + type: String, + default: "" + }, + inputId: { + type: String, + required: true + } +}); +</script> + +<template> + <div> + <label :for="inputId">{{ label }}</label> + <input :type="props.type" + class="form-control" + :placeholder="props.placeholder" + :id="inputId" required /> + <div class="invalid-feedback">Invalid {{ label }}</div> + <div class="valid-feedback">Correct {{ label }}</div> + </div> +</template> + +<style scoped> + +</style> \ No newline at end of file diff --git a/src/components/Login/Login.vue b/src/components/Login/Login.vue new file mode 100644 index 0000000000000000000000000000000000000000..25469a19d2cbd3c352545218fe480114ae8c6181 --- /dev/null +++ b/src/components/Login/Login.vue @@ -0,0 +1,13 @@ +<script setup lang="ts"> +import LoginForm from '@/components/Login/LoginForm.vue' +</script> + +<template> + <div class="container-fluid"> + <LoginForm/> + </div> +</template> + +<style> + +</style> \ No newline at end of file diff --git a/src/components/Login/LoginForm.vue b/src/components/Login/LoginForm.vue new file mode 100644 index 0000000000000000000000000000000000000000..6b0da2a71cf6b6b87f27c8feaff2ef259c951520 --- /dev/null +++ b/src/components/Login/LoginForm.vue @@ -0,0 +1,50 @@ +<script setup lang="ts"> +import BaseInput from '@/components/InputFields/BaseInput.vue' +import Button1 from '@/components/Buttons/Button1.vue' + +const handleSubmit = () => { + alert("Expected to be logged in when backend are finished") // Todo remove this line +} +</script> + +<template> + <div class="container-fluid"> + <form id="loginForm" @submit.prevent="handleSubmit"> + <BaseInput id="usernameInput" + input-id="username" + type="text" + label="Username" + placeholder="Enter username"/> + <BaseInput id="passwordInput" + input-id="password" + type="password" + label="Password" + placeholder="Enter password"/> + <button1 id="confirmButton" @click="handleSubmit" button-text="Login"></button1> + </form> + </div> +</template> + +<style scoped> +.container-fluid { + height: 91vh; + display: grid; + justify-items: center; + align-items: center; +} + +#usernameInput, #passwordInput, #confirmButton { + margin: 15px 0; +} + +#confirmButton { + justify-content: center; +} + +#loginForm { + display: flex; + flex-direction: column; + min-width: 280px; + width: 40%; +} +</style> \ No newline at end of file diff --git a/src/components/NewsComponents/NewsComponent.vue b/src/components/NewsComponents/NewsComponent.vue new file mode 100644 index 0000000000000000000000000000000000000000..4fe743f4a3af6146dfb226d11e5ac842e27d0677 --- /dev/null +++ b/src/components/NewsComponents/NewsComponent.vue @@ -0,0 +1,106 @@ +<script lang="ts"> +export default { + data() { + return { + articles: [] + }; + }, + mounted() { + this.fetchFinanceNews(); + // Call fetchFinanceNews() every 5 minutes (300,000 milliseconds) + // Done so the user does not need to refresh for news to be updated + // Might remove for consistent reading + setInterval(this.fetchFinanceNews, 300000); + }, + methods: { + async fetchFinanceNews() { + try { + const response = await fetch( + 'https://newsapi.org/v2/everything?q=saving%20money&pageSize=10&apiKey=f092756b3b6b41369b047cb7ae980db5' + ); + const data = await response.json(); + + //English articles, might want to translate to norwegian + this.articles = data.articles; + + + } catch (error) { + console.error('Error fetching saving money news:', error); + } + } + } +}; +</script> + + +<template> + <div class="center-box"> + <div class="box"> + <br> + <h1>Nyheter</h1> + <br> + <div v-for="(article, index) in articles" :key="index" class="article-container"> + <div class="content"> + <h3>{{ article.title }}</h3> + <p>{{ article.description }}</p> + <a :href="article.url" target="_blank">Read more</a> + </div> + <div class="image"> + <img :src="article.urlToImage" alt="Article Image"/> + </div> + </div> + </div> + </div> +</template> + +<style scoped> +.center-box { + display: flex; + justify-content: center; + align-items: center; +} + +.box { + width:90%; +} + +.article-container { + display: flex; + align-items: center; + margin-bottom: 30px; +} + +.image { + flex: 1; + text-align: center; + padding: 0 20px; +} + +.image img { + max-width: 100%; + border-radius: 1em; +} + +.content { + flex: 3; + padding: 0 20px; +} + +.content h3 { + margin-top: 0; +} + +.content a { + display: inline-block; + padding: 10px 20px; + background-color: #007bff; + color: #fff; + text-decoration: none; + border-radius: 5px; +} + +.content a:hover { + background-color: #0056b3; +} + +</style> \ No newline at end of file diff --git a/src/components/__tests__/HelloWorld.spec.ts b/src/components/__tests__/HelloWorld.spec.ts deleted file mode 100644 index 2533202008f7270910420c60a420efaf9b505c90..0000000000000000000000000000000000000000 --- a/src/components/__tests__/HelloWorld.spec.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { describe, it, expect } from 'vitest' - -import { mount } from '@vue/test-utils' -import HelloWorld from '../HelloWorld.vue' - -describe('HelloWorld', () => { - it('renders properly', () => { - const wrapper = mount(HelloWorld, { props: { msg: 'Hello Vitest' } }) - expect(wrapper.text()).toContain('Hello Vitest') - }) -}) diff --git a/src/router/index.ts b/src/router/index.ts index b5719b3468524ac9b958344ecafa5df349c6e0d3..5f3a683a89ec1edd6c1e2d20c2b31f46102ff1e1 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -1,6 +1,7 @@ // Import necessary dependencies from Vue Router and your views import { createRouter, createWebHistory } from 'vue-router'; import LoginView from '../views/Authentication/LoginView.vue'; +import { useUserInfoStore } from '@/stores/UserStore'; const routes = [ { @@ -19,6 +20,27 @@ const routes = [ name: 'not-found', component: () => import('@/views/NotFoundView.vue'), }, + { + path: '/news', + name: 'news', + component: () => import('@/views/NewsView.vue'), + }, + { + path: 'test', + name: 'test', + component: () => import('@/views/TestView.vue'), + }, + { + path: 'admin', + name: 'admin', + component: () => import('@/views/TestView.vue'), + meta: { requiresAdmin: true } + }, + { + path: 'unauthorized', + name: 'unauthorized', + component: () => import('@/views/UnauthorizedView.vue'), + }, ] }, { @@ -41,7 +63,15 @@ const router = createRouter({ }); router.beforeEach((to, from, next) => { + const requiresAuth = to.matched.some(record => record.meta.requiresAuth); + const requiresAdmin = to.matched.some(record => record.meta.requiresAdmin); + const userRole = useUserInfoStore().role; + + if (requiresAdmin && userRole !== 'admin') { + next({ name: 'unauthorized' }); + } else { next(); + } }); export default router; \ No newline at end of file diff --git a/src/stores/ErrorStore.ts b/src/stores/ErrorStore.ts new file mode 100644 index 0000000000000000000000000000000000000000..32ffb91af9d857c3ad747853265724d3843f4ea4 --- /dev/null +++ b/src/stores/ErrorStore.ts @@ -0,0 +1,33 @@ +import { defineStore } from 'pinia'; + +// A userstore which can be used to store several errors at the same time +export const useErrorStore = defineStore('ErrorStore', { + state: () => ({ + errors: [] as string[], + }), + actions: { + addError(error: string) { + console.log(error); + this.errors = [error]; + }, + removeCurrentError() { + if (this.errors.length > 0) { + this.errors.shift(); + } + }, + }, + getters: { + getFirstError(): string { + if (this.errors.length > 0) { + return `Exceptions.${this.errors[0]}`; + } + return ''; + }, + getLastError(): string { + if (this.errors.length > 0) { + return `Exceptions.${this.errors[this.errors.length - 1]}`; + } + return ''; + }, + }, +}); \ No newline at end of file diff --git a/src/stores/UserStore.ts b/src/stores/UserStore.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd1a32d7b5dab2a33b1b73b1df068f9688fd9415 --- /dev/null +++ b/src/stores/UserStore.ts @@ -0,0 +1,74 @@ +import { OpenAPI } from '@/api'; +import Cookies from 'js-cookie'; +import { defineStore } from 'pinia'; + +const cookiesStorage: Storage = { + setItem(key, state) { + return Cookies.set(key, state, { expires: 3 }); + }, + getItem(key) { + const store = Cookies.get(key); + if (store === undefined) { + OpenAPI.TOKEN = ''; + return ''; + } + + OpenAPI.TOKEN = JSON.parse(Cookies.get(key) || '').accessToken; + return Cookies.get(key) || ''; + }, + length: 0, + clear: function (): void { + Cookies.remove('userInfo'); + }, + key: function (index: number): string | null { + throw new Error('Function not implemented.'); + }, + removeItem: function (key: string): void { + throw new Error('Function not implemented.'); + }, +}; + +export type UserStoreInfo = { + username?: string; + firstname?: string; + lastname?: string; + accessToken?: string; + role?: string; +}; + +export const useUserInfoStore = defineStore('UserInfoStore', { + state: () => ({ + username: '', + firstname: '', + lastname: '', + accessToken: '', + role: '', + }), + actions: { + setUserInfo(userinfo: UserStoreInfo) { + userinfo.username && (this.$state.username = userinfo.username); + userinfo.firstname && (this.$state.firstname = userinfo.firstname); + userinfo.lastname && (this.$state.lastname = userinfo.lastname); + userinfo.accessToken && (this.$state.accessToken = userinfo.accessToken); + userinfo.accessToken && (OpenAPI.TOKEN = this.$state.accessToken); + userinfo.role && (this.$state.role = userinfo.role); + }, + clearUserInfo() { + this.$state.username = ''; + this.$state.firstname = ''; + this.$state.lastname = ''; + this.$state.accessToken = ''; + this.$state.role = ''; + OpenAPI.TOKEN = undefined; + }, + }, + getters: { + isLoggedIn(): boolean { + return this.accessToken !== ''; + }, + }, + persist: { + enabled: true, + strategies: [{ key: 'userInfo', storage: cookiesStorage }], + }, +}); \ No newline at end of file diff --git a/src/views/Authentication/LoginView.vue b/src/views/Authentication/LoginView.vue index 9095328987e0c5479f039dd59d667f0533d4cb71..a2aa0f410c32b81ef253338a1b657b19d732e442 100644 --- a/src/views/Authentication/LoginView.vue +++ b/src/views/Authentication/LoginView.vue @@ -1,3 +1,15 @@ +<script setup lang="ts"> +import Footer from '@/components/BaseComponents/Footer.vue' +import Menu from '@/components/BaseComponents/Menu.vue' +import Login from '@/components/Login/Login.vue' +</script> + <template> - Hallo -</template> \ No newline at end of file + <Menu/> + <Login/> + <Footer/> +</template> + +<style scoped> + +</style> \ No newline at end of file diff --git a/src/views/HomeView.vue b/src/views/HomeView.vue index 58542e7572cfd117267c3f71e29c25dc9ea000f7..f30d4244a9ec682c7a3c4d1c8a9bc3575779e181 100644 --- a/src/views/HomeView.vue +++ b/src/views/HomeView.vue @@ -1,7 +1,7 @@ <script setup lang="ts"> -import TheWelcome from '../components/TheWelcome.vue' + </script> <template> - Hallo + <RouterLink to="login">Login</RouterLink> </template> diff --git a/src/views/NewsView.vue b/src/views/NewsView.vue new file mode 100644 index 0000000000000000000000000000000000000000..bdfb7c611f43f0301e460f56b150a97213393a9d --- /dev/null +++ b/src/views/NewsView.vue @@ -0,0 +1,8 @@ +<script setup lang="ts"> +import NewsComponent from "@/components/NewsComponents/NewsComponent.vue"; +</script> + + +<template> + <NewsComponent></NewsComponent> +</template> \ No newline at end of file diff --git a/src/views/NotFoundView.vue b/src/views/NotFoundView.vue index 946391e88dbcaa2ac6d0d4964c5be9adda5a2421..650e2b20ce9dca2abb7fa351c48948a304fbb28e 100644 --- a/src/views/NotFoundView.vue +++ b/src/views/NotFoundView.vue @@ -1,5 +1,44 @@ <template> - <div> - <h1 id="errorMessage">404 - Not Found</h1> + <div class="container-fluid"> <!-- Changed from 'container' to 'container-fluid' --> + <div class="row"> + <div class="col-md-12"> + <div class="error-template text-center"> <!-- 'text-center' for centering text content --> + <h1> + Oops!</h1> + <h2> + 404 Not Found</h2> + <div class="error-details"> + Sorry, an error has occurred, Requested page not found! + </div> + <div class="error-actions"> + <Button1 button-text="Take Me Home" @click="home" /> + </div> + </div> + </div> </div> -</template> \ No newline at end of file +</div> +</template> + +<script setup lang="ts"> +import { useRouter } from 'vue-router'; +import Button1 from '@/components/Buttons/Button1.vue'; + +const router = useRouter(); + +const home = () => { + router.push('/'); // Assuming the root URL '/' is your home route +}; +</script> + + +<style scoped> + .error-template { + text-align: center; /* Ensures all text and inline elements within are centered */ + display: flex; + flex-direction: column; + align-items: center; /* Aligns child elements (which are block-level) centrally */ + justify-content: center; /* Optional: if you want vertical centering */ + margin: 2rem; +} + +</style> \ No newline at end of file diff --git a/src/views/TestView.vue b/src/views/TestView.vue new file mode 100644 index 0000000000000000000000000000000000000000..a7fce444288c4df4c5f9f531c3e92e3da43935a4 --- /dev/null +++ b/src/views/TestView.vue @@ -0,0 +1,11 @@ +<template> + <div> + <Button1 :buttonText="hallo"></Button1> + </div> +</template> + +<script setup lang="ts"> + import Button1 from '@/components/Buttons/Button1.vue' + + const hallo = 'Hallo' +</script> \ No newline at end of file diff --git a/src/views/UnauthorizedView.vue b/src/views/UnauthorizedView.vue new file mode 100644 index 0000000000000000000000000000000000000000..8a3f78ed6bdc87283db0ea40dbcdd5b2306c371d --- /dev/null +++ b/src/views/UnauthorizedView.vue @@ -0,0 +1,28 @@ +<template> + <body class="bg-dark text-white py-5"> + <div class="container py-5"> + <div class="row"> + <div class="col-md-2 text-center"> + <p><img src="@/assets/icons/danger.svg"> <br/>Status Code: 403</p> + </div> + <div class="col-md-10"> + <h3>OPPSSS!!!! Sorry...</h3> + <p>Sorry, your access is refused due to security reasons of our server and also our sensitive data.<br/>Please go back to the home page to continue browsing.</p> + <Button1 :button-text="'Take Me Home'" @click="home" /> + </div> + </div> + </div> + </body> +</template> + +<script setup lang="ts"> +import { defineComponent } from 'vue'; +import { useRouter } from 'vue-router'; +import Button1 from '@/components/Buttons/Button1.vue'; + +const router = useRouter(); + +const home = () => { + router.push('/'); +}; +</script>