From 6aeb7dcbc9957aa1f211840d7a5999797c3ba099 Mon Sep 17 00:00:00 2001 From: Bubka <858858+Bubka@users.noreply.github.com> Date: Mon, 29 Jan 2024 08:55:56 +0100 Subject: [PATCH] Add User management features to front-end --- resources/js/assets/app.scss | 26 +- resources/js/layouts/AdminTabs.vue | 35 +++ resources/js/layouts/Footer.vue | 7 +- resources/js/layouts/ListItem.vue | 17 ++ resources/js/router/index.js | 6 + resources/js/router/middlewares/adminOnly.js | 14 + resources/js/router/middlewares/authGuard.js | 1 + resources/js/services/userService.js | 69 ++++- resources/js/stores/user.js | 1 + resources/js/views/admin/AppSetup.vue | 58 ++++ resources/js/views/admin/Users.vue | 180 +++++++++++ resources/js/views/admin/users/Create.vue | 44 +++ resources/js/views/admin/users/Manage.vue | 304 +++++++++++++++++++ resources/js/views/auth/Login.vue | 2 + resources/js/views/settings/Options.vue | 32 -- resources/lang/en/admin.php | 81 +++++ resources/lang/en/auth.php | 2 +- resources/lang/en/commons.php | 3 + resources/lang/en/errors.php | 3 +- resources/lang/en/settings.php | 15 - 20 files changed, 844 insertions(+), 56 deletions(-) create mode 100644 resources/js/layouts/AdminTabs.vue create mode 100644 resources/js/layouts/ListItem.vue create mode 100644 resources/js/router/middlewares/adminOnly.js create mode 100644 resources/js/views/admin/AppSetup.vue create mode 100644 resources/js/views/admin/Users.vue create mode 100644 resources/js/views/admin/users/Create.vue create mode 100644 resources/js/views/admin/users/Manage.vue create mode 100644 resources/lang/en/admin.php diff --git a/resources/js/assets/app.scss b/resources/js/assets/app.scss index 875a6013..d0747da6 100644 --- a/resources/js/assets/app.scss +++ b/resources/js/assets/app.scss @@ -120,14 +120,18 @@ a:hover { } // has-text-weight-semibold / $weight-semibold -.group-item { +.group-item, .list-item { border-bottom: 1px solid $grey-lighter; padding: 0.75rem; } -:root[data-theme="dark"] .group-item { +:root[data-theme="dark"] .group-item, +:root[data-theme="dark"] .list-item { border-color: $grey-darker; color: $light; } +:root[data-theme="dark"] .list-item { + color: $grey-lighter; +} .group-item:first-of-type { margin-top: 2.5rem; @@ -386,6 +390,24 @@ a:hover { background: none !important; } +.is-left-bordered-link, +.is-left-bordered-warning, +.is-left-bordered-danger { + border: none; + border-left-style: solid; + border-left-width: 3px; + padding-left: $size-normal; +} +.is-left-bordered-link { + border-left-color: $link; +} +.is-left-bordered-warning { + border-left-color: $warning; +} +.is-left-bordered-danger { + border-left-color: $danger; +} + .add-icon-button { height: 64px; width: 64px; diff --git a/resources/js/layouts/AdminTabs.vue b/resources/js/layouts/AdminTabs.vue new file mode 100644 index 00000000..252cec35 --- /dev/null +++ b/resources/js/layouts/AdminTabs.vue @@ -0,0 +1,35 @@ + + + \ No newline at end of file diff --git a/resources/js/layouts/Footer.vue b/resources/js/layouts/Footer.vue index 2e95a5cf..571c97d8 100644 --- a/resources/js/layouts/Footer.vue +++ b/resources/js/layouts/Footer.vue @@ -46,8 +46,11 @@
- - {{ $t('settings.settings') }} + + {{ $t('settings.settings') }} + + + | {{ $t('admin.admin') }} - diff --git a/resources/js/layouts/ListItem.vue b/resources/js/layouts/ListItem.vue new file mode 100644 index 00000000..fcfa7806 --- /dev/null +++ b/resources/js/layouts/ListItem.vue @@ -0,0 +1,17 @@ + + + \ No newline at end of file diff --git a/resources/js/router/index.js b/resources/js/router/index.js index df42a5a0..236786c4 100644 --- a/resources/js/router/index.js +++ b/resources/js/router/index.js @@ -6,6 +6,7 @@ import { useAppSettingsStore } from '@/stores/appSettings' import { useNotifyStore } from '@/stores/notify' import authGuard from './middlewares/authGuard' +import adminOnly from './middlewares/adminOnly' import starter from './middlewares/starter' import noEmptyError from './middlewares/noEmptyError' import noRegistration from './middlewares/noRegistration' @@ -34,6 +35,11 @@ const router = createRouter({ { path: '/settings/webauthn/:credentialId/edit', name: 'settings.webauthn.editCredential', component: () => import('../views/settings/Credentials/Edit.vue'), meta: { middlewares: [authGuard], watchedByKicker: true, showAbout: true }, props: true }, { path: '/settings/webauthn', name: 'settings.webauthn.devices', component: () => import('../views/settings/WebAuthn.vue'), meta: { middlewares: [authGuard], watchedByKicker: true, showAbout: true } }, + { path: '/admin/app', name: 'admin.appSetup', component: () => import('../views/admin/AppSetup.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true } }, + { path: '/admin/users', name: 'admin.users', component: () => import('../views/admin/Users.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true } }, + { path: '/admin/users/create', name: 'admin.users.create', component: () => import('../views/admin/users/Create.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true } }, + { path: '/admin/users/:userId/manage', name: 'admin.users.manage', component: () => import('../views/admin/users/Manage.vue'), meta: { middlewares: [authGuard, adminOnly], watchedByKicker: true, showAbout: true }, props: true }, + { path: '/login', name: 'login', component: () => import('../views/auth/Login.vue'), meta: { middlewares: [skipIfAuthProxy, setReturnTo], showAbout: true } }, { path: '/register', name: 'register', component: () => import('../views/auth/Register.vue'), meta: { middlewares: [skipIfAuthProxy, noRegistration, setReturnTo], showAbout: true } }, { path: '/password/request', name: 'password.request', component: () => import('../views/auth/RequestReset.vue'), meta: { middlewares: [skipIfAuthProxy, setReturnTo], showAbout: true } }, diff --git a/resources/js/router/middlewares/adminOnly.js b/resources/js/router/middlewares/adminOnly.js new file mode 100644 index 00000000..1f109726 --- /dev/null +++ b/resources/js/router/middlewares/adminOnly.js @@ -0,0 +1,14 @@ +/** + * Allows an authenticated user to access the route only if he has administrator rights + */ +export default async function adminOnly({ to, next, nextMiddleware, stores }) { + const { user } = stores + const { notify } = stores + + if (! user.isAdmin) { + let err = new Error('unauthorized') + err.response.status = 403 + notify.error(err) + } + else nextMiddleware() +} \ No newline at end of file diff --git a/resources/js/router/middlewares/authGuard.js b/resources/js/router/middlewares/authGuard.js index 3e2a2025..1ab26fd7 100644 --- a/resources/js/router/middlewares/authGuard.js +++ b/resources/js/router/middlewares/authGuard.js @@ -9,6 +9,7 @@ export default async function authGuard({ to, next, nextMiddleware, stores }) { await authService.getCurrentUser({ returnError: true }).then(async (response) => { const currentUser = response.data await user.loginAs({ + id: currentUser.id, name: currentUser.name, email: currentUser.email, oauth_provider: currentUser.oauth_provider, diff --git a/resources/js/services/userService.js b/resources/js/services/userService.js index 5ad84e3e..fe60fac3 100644 --- a/resources/js/services/userService.js +++ b/resources/js/services/userService.js @@ -50,7 +50,7 @@ export default { * Get all user PATs * * @param {*} config - * @returns + * @returns promise */ getPersonalAccessTokens(config = {}) { return webClient.get('/oauth/personal-access-tokens', { ...config }) @@ -60,10 +60,73 @@ export default { * Delete a user PAT * * @param {*} tokenId - * @returns + * @returns promise */ deletePersonalAccessToken(tokenId, config = {}) { return webClient.delete('/oauth/personal-access-tokens/' + tokenId, { ...config }) - } + }, + + /** + * Get all registered users + * + * @returns promise + */ + getAll(config = {}) { + return apiClient.get('/users', { ...config }) + }, + + /** + * Get a registered user by id + * + * @returns promise + */ + getById(id, config = {}) { + return apiClient.get('/users/' + id, { ...config }) + }, + + /** + * Reset user password + * + * @returns promise + */ + resetPassword(id, config = {}) { + return apiClient.patch('/users/' + id + '/password/reset', {}, { ...config }) + }, + + /** + * Delete user + * + * @returns promise + */ + delete(id, config = {}) { + return apiClient.delete('/users/' + id, { ...config }) + }, + + /** + * Update user + * + * @returns promise + */ + update(id, payload, config = {}) { + return apiClient.patch('/users/' + id, payload, { ...config }) + }, + + /** + * Purge user's PATs + * + * @returns promise + */ + revokePATs(id, config = {}) { + return apiClient.delete('/users/' + id + '/pats', { ...config }) + }, + + /** + * Purge user's PATs + * + * @returns promise + */ + revokeWebauthnCredentials(id, config = {}) { + return apiClient.delete('/users/' + id + '/credentials', { ...config }) + }, } \ No newline at end of file diff --git a/resources/js/stores/user.js b/resources/js/stores/user.js index 74090626..b115741b 100644 --- a/resources/js/stores/user.js +++ b/resources/js/stores/user.js @@ -12,6 +12,7 @@ export const useUserStore = defineStore({ state: () => { return { + id: undefined, name: undefined, email: undefined, oauth_provider: undefined, diff --git a/resources/js/views/admin/AppSetup.vue b/resources/js/views/admin/AppSetup.vue new file mode 100644 index 00000000..cef98854 --- /dev/null +++ b/resources/js/views/admin/AppSetup.vue @@ -0,0 +1,58 @@ + + + \ No newline at end of file diff --git a/resources/js/views/admin/Users.vue b/resources/js/views/admin/Users.vue new file mode 100644 index 00000000..de6c0ce2 --- /dev/null +++ b/resources/js/views/admin/Users.vue @@ -0,0 +1,180 @@ + + + \ No newline at end of file diff --git a/resources/js/views/admin/users/Create.vue b/resources/js/views/admin/users/Create.vue new file mode 100644 index 00000000..d1d97d2e --- /dev/null +++ b/resources/js/views/admin/users/Create.vue @@ -0,0 +1,44 @@ + + + diff --git a/resources/js/views/admin/users/Manage.vue b/resources/js/views/admin/users/Manage.vue new file mode 100644 index 00000000..b69cab38 --- /dev/null +++ b/resources/js/views/admin/users/Manage.vue @@ -0,0 +1,304 @@ + + + + diff --git a/resources/js/views/auth/Login.vue b/resources/js/views/auth/Login.vue index aac75df3..8b8bee52 100644 --- a/resources/js/views/auth/Login.vue +++ b/resources/js/views/auth/Login.vue @@ -33,6 +33,7 @@ form.post('/user/login', {returnError: true}).then(async (response) => { await user.loginAs({ + id: response.data.id, name: response.data.name, email: response.data.email, oauth_provider: response.data.oauth_provider, @@ -62,6 +63,7 @@ webauthnService.authenticate(form.email).then(async (response) => { await user.loginAs({ + id: response.data.id, name: response.data.name, email: response.data.email, oauth_provider: response.data.oauth_provider, diff --git a/resources/js/views/settings/Options.vue b/resources/js/views/settings/Options.vue index 71d81348..15183c6e 100644 --- a/resources/js/views/settings/Options.vue +++ b/resources/js/views/settings/Options.vue @@ -1,18 +1,14 @@