diff --git a/fda-ui/api/authentication.service.js b/fda-ui/api/authentication.service.js index d031ead6bac74c9b4e3e51e04332497bf5cf9be2..43bbf8274277da3ef53d03edddb200dd8edef9f4 100644 --- a/fda-ui/api/authentication.service.js +++ b/fda-ui/api/authentication.service.js @@ -1,6 +1,7 @@ import Vue from 'vue' +import store from '@/store' import qs from 'qs' -import { setRefreshToken, setToken } from '@/server-middleware/store' +import UserMapper from '@/api/user.mapper' import axios from 'axios' import { clientSecret } from '@/config' @@ -26,6 +27,15 @@ class AuthenticationService { client_secret: clientSecret, scope: 'openid profile roles attributes' } + if (!username) { + throw new Error('parameter username is empty') + } + if (!password) { + throw new Error('parameter password is empty') + } + if (!clientSecret) { + throw new Error('parameter clientSecret is empty') + } return this._authenticate(payload) } @@ -36,6 +46,12 @@ class AuthenticationService { client_secret: clientSecret, refresh_token: refreshToken } + if (!refreshToken) { + throw new Error('parameter refreshToken is empty') + } + if (!clientSecret) { + throw new Error('parameter clientSecret is empty') + } return this._authenticate(payload) } @@ -50,8 +66,10 @@ class AuthenticationService { // eslint-disable-next-line camelcase const { access_token, refresh_token } = authentication console.debug('response authenticate', authentication) - setToken(access_token) - setRefreshToken(refresh_token) + store().commit('SET_TOKEN', access_token) + store().commit('SET_REFRESH_TOKEN', refresh_token) + const user = UserMapper.tokenToUser(access_token) + store().commit('SET_USER', user) resolve(authentication) }).catch((error) => { console.error('Failed to authenticate', error) diff --git a/fda-ui/api/user.mapper.js b/fda-ui/api/user.mapper.js new file mode 100644 index 0000000000000000000000000000000000000000..dec8f31910d956c07a2946ae3416b56d8be3098d --- /dev/null +++ b/fda-ui/api/user.mapper.js @@ -0,0 +1,32 @@ +import jwtDecode from 'jwt-decode' + +class UserMapper { + tokenToUser (token) { + const data = jwtDecode(token) + return { + id: data.sub, + firstname: data.given_name || null, + lastname: data.family_name || null, + username: data.client_id, + roles: data.realm_access.roles || [], + attributes: data.attributes || [] + } + } + + tokenToRoles (token) { + const data = jwtDecode(token) + if (!data) { + return [] + } + return data.realm_access.roles || [] + } + + getThemeDark (user) { + if (!user || !user.attributes || user.attributes.filter(a => a.name === 'theme_dark').length === 0) { + return false + } + return user.attributes.filter(a => a.name === 'theme_dark')[0].value === 'true' + } +} + +export default new UserMapper() diff --git a/fda-ui/api/user.service.js b/fda-ui/api/user.service.js new file mode 100644 index 0000000000000000000000000000000000000000..c86f1b938972077337916c072f793fb3eb724542 --- /dev/null +++ b/fda-ui/api/user.service.js @@ -0,0 +1,91 @@ +import Vue from 'vue' +import api from '@/api' + +class UserService { + findAll () { + return new Promise((resolve, reject) => { + api.get('/api/user', { headers: { Accept: 'application/json' } }) + .then((response) => { + const users = response.data + console.debug('response users', users) + resolve(users) + }) + .catch((error) => { + const { code, message } = error + console.error('Failed to load users', error) + Vue.$toast.error(`[${code}] Failed to load users: ${message}`) + reject(error) + }) + }) + } + + findOne (id) { + return new Promise((resolve, reject) => { + api.get(`/api/user/${id}`, { headers: { Accept: 'application/json' } }) + .then((response) => { + const user = response.data + console.debug('response user', user) + resolve(user) + }).catch((error) => { + const { code, message } = error + console.error('Failed to load user', error) + Vue.$toast.error(`[${code}] Failed to load user: ${message}`) + reject(error) + }) + }) + } + + create (data) { + return new Promise((resolve, reject) => { + api.post('/api/user', data, { headers: { Accept: 'application/json' } }) + .then((response) => { + const user = response.data + console.debug('response user', user) + resolve(user) + }).catch((error) => { + const { code, message, response } = error + const { status } = response + if (status === 417) { + Vue.$toast.error(`[${code}] This e-mail address is taken: ${message}`) + } else if (status === 409) { + Vue.$toast.error(`[${code}] This username is taken: ${message}`) + } else if (status === 428) { + Vue.$toast.warning(`[${code}] Account was created: ${message}`) + } else { + Vue.$toast.error(`[${code}] Failed to create user: ${message}`) + } + console.error('Failed to create user', error) + this.loading = false + reject(error) + }) + }) + } + + updatePassword (id, password) { + return new Promise((resolve, reject) => { + api.post(`/api/user/${id}/password`, { password }, { headers: { Accept: 'application/json' } }) + .then(() => resolve()) + .catch((error) => { + const { code, message } = error + console.error('Failed to update user password', error) + Vue.$toast.error(`[${code}] Failed to update user password: ${message}`) + reject(error) + }) + }) + } + + updateTheme (id, themeDark) { + return new Promise((resolve, reject) => { + api.post(`/api/user/${id}/theme`, { theme_dark: themeDark }, { headers: { Accept: 'application/json' } }) + .then(() => resolve()) + .catch((error) => { + const { code, message } = error + console.error('Failed to update user theme', error) + Vue.$toast.error(`[${code}] Failed to update user theme: ${message}`) + reject(error) + }) + }) + } +} + +export default new UserService() diff --git a/fda-ui/layouts/default.vue b/fda-ui/layouts/default.vue index d342b1e299ca1db50fe4494fdb721f46219549fe..9b8acb966419c1c31d365b74716070ab7c299d86 100644 --- a/fda-ui/layouts/default.vue +++ b/fda-ui/layouts/default.vue @@ -68,20 +68,6 @@ to="/signup"> <v-icon left>mdi-account-plus</v-icon> Signup </v-btn> - <v-btn v-if="user" to="/user" plain> - {{ user.username }} <sup v-if="isDeveloper"> - <v-tooltip bottom> - <template v-slot:activator="{ on, attrs }"> - <v-icon - color="primary" - small - v-bind="attrs" - v-on="on">mdi-check-decagram</v-icon> - </template> - <span>Developer</span> - </v-tooltip> - </sup> - </v-btn> <v-menu v-if="user" bottom offset-y left> <template v-slot:activator="{ on, attrs }"> <v-btn @@ -125,11 +111,14 @@ </v-card-text> </v-card> </v-footer> + <pre>{{ $store.state }}</pre> </v-app> </template> <script> import { isDeveloper } from '@/utils' +import AuthenticationService from '@/api/authentication.service' + export default { name: 'DefaultLayout', data () { @@ -205,6 +194,13 @@ export default { } }, watch: { + $route: { + handler () { + if (this.refreshToken) { + AuthenticationService.authenticateToken(this.refreshToken) + } + } + }, '$route.params.database_id': { handler (id, oldId) { if (this.user) { @@ -212,7 +208,7 @@ export default { } if (id !== oldId) { this.loadDatabase() - this.loadAccess() + // this.loadAccess() } }, deep: true, @@ -254,6 +250,7 @@ export default { this.$toast.warning(message) } this.$store.commit('SET_TOKEN', null) + this.$store.commit('SET_REFRESH_TOKEN', null) this.$store.commit('SET_ROLES', []) this.$store.commit('SET_USER', null) this.$store.commit('SET_ACCESS', null) diff --git a/fda-ui/pages/login.vue b/fda-ui/pages/login.vue index 2df061095a430aa5a0431d5948a67fecad99ecfa..d77f7df45a85fe8363992d6964a2086f84eaf777 100644 --- a/fda-ui/pages/login.vue +++ b/fda-ui/pages/login.vue @@ -59,6 +59,7 @@ <script> import AuthenticationService from '@/api/authentication.service' +import UserMapper from '@/api/user.mapper' export default { data () { return { @@ -94,15 +95,6 @@ export default { } } }, - mounted () { - if (this.$route.query.email_verified !== undefined) { - console.info('Successfully verified your E-Mail Address') - this.$toast.success('Successfully verified your E-Mail Address!') - } else if (this.$route.query.password_reset !== undefined) { - console.info('Successfully reset password') - this.$toast.success('Successfully reset password!') - } - }, methods: { submit () { this.$refs.form.validate() @@ -110,20 +102,16 @@ export default { login () { this.loading = true AuthenticationService.authenticatePlain(this.username, this.password) - .then(() => this.$router.push({ path: '/container' })) - this.loading = false + .then(() => { + const themeDark = UserMapper.getThemeDark(this.user) + console.debug('theme_dark', themeDark) + this.$vuetify.theme.dark = themeDark + this.$router.push('/container') + }) + .catch(() => { + this.loading = false + }) }, - // async setTheme () { - // try { - // const res = await findUser(this.token) - // const user = res.data - // console.debug('user', user) - // this.$store.commit('SET_USER', user) - // this.$vuetify.theme.dark = getThemeDark(user) - // } catch (error) { - // console.error('Failed to set theme', error) - // } - // }, signup () { this.$router.push('/signup') }, diff --git a/fda-ui/pages/signup.vue b/fda-ui/pages/signup.vue index cb243ee531ebe1f189c119985a002eb5d27089c1..f684bf48dc46e9c5449f1528bf0163b3147f1ccb 100644 --- a/fda-ui/pages/signup.vue +++ b/fda-ui/pages/signup.vue @@ -104,6 +104,7 @@ </template> <script> +import UserService from '@/api/user.service' export default { data () { return { @@ -135,39 +136,17 @@ export default { submit () { this.$refs.form.validate() }, - async register () { - const url = '/api/user' - try { - this.loading = true - const res = await this.$axios.post(url, this.createAccount) - console.debug('create user', res.data) - this.$toast.success(`Success! ${this.mailVerify ? 'Check your inbox!' : ''}`) - this.$router.push('/login') - } catch (err) { - if (err.response !== undefined && err.response.status !== undefined) { - if (err.response.status === 417) { - this.$toast.error('This e-mail address is taken') - console.error('email taken', err) - this.loading = false - return - } - if (err.response.status === 409) { - this.$toast.error('This username is taken') - console.error('username taken', err) - this.loading = false - return - } - if (err.response.status === 428) { - this.$toast.warning('Account was created but the server failed to send a mail') - console.warn('email sending failed', err) - this.loading = false - return - } - } - console.error('create user failed', err) - this.$toast.error('Failed to create user') - } - this.loading = false + register () { + this.loading = true + UserService.create(this.createAccount) + .then(() => { + this.$toast.success(`Success! ${this.mailVerify ? 'Check your inbox!' : ''}`) + this.$router.push('/login') + this.loading = false + }) + .catch(() => { + this.loading = false + }) } } } diff --git a/fda-ui/plugins/axios.js b/fda-ui/plugins/axios.js index 46eb62841682562c308cb1098049d95727e8d623..f627cf9093166194f6bbe47b51baba5a0d9360c6 100644 --- a/fda-ui/plugins/axios.js +++ b/fda-ui/plugins/axios.js @@ -1,27 +1,29 @@ import Vue from 'vue' +import store from '@/store' import api from '@/api' import AuthenticationService from '@/api/authentication.service' -import { getRefreshToken, getToken, setRefreshToken, setToken } from '@/server-middleware/store' import jwtDecode from 'jwt-decode' api.interceptors.request.use((config) => { - const token = getToken() + const token = store().state.token if (!token) { return config } const { exp } = jwtDecode(token) if (new Date(exp) <= new Date()) { /* token expired */ - const refreshToken = getRefreshToken() + const refreshToken = store().state.refreshToken const { exp2 } = jwtDecode(refreshToken) if (new Date(exp2) <= new Date()) { /* refresh token expired */ - setToken(null) - setRefreshToken(null) + store().commit('SET_TOKEN', null) + store().commit('SET_REFRESH_TOKEN', null) console.warn('Refresh token expired') } AuthenticationService.authenticateToken(refreshToken) - return config + .then(() => { + return config + }) } console.debug('interceptor inject authorization header', exp) config.headers.Authorization = `Bearer ${token}` diff --git a/fda-ui/server-middleware/store.js b/fda-ui/server-middleware/store.js deleted file mode 100644 index 4c63653242c4aabbaa6d7deb9148c1cc744e624c..0000000000000000000000000000000000000000 --- a/fda-ui/server-middleware/store.js +++ /dev/null @@ -1,57 +0,0 @@ -export function setToken (value) { - const state = _getState() - state.token = value - _setState(state) -} - -export function getToken () { - const state = _getState() - return state.token -} - -export function setRefreshToken (value) { - const state = _getState() - state.refresh_token = value - _setState(state) -} - -export function getRefreshToken () { - const state = _getState() - return state.refresh_token -} - -export function setUser (value) { - const state = _getState() - state.user = value - _setState(state) -} - -export function getUser () { - const state = _getState() - return state.user -} - -export function _getState () { - if (!JSON.parse(localStorage.getItem('vuex'))) { - init() - } - return JSON.parse(localStorage.getItem('vuex')) -} - -function _setState (state) { - const json = JSON.stringify(state) - localStorage.setItem('vuex', json) -} - -function init () { - const state = { - token: null, - roles: [], - user: null, - database: null, - table: null, - access: null - } - localStorage.setItem('vuex', JSON.stringify(state)) - console.debug('initialized vuex state') -} diff --git a/fda-ui/store/index.js b/fda-ui/store/index.js index e98dbe5ffa4f24cf44001b68363d0b3999ce8b17..3de9499b7a4ecf8e0be4a51881e7a45a8b164d96 100644 --- a/fda-ui/store/index.js +++ b/fda-ui/store/index.js @@ -1,32 +1,52 @@ -export const state = () => ({ - token: null, - roles: [], - user: null, - database: null, - table: null, - access: null -}) +import Vue from 'vue' +import Vuex, { Store } from 'vuex' + +Vue.use(Vuex) -export const mutations = { - SET_DATABASE (state, database) { - state.database = database +// https://github.com/hua1995116/webchat/blob/7c6544d3defd41cb7cf68306accea97800858bc3/client/src/store/index.js#L293 +const store = new Store({ + state: { + token: null, + refreshToken: null, + roles: [], + user: null, + database: null, + table: null, + access: null }, - SET_TOKEN (state, token) { - state.token = token + getters: { + getToken: state => state.token, + getRefreshToken: state => state.refreshToken, + getRoles: state => state.roles, + getUser: state => state.user, + getDatabase: state => state.database, + getTable: state => state.table, + getAccess: state => state.access }, - SET_USER (state, user) { - if (user != null && user.token) { - delete user.token + mutations: { + SET_TOKEN (state, token) { + state.token = token + }, + SET_REFRESH_TOKEN (state, refreshToken) { + state.refreshToken = refreshToken + }, + SET_ROLES (state, roles) { + state.roles = roles + }, + SET_USER (state, user) { + state.user = user + }, + SET_DATABASE (state, database) { + state.database = database + }, + SET_TABLE (state, table) { + state.table = table + }, + SET_ACCESS (state, access) { + state.access = access } - state.user = user - }, - SET_ROLES (state, roles) { - state.roles = roles }, - SET_ACCESS (state, access) { - state.access = access - }, - SET_TABLE (state, table) { - state.table = table + actions: { } -} +}) +export default () => store