diff --git a/frontend/src/PasswordChangeDialog.vue b/frontend/src/PasswordChangeDialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..c81be2b9714f4a127fa75e3a9a455386bd3cbe8d --- /dev/null +++ b/frontend/src/PasswordChangeDialog.vue @@ -0,0 +1,101 @@ +<template> + <v-dialog v-model="show" width="30%"> + <v-card> + <v-card-title class="title">Change your password</v-card-title> + <v-card-text> + <v-form class="mx-4"> + <v-text-field + label="Current password" + type="password" + v-model="currentPassword" + autofocus + required + /> + <v-text-field + label="New password" + type="password" + v-model="newPassword" + required + /> + <v-text-field + label="Repeat new password" + type="password" + v-model="newPasswordRepeated" + :error-messages="errorMessageRepeat" + required + /> + </v-form> + </v-card-text> + <v-card-actions> + <v-btn @click="submitChange" :disabled="!allowChange">Change password</v-btn> + <v-btn @click="$emit('hide')" color="red">Cancel</v-btn> + </v-card-actions> + </v-card> + </v-dialog> +</template> + +<script> + import {mapState} from 'vuex' + import { changePassword } from '@/api' + + export default { + name: 'PasswordChangeDialog', + data () { + return { + show: true, + currentPassword: '', + newPassword: '', + newPasswordRepeated: '' + } + }, + computed: { + ...mapState({ + userPk: state => state.authentication.user.pk + }), + equalNewPasswords () { + return this.newPassword === this.newPasswordRepeated + }, + allowChange () { + return this.equalNewPasswords && !!this.currentPassword + }, + errorMessageRepeat () { + if (!this.equalNewPasswords) { + return 'Repeated new password is different than new one' + } + } + }, + methods: { + submitChange () { + const data = { + old_password: this.currentPassword, + new_password: this.newPassword + } + changePassword(this.userPk, data).then(() => { + this.$notify({ + title: 'Success!', + text: 'Successfully changed password!', + type: 'success' + }) + this.$emit('hide') + }).catch(() => { + this.$notify({ + title: 'Error!', + text: 'Unable to change password', + type: 'error' + }) + }) + } + }, + watch: { + show (val) { + if (!val) { + this.$emit('hide') + } + } + } + } +</script> + +<style scoped> + +</style> diff --git a/frontend/src/api.js b/frontend/src/api.js index d992c3b3fb1642cd5c6ea285cf71d73cc7abd933..3a54904fa72ba5cfd6a0fe0868ab22cf98de5635 100644 --- a/frontend/src/api.js +++ b/frontend/src/api.js @@ -14,7 +14,6 @@ function getInstanceBaseUrl () { let ax = axios.create({ baseURL: getInstanceBaseUrl() - // headers: {'Authorization': 'JWT ' + sessionStorage.getItem('token')} }) { let token = sessionStorage.getItem('token') @@ -190,4 +189,12 @@ export async function deactivateAllStudentAccess () { return ax.post('/api/student/deactivate/') } +export async function changePassword (userPk, data) { + return ax.patch(`/api/user/${userPk}/change_password/`, data) +} + +export async function getOwnUser () { + return (await ax.get('/api/user/me/')).data +} + export default ax diff --git a/frontend/src/components/BaseLayout.vue b/frontend/src/components/BaseLayout.vue index 637235207c0bc6f8371083d78a75d3dc068733ef..d49771f643f50b5581dc212099d5da0e39855b4b 100644 --- a/frontend/src/components/BaseLayout.vue +++ b/frontend/src/components/BaseLayout.vue @@ -70,7 +70,12 @@ <v-spacer/> <slot name="toolbar-center"/> <div class="toolbar-content"> - <span>{{ userRole }} | {{ username }}</span> + <v-menu bottom offset-y> + <v-btn slot="activator" color="cyan" style="text-transform: none"> + {{ userRole }} | {{ username }} <v-icon>arrow_drop_down</v-icon> + </v-btn> + <user-options/> + </v-menu> </div> <v-btn color="blue darken-1" to="/" @click.native="logout">Logout</v-btn> <slot name="toolbar-right"></slot> @@ -82,15 +87,17 @@ import { mapGetters, mapState } from 'vuex' import {uiMut} from '@/store/modules/ui' import { createComputedGetterSetter } from '@/util/helpers' + import UserOptions from '@/components/UserOptions' export default { name: 'base-layout', + components: {UserOptions}, computed: { ...mapGetters([ 'gradySpeak' ]), ...mapState({ - username: state => state.authentication.username, - userRole: state => state.authentication.userRole + username: state => state.authentication.user.username, + userRole: state => state.authentication.user.role }), darkMode: createComputedGetterSetter({ path: 'ui.darkMode', diff --git a/frontend/src/components/UserOptions.vue b/frontend/src/components/UserOptions.vue new file mode 100644 index 0000000000000000000000000000000000000000..fd72a71ee7ef937c4d79de2034e2fe77c715199c --- /dev/null +++ b/frontend/src/components/UserOptions.vue @@ -0,0 +1,45 @@ +<template> + <div> + <v-list> + <template v-for="(opt, i) in userOptions"> + <v-list-tile + v-if="opt.condition()" + @click="opt.action" + :key="i" + > + {{opt.display}} + </v-list-tile> + </template> + </v-list> + <component v-if="displayComponent" :is="displayComponent" @hide="hideComponent"/> + </div> +</template> + +<script> + import PasswordChangeDialog from '@/PasswordChangeDialog' + export default { + name: 'UserOptions', + components: {PasswordChangeDialog}, + data () { + return { + displayComponent: null, + userOptions: [ + { + display: 'Change password', + action: () => { this.displayComponent = PasswordChangeDialog }, + condition: () => !this.$store.getters.isStudent + } + ] + } + }, + methods: { + hideComponent () { + this.displayComponent = null + } + } + } +</script> + +<style scoped> + +</style> diff --git a/frontend/src/components/student_list/StudentList.vue b/frontend/src/components/student_list/StudentList.vue index ca2e42f3d7e66da7a0912e3934608250b6e54e33..9e72c2041ac0329e2f3f1ce9aa4009dd9ac3df15 100644 --- a/frontend/src/components/student_list/StudentList.vue +++ b/frontend/src/components/student_list/StudentList.vue @@ -58,6 +58,7 @@ <v-btn small round outline class="submission-button" exact + v-if="props.item[type.pk]" :to="{name: 'submission-side-view', params: { studentPk: props.item.pk, submissionPk: props.item[type.pk].pk @@ -66,6 +67,7 @@ > {{props.item[type.pk].score}} </v-btn> + <span v-else>N.A</span> </td> <td style="padding: 0 15px;" @@ -76,16 +78,12 @@ <template slot="expand" slot-scope="props"> <v-card flat> <v-card-text> - <v-btn - outline class="mx-4" - @click="correctStudent(props.item)" - >Correct</v-btn> <ul class="student-info-list"> <li> - Modul: {{props.item.exam}} + <b>Modul:</b> {{props.item.exam}} </li> <li> - MatrikelNr: {{props.item.matrikel_no}} + <b>MatrikelNr:</b> {{props.item.matrikel_no}} </li> </ul> </v-card-text> @@ -165,14 +163,8 @@ }, methods: { ...mapActions([ - 'getStudents', - 'subscribeTo' + 'getStudents' ]), - correctStudent (student) { - this.subscribeTo({type: 'student', key: student.pk}).then(subscription => { - this.$router.push({name: 'subscription', params: {pk: subscription.pk}}) - }) - }, reduceArrToDict (arr, key) { return arr.reduce((acc, curr) => { const keyInDict = curr[key] diff --git a/frontend/src/components/submission_notes/SubmissionCorrection.vue b/frontend/src/components/submission_notes/SubmissionCorrection.vue index 49cdd72f9a19686f927b62ed3ba199530a4ae8c4..d8dc2776680913cf5764c361251f18ccf09a46d5 100644 --- a/frontend/src/components/submission_notes/SubmissionCorrection.vue +++ b/frontend/src/components/submission_notes/SubmissionCorrection.vue @@ -98,7 +98,7 @@ }, computed: { ...mapState({ - user: state => state.authentication.username, + user: state => state.authentication.user.username, showEditorOnLine: state => state.submissionNotes.ui.showEditorOnLine, selectedComment: state => state.submissionNotes.ui.selectedCommentOnLine, origFeedback: state => state.submissionNotes.origFeedback.feedback_lines, diff --git a/frontend/src/pages/Login.vue b/frontend/src/pages/Login.vue index 13fd519fff44bc11357ee45a9bd9abe48c64c95f..6c34d2fd237eee30a668b9e9709680e84c5706a0 100644 --- a/frontend/src/pages/Login.vue +++ b/frontend/src/pages/Login.vue @@ -60,7 +60,7 @@ computed: { ...mapState({ msg: state => state.authentication.message, - userRole: state => state.authentication.userRole + userRole: state => state.authentication.user.role }), production () { return process.env.NODE_ENV === 'production' @@ -72,13 +72,13 @@ methods: { ...mapActions([ 'getJWT', - 'getUserRole', + 'getUser', 'getJWTTimeDelta' ]), submit () { this.loading = true this.getJWT(this.credentials).then(() => { - this.getUserRole().then(() => { + this.getUser().then(() => { this.$router.push({name: 'home'}) }) this.getJWTTimeDelta() diff --git a/frontend/src/store/modules/authentication.js b/frontend/src/store/modules/authentication.js index db2d6c60364c669defd741f7fb54bf86fc59c53e..ce8bbb045e98a85a10ec70e979ce41f116b25b11 100644 --- a/frontend/src/store/modules/authentication.js +++ b/frontend/src/store/modules/authentication.js @@ -6,10 +6,14 @@ function initialState () { token: sessionStorage.getItem('token'), lastTokenRefreshTry: Date.now(), refreshingToken: false, - username: '', jwtTimeDelta: 0, - userRole: '', - message: '' + message: '', + user: { + pk: '', + username: '', + role: '', + is_admin: '' + } } } @@ -17,8 +21,7 @@ export const authMut = Object.freeze({ SET_MESSAGE: 'SET_MESSAGE', SET_JWT_TOKEN: 'SET_JWT_TOKEN', SET_JWT_TIME_DELTA: 'SET_JWT_TIME_DELTA', - SET_USERNAME: 'SET_USERNAME', - SET_USER_ROLE: 'SET_USER_ROLE', + SET_USER: 'SET_USER', SET_LAST_TOKEN_REFRESH_TRY: 'SET_LAST_TOKEN_REFRESH_TRY', RESET_STATE: 'RESET_STATE', SET_REFRESHING_TOKEN: 'SET_REFRESHING_TOKEN' @@ -31,13 +34,13 @@ const authentication = { return gradySays[Math.floor(Math.random() * gradySays.length)] }, isStudent: state => { - return state.userRole === 'Student' + return state.user.role === 'Student' }, isTutor: state => { - return state.userRole === 'Tutor' + return state.user.role === 'Tutor' }, isReviewer: state => { - return state.userRole === 'Reviewer' + return state.user.role === 'Reviewer' }, isTutorOrReviewer: (state, getters) => { return getters.isTutor || getters.isReviewer @@ -45,29 +48,26 @@ const authentication = { isLoggedIn: state => !!state.token }, mutations: { - [authMut.SET_MESSAGE]: function (state, message) { + [authMut.SET_MESSAGE] (state, message) { state.message = message }, - [authMut.SET_JWT_TOKEN]: function (state, token) { + [authMut.SET_JWT_TOKEN] (state, token) { sessionStorage.setItem('token', token) state.token = token }, - [authMut.SET_JWT_TIME_DELTA]: function (state, timeDelta) { + [authMut.SET_JWT_TIME_DELTA] (state, timeDelta) { state.jwtTimeDelta = timeDelta }, - [authMut.SET_USERNAME]: function (state, username) { - state.username = username - }, - [authMut.SET_USER_ROLE]: function (state, userRole) { - state.userRole = userRole + [authMut.SET_USER] (state, user) { + state.user = user }, - [authMut.SET_REFRESHING_TOKEN]: function (state, refreshing) { + [authMut.SET_REFRESHING_TOKEN] (state, refreshing) { state.refreshingToken = refreshing }, - [authMut.SET_LAST_TOKEN_REFRESH_TRY]: function (state) { + [authMut.SET_LAST_TOKEN_REFRESH_TRY] (state) { state.lastTokenRefreshTry = Date.now() }, - [authMut.RESET_STATE]: function (state) { + [authMut.RESET_STATE] (state) { sessionStorage.setItem('token', '') Object.assign(state, initialState()) } @@ -76,7 +76,6 @@ const authentication = { async getJWT (context, credentials) { try { const token = await api.fetchJWT(credentials) - context.commit(authMut.SET_USERNAME, credentials.username) context.commit(authMut.SET_JWT_TOKEN, token) } catch (error) { let errorMsg @@ -103,12 +102,12 @@ const authentication = { commit(authMut.SET_LAST_TOKEN_REFRESH_TRY) } }, - async getUserRole ({commit}) { + async getUser ({commit}) { try { - const userRole = await api.fetchUserRole() - commit(authMut.SET_USER_ROLE, userRole) + const user = await api.getOwnUser() + commit(authMut.SET_USER, user) } catch (err) { - commit(authMut.SET_MESSAGE, "You've been logged out.") + commit(authMut.SET_MESSAGE, 'Unable to fetch user.') } }, async getJWTTimeDelta ({commit}) { diff --git a/frontend/src/store/store.js b/frontend/src/store/store.js index 28fa292fa672e7f314889d7c733cca5397873646..0bf79157979cfe15fdadece56ed9c61781ec744b 100644 --- a/frontend/src/store/store.js +++ b/frontend/src/store/store.js @@ -54,7 +54,7 @@ const store = new Vuex.Store({ // when manually reloading the page paths: Object.keys(initialState()).concat( ['ui', 'studentPage', 'submissionNotes', 'feedbackSearchOptions', 'subscriptions', - 'authentication.username', 'authentication.userRole', 'authentication.jwtTimeDelta', + 'authentication.user', 'authentication.jwtTimeDelta', 'authentication.tokenCreationTime']) }), lastInteraction],