@ghentcdh/authentication-vue 0.0.2-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ ---
2
+ title: Authentication
3
+ ---
4
+
5
+ # Use the GhentCDH keycloak libraries for authentication
6
+
7
+ ## Install the libraries
8
+
9
+ ```ssh
10
+ pnpm add @ghentcdh/auth/frontend @ghentcdh/auth/backend
11
+ ```
12
+
13
+ ## Frontend vuejs
14
+
15
+ Add following environment variables to the `.env` file.
16
+
17
+ ```
18
+ - VITE_KEYCLOAK_HOST=$KEYCLOAK_HOST
19
+ - VITE_KEYCLOAK_REALM=$KEYCLOAK_REALM
20
+ - VITE_KEYCLOAK_CLIENT_ID=$KEYCLOAK_CLIENT_ID
21
+ ```
22
+
23
+ ### Check if a user is logged in
24
+
25
+ ```vue
26
+
27
+ <script setup lang="ts">
28
+ import {useAuthenticationStore} from "@ghentcdh/authentication/frontend";
29
+
30
+ const authenticationStore = useAuthenticationStore();
31
+ </script>
32
+
33
+ <template>
34
+ <pre>user: {{ authenticationStore.user() }}</pre>
35
+ </template>
36
+
37
+ ```
38
+
39
+ ### Perform backend requests with the token
40
+
41
+ ```vue
42
+
43
+ <script setup lang="ts">
44
+ import {useHttpStore} from "@ghentcdh/authentication/frontend";
45
+
46
+ const httpStore = useHttpStore();
47
+
48
+ httpStore.post('/api/auth/login', {}).then(response => {
49
+ alert('login ok')
50
+ });
51
+ </script>
52
+
53
+
54
+ ```
55
+
56
+ > TODO list
57
+ > - [ ] Add roles guard to see if routes or parts of the application can be accessed by that user
58
+ > - [ ] Test if it's possible to have public routes
59
+ > - [ ] Add a
60
+ > - [ ] Add logout functionality
61
+
62
+ ## Backend nestjs
63
+
64
+ Add following environment variables to the `.env` file.
65
+
66
+ ```
67
+ - KEYCLOAK_HOST=$KEYCLOAK_HOST
68
+ - KEYCLOAK_REALM=$KEYCLOAK_REALM
69
+ ```
70
+
71
+ ### Secure requests
72
+
73
+ Add the module to your module.ts imports
74
+
75
+ ```typescript
76
+
77
+ import {AuthenticationApiModule} from "@ghentcdh/authentication/api";
78
+
79
+ @Module({
80
+ imports: [AuthenticationApiModule],
81
+ })
82
+ export class MyModule {
83
+ }
84
+
85
+ ```
86
+
87
+ Use the `@GhentCdhGuard` decorator to secure your routes
88
+
89
+ ```typescript
90
+ import {GhentCdhGuard} from "@ghentcdh/authentication/api";
91
+
92
+ @Controller()
93
+ export class MyController {
94
+ @UseGuards(GhentCdhGuard)
95
+ @Post('/secure')
96
+ async securePath(@User() user: any, @Request() req: any) {
97
+ return user;
98
+ }
99
+ }
100
+ ```
101
+
102
+ The `@User()` decorator will give you the user object from the keycloak token.
103
+
104
+
105
+ > TODO list
106
+ > - [ ] Add roles decorator and implement logic `@GhentCdhRoles(['admin'])`
107
+
108
+ ## Development environment
109
+
110
+ Make sure the following is added to your hosts file, if you are running a local keycloak instance. Internally the docker
111
+ containers cannot connect to the localhost:8080 port, so a hook is needed in terms of host rewrite.
112
+
113
+ ```sh
114
+ echo "127.0.0.1 authentication\n" | sudo tee -a /etc/hosts
115
+ ```
116
+
117
+ ## Setup docker
118
+
119
+ > TODO describe me
@@ -0,0 +1,21 @@
1
+ const vue = require('eslint-plugin-vue');
2
+ const baseConfig = require('../../../eslint.config.js');
3
+
4
+ module.exports = [
5
+ ...baseConfig,
6
+ ...vue.configs['flat/recommended'],
7
+ {
8
+ files: ['**/*.vue'],
9
+ languageOptions: {
10
+ parserOptions: {
11
+ parser: require('@typescript-eslint/parser'),
12
+ },
13
+ },
14
+ },
15
+ {
16
+ files: ['**/*.ts', '**/*.tsx', '**/*.js', '**/*.jsx', '**/*.vue'],
17
+ rules: {
18
+ 'vue/multi-word-component-names': 'off',
19
+ },
20
+ },
21
+ ];
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@ghentcdh/authentication-vue",
3
+ "version": "0.0.2-0",
4
+ "main": "./index.js",
5
+ "types": "./index.d.ts",
6
+ "dependencies": {
7
+ "vue": "^3.5.13",
8
+ "pinia": "^3.0.1",
9
+ "keycloak-js": "^26.1.2"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./index.mjs",
14
+ "require": "./index.js",
15
+ "types": "./index.d.ts"
16
+ }
17
+ },
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/GhentCDH/ghentcdh-monorepo.git",
21
+ "directory": "libs/authentication/vue"
22
+ }
23
+ }
package/project.json ADDED
@@ -0,0 +1,13 @@
1
+ {
2
+ "name": "authentication-vue",
3
+ "$schema": "../../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "libs/authentication/vue/src",
5
+ "projectType": "library",
6
+ "tags": [
7
+ "publish",
8
+ "scope:api",
9
+ "scope:feature"
10
+ ],
11
+ "// targets": "to see all targets run: nx show project authentication-vue --web",
12
+ "targets": {}
13
+ }
@@ -0,0 +1,46 @@
1
+ import { defineStore } from 'pinia';
2
+ import { ref, shallowRef, watch } from 'vue';
3
+
4
+ import { KeycloakAdapter } from './keycloak.adapter';
5
+
6
+ const AUTH_STORE_NAME = 'GHENT_CDH_AUTH_STORE';
7
+
8
+ export const useAuthenticationStore = defineStore(AUTH_STORE_NAME, () => {
9
+ const isAuthenticated = shallowRef(false);
10
+
11
+ const keycloackAdapter = ref<KeycloakAdapter>();
12
+
13
+ // create a promise initeDone
14
+ const initDone = ref(false);
15
+
16
+ KeycloakAdapter.init().then((adapter) => {
17
+ isAuthenticated.value = adapter.isAuthenticated;
18
+ keycloackAdapter.value = adapter;
19
+ initDone.value = true;
20
+ return adapter;
21
+ });
22
+
23
+ const logout = () => {
24
+ console.warn('logout');
25
+ };
26
+
27
+ return {
28
+ token: () => keycloackAdapter.value?.token,
29
+ user: () => keycloackAdapter.value?.userInfo,
30
+ isAuthenticated: () => keycloackAdapter.value?.isAuthenticated,
31
+ logout,
32
+ updateToken: async () => {
33
+ if (!initDone.value) {
34
+ await new Promise<void>((resolve) => {
35
+ const unwatch = watch(initDone, (newValue) => {
36
+ if (newValue) {
37
+ unwatch();
38
+ resolve();
39
+ }
40
+ });
41
+ });
42
+ }
43
+ return keycloackAdapter.value?.updateToken();
44
+ },
45
+ };
46
+ });
package/src/index.ts ADDED
@@ -0,0 +1,3 @@
1
+ export * from './keycloak.adapter';
2
+ export * from './authentication.store';
3
+ export * from './request.store';
@@ -0,0 +1,55 @@
1
+ import Keycloak from 'keycloak-js';
2
+
3
+
4
+ export class KeycloakAdapter extends Keycloak {
5
+
6
+
7
+ private constructor() {
8
+ const {VITE_KEYCLOAK_REALM, VITE_KEYCLOAK_HOST, VITE_KEYCLOAK_CLIENT_ID} = import.meta.env;
9
+
10
+ super({
11
+ url: VITE_KEYCLOAK_HOST,
12
+ realm: VITE_KEYCLOAK_REALM,
13
+ clientId: VITE_KEYCLOAK_CLIENT_ID
14
+ })
15
+
16
+ }
17
+
18
+ private async initialize() {
19
+ try {
20
+ const authenticated = await this.init(
21
+ {
22
+ onLoad: 'login-required'
23
+ }
24
+ );
25
+ if (authenticated) {
26
+ console.log('User is authenticated');
27
+ }
28
+ console.log('User is not authenticated');
29
+
30
+ } catch (error) {
31
+ console.error('Failed to initialize adapter:', error);
32
+ }
33
+ }
34
+
35
+
36
+ static async init(): Promise<KeycloakAdapter> {
37
+ const instance = new KeycloakAdapter();
38
+
39
+ await instance.initialize();
40
+ return instance;
41
+ }
42
+
43
+
44
+ get userInfo() {
45
+ return this.idTokenParsed
46
+ }
47
+
48
+ updateToken() {
49
+ return this.updateToken(30)
50
+ }
51
+
52
+ get isAuthenticated() {
53
+ return this.authenticated ?? false;
54
+ }
55
+ }
@@ -0,0 +1,126 @@
1
+ import { defineStore } from 'pinia';
2
+
3
+ import { useAuthenticationStore } from './authentication.store';
4
+
5
+ const AUTH_STORE_NAME = 'GHENT_CDH_HTTP_REQUEST';
6
+
7
+ // TODO add white list of request
8
+ type RequestOptions = {
9
+ skipAuth?: boolean;
10
+ queryParams?: Record<string, any>;
11
+ contentType?: string;
12
+ };
13
+
14
+ type Error = {
15
+ status: number;
16
+ content: any;
17
+ };
18
+
19
+ export const useHttpStore = defineStore(AUTH_STORE_NAME, () => {
20
+ const authStore = useAuthenticationStore();
21
+
22
+ const makeRequest = async (
23
+ url: string,
24
+ requestInit: RequestInit,
25
+ options: RequestOptions = { contentType: 'application/json' },
26
+ ) => {
27
+ const headers: Record<string, string> = {
28
+ accept: 'application/json',
29
+ ...(requestInit.headers ?? {}),
30
+ } as Record<string, string>;
31
+
32
+ if (options.contentType) {
33
+ headers['Content-Type'] = options.contentType;
34
+ }
35
+
36
+ if (!options?.skipAuth) {
37
+ await authStore.updateToken();
38
+ headers['Authorization'] = `Bearer ${authStore.token()}`;
39
+ }
40
+
41
+ const _url = new URL(url, window.location.href);
42
+
43
+ if (options?.queryParams) {
44
+ for (const [key, value] of Object.entries(options.queryParams)) {
45
+ _url.searchParams.set(key, value);
46
+ }
47
+ }
48
+
49
+ const response = await fetch(_url.toString(), {
50
+ ...requestInit,
51
+ headers,
52
+ });
53
+
54
+ if (!response.ok) {
55
+ if (!options?.skipAuth) {
56
+ // TODO if response return 400 then redirect to login page
57
+ }
58
+
59
+ return Promise.reject({
60
+ content: response.body,
61
+ status: response.status,
62
+ } as any);
63
+ }
64
+
65
+ return response.json();
66
+ };
67
+
68
+ return {
69
+ get: <T>(url: string, options?: RequestOptions): Promise<T> =>
70
+ makeRequest(url, { method: 'GET' }, options),
71
+ postFile: <T>(
72
+ url: string,
73
+ file: File,
74
+ data: any = {},
75
+ options?: RequestOptions,
76
+ ): Promise<T> => {
77
+ const formData = new FormData();
78
+
79
+ for (const name in data) {
80
+ formData.append(name, data[name]);
81
+ }
82
+
83
+ formData.append('file', file);
84
+
85
+ return makeRequest(
86
+ url,
87
+ {
88
+ method: 'POST',
89
+ body: formData,
90
+ },
91
+ { ...options, contentType: undefined },
92
+ );
93
+ },
94
+ post: <T>(url: string, data: any, options?: RequestOptions): Promise<T> =>
95
+ makeRequest(
96
+ url,
97
+ {
98
+ method: 'POST',
99
+ body: JSON.stringify(data),
100
+ },
101
+ options,
102
+ ),
103
+ patch: <T>(url: string, data: any, options?: RequestOptions): Promise<T> =>
104
+ makeRequest(
105
+ url,
106
+ {
107
+ method: 'PATCH',
108
+ body: JSON.stringify(data),
109
+ },
110
+ options,
111
+ ),
112
+ delete: <T>(
113
+ url: string,
114
+ data?: any,
115
+ options?: RequestOptions,
116
+ ): Promise<T> =>
117
+ makeRequest(
118
+ url,
119
+ {
120
+ method: 'DELETE',
121
+ body: JSON.stringify(data),
122
+ },
123
+ options,
124
+ ),
125
+ };
126
+ });
@@ -0,0 +1,7 @@
1
+ declare module '*.vue' {
2
+ import type { defineComponent } from 'vue';
3
+
4
+
5
+ const component: ReturnType<typeof defineComponent>;
6
+ export default component;
7
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "compilerOptions": {
3
+ "allowJs": true,
4
+ "esModuleInterop": false,
5
+ "allowSyntheticDefaultImports": true,
6
+ "strict": true,
7
+ "jsx": "preserve",
8
+ "jsxImportSource": "vue",
9
+ "moduleResolution": "node",
10
+ "resolveJsonModule": true
11
+ },
12
+ "files": [],
13
+ "include": [],
14
+ "references": [
15
+ {
16
+ "path": "./tsconfig.lib.json"
17
+ },
18
+ {
19
+ "path": "./tsconfig.spec.json"
20
+ }
21
+ ],
22
+ "extends": "../../../tsconfig.base.json"
23
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../../dist/out-tsc",
5
+ "types": ["vite/client"]
6
+ },
7
+ "exclude": [
8
+ "src/**/__tests__/*",
9
+ "src/**/*.spec.vue",
10
+ "src/**/*.test.vue",
11
+ "vite.config.ts",
12
+ "vite.config.mts",
13
+ "vitest.config.ts",
14
+ "vitest.config.mts",
15
+ "src/**/*.test.ts",
16
+ "src/**/*.spec.ts",
17
+ "src/**/*.test.tsx",
18
+ "src/**/*.spec.tsx",
19
+ "src/**/*.test.js",
20
+ "src/**/*.spec.js",
21
+ "src/**/*.test.jsx",
22
+ "src/**/*.spec.jsx"
23
+ ],
24
+ "include": [
25
+ "src/**/*.js",
26
+ "src/**/*.jsx",
27
+ "src/**/*.ts",
28
+ "src/**/*.tsx",
29
+ "src/**/*.vue"
30
+ ]
31
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "outDir": "../../../dist/out-tsc",
5
+ "types": [
6
+ "vitest/globals",
7
+ "vitest/importMeta",
8
+ "vite/client",
9
+ "node",
10
+ "vitest"
11
+ ]
12
+ },
13
+ "include": [
14
+ "vite.config.ts",
15
+ "vite.config.mts",
16
+ "vitest.config.ts",
17
+ "vitest.config.mts",
18
+ "src/**/*.test.ts",
19
+ "src/**/*.spec.ts",
20
+ "src/**/*.test.tsx",
21
+ "src/**/*.spec.tsx",
22
+ "src/**/*.test.js",
23
+ "src/**/*.spec.js",
24
+ "src/**/*.test.jsx",
25
+ "src/**/*.spec.jsx",
26
+ "src/**/*.d.ts"
27
+ ]
28
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,61 @@
1
+ /// <reference types='vitest' />
2
+ import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin';
3
+ import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin';
4
+ import vue from '@vitejs/plugin-vue';
5
+ import { defineConfig } from 'vite';
6
+ import dts from 'vite-plugin-dts';
7
+
8
+ import * as path from 'path';
9
+
10
+
11
+ export default defineConfig({
12
+ root: __dirname,
13
+ cacheDir: '../../../node_modules/.vite/libs/authentication/vue',
14
+ plugins: [
15
+ vue(),
16
+ nxViteTsPaths(),
17
+ nxCopyAssetsPlugin(['*.md']),
18
+ dts({
19
+ entryRoot: 'src',
20
+ tsconfigPath: path.join(__dirname, 'tsconfig.lib.json'),
21
+ }),
22
+ ],
23
+ // Uncomment this if you are using workers.
24
+ // worker: {
25
+ // plugins: [ nxViteTsPaths() ],
26
+ // },
27
+ // Configuration for building your library.
28
+ // See: https://vitejs.dev/guide/build.html#library-mode
29
+ build: {
30
+ outDir: '../../../dist/libs/authentication/vue',
31
+ emptyOutDir: true,
32
+ reportCompressedSize: true,
33
+ commonjsOptions: {
34
+ transformMixedEsModules: true,
35
+ },
36
+ lib: {
37
+ // Could also be a dictionary or array of multiple entry points.
38
+ entry: 'src/index.ts',
39
+ name: 'authentication-vue',
40
+ fileName: 'index',
41
+ // Change this to the formats you want to support.
42
+ // Don't forget to update your package.json as well.
43
+ formats: ['es'],
44
+ },
45
+ rollupOptions: {
46
+ // External packages that should not be bundled into your library.
47
+ external: [],
48
+ },
49
+ },
50
+ test: {
51
+ watch: false,
52
+ globals: true,
53
+ environment: 'jsdom',
54
+ include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
55
+ reporters: ['default'],
56
+ coverage: {
57
+ reportsDirectory: '../../../coverage/libs/authentication/vue',
58
+ provider: 'v8',
59
+ },
60
+ },
61
+ });