@@ -76,7 +76,7 @@ if (state.value.token) { | |||||
userData.value = jwtDecode<Claims>(state.value.token); | userData.value = jwtDecode<Claims>(state.value.token); | ||||
} | } | ||||
const hasToken = computed(() => !!store.getters.getToken); | |||||
const hasToken = computed(() => !!store.getters.token); | |||||
interface LogoElement { | interface LogoElement { | ||||
letter: string; | letter: string; | ||||
@@ -2,6 +2,7 @@ import SignetRequestController from '@/api/requests'; | |||||
import { | import { | ||||
AuthenticationRequest, | AuthenticationRequest, | ||||
Bonus, | Bonus, | ||||
ChangePasswordRequest, | |||||
CloseRewardFundRequest, | CloseRewardFundRequest, | ||||
ContributeRequest, | ContributeRequest, | ||||
CreateQueueRequest, | CreateQueueRequest, | ||||
@@ -9,6 +10,7 @@ import { | |||||
CreateRewardFundRequest, | CreateRewardFundRequest, | ||||
DistributeRewardsRequest, | DistributeRewardsRequest, | ||||
EditQueueRequest, | EditQueueRequest, | ||||
EscalatePrivilegesRequest, | |||||
GetBalanceRequest, | GetBalanceRequest, | ||||
GetBalanceResponse, | GetBalanceResponse, | ||||
GetContributionsRequest, | GetContributionsRequest, | ||||
@@ -24,6 +26,7 @@ import { | |||||
LoginResponse, | LoginResponse, | ||||
NearlyCompleteFundsRequest, | NearlyCompleteFundsRequest, | ||||
NearlyCompleteFundsResponse, | NearlyCompleteFundsResponse, | ||||
Privileges, | |||||
QueueMember, | QueueMember, | ||||
RewardDistributionInfo, | RewardDistributionInfo, | ||||
SubmitRewardFundRequest, | SubmitRewardFundRequest, | ||||
@@ -133,3 +136,13 @@ export const distributeRewardFund = (rewardFundID: number, payments: RewardDistr | |||||
}); | }); | ||||
export const getUsers = () => controller.post<GetUsersResponse, null>('GetUsers', null); | export const getUsers = () => controller.post<GetUsersResponse, null>('GetUsers', null); | ||||
export const changePrivileges = (userID: number, privileges: Privileges) => controller.post<SuccessResponse, EscalatePrivilegesRequest>('ChangePrivileges', { | |||||
userID, | |||||
privileges, | |||||
}); | |||||
export const changePassword = (userID: number, password: string) => controller.post<SuccessResponse, ChangePasswordRequest>('ChangePassword', { | |||||
userID, | |||||
password, | |||||
}); |
@@ -24,7 +24,7 @@ class SignetRequestController { | |||||
method: 'GET', | method: 'GET', | ||||
headers: setHeaders( | headers: setHeaders( | ||||
undefined, | undefined, | ||||
store.getters.getToken, | |||||
store.getters.token, | |||||
), | ), | ||||
}, | }, | ||||
); | ); | ||||
@@ -42,7 +42,7 @@ class SignetRequestController { | |||||
body: JSON.stringify(payload), | body: JSON.stringify(payload), | ||||
headers: setHeaders( | headers: setHeaders( | ||||
{ 'Content-Type': 'application/json' }, | { 'Content-Type': 'application/json' }, | ||||
store.getters.getToken, | |||||
store.getters.token, | |||||
), | ), | ||||
}, | }, | ||||
); | ); | ||||
@@ -1,5 +1,6 @@ | |||||
// eslint-disable-next-line no-shadow | // eslint-disable-next-line no-shadow | ||||
import Decimal from 'decimal.js'; | import Decimal from 'decimal.js'; | ||||
import { DateTime } from 'luxon'; | |||||
// eslint-disable-next-line no-shadow | // eslint-disable-next-line no-shadow | ||||
export enum Privileges { | export enum Privileges { | ||||
@@ -131,6 +132,7 @@ export interface AuthenticationRequest { | |||||
export interface LoginResponse { | export interface LoginResponse { | ||||
token: string | null; | token: string | null; | ||||
lastLogin: DateTime | null; | |||||
} | } | ||||
export interface GetQueueMembersRequest { | export interface GetQueueMembersRequest { | ||||
@@ -151,6 +153,7 @@ export interface GetRewardFundsResponse { | |||||
} | } | ||||
export interface Claims { | export interface Claims { | ||||
id: number; | |||||
username: string; | username: string; | ||||
privileges: Privileges; | privileges: Privileges; | ||||
exp: number; | exp: number; | ||||
@@ -210,11 +213,22 @@ export interface DistributeRewardsRequest { | |||||
} | } | ||||
export interface User { | export interface User { | ||||
username: string, | |||||
password: string, | |||||
admin: number, | |||||
id: number; | |||||
username: string; | |||||
password: string; | |||||
admin: number; | |||||
} | } | ||||
export interface GetUsersResponse { | export interface GetUsersResponse { | ||||
users: User[]; | users: User[]; | ||||
} | } | ||||
export interface EscalatePrivilegesRequest { | |||||
userID: number; | |||||
privileges: Privileges; | |||||
} | |||||
export interface ChangePasswordRequest { | |||||
userID: number; | |||||
password: string; | |||||
} |
@@ -8,10 +8,10 @@ const removeToken = () => { | |||||
store.commit('clearToken'); | store.commit('clearToken'); | ||||
}; | }; | ||||
const hasPermission = (requiredRights: number) => { | const hasPermission = (requiredRights: number) => { | ||||
const jwt = store.getters.getToken; | |||||
const jwt = store.getters.token; | |||||
if (jwt !== undefined && requiredRights !== undefined) { | if (jwt !== undefined && requiredRights !== undefined) { | ||||
try { | try { | ||||
const decoded = jwtDecode<Claims>(store.getters.getToken); | |||||
const decoded = jwtDecode<Claims>(store.getters.token); | |||||
const expired = luxon.DateTime.now() | const expired = luxon.DateTime.now() | ||||
.toUnixInteger() > decoded.exp; | .toUnixInteger() > decoded.exp; | ||||
jwtDecode(jwt, { header: true }); | jwtDecode(jwt, { header: true }); | ||||
@@ -14,6 +14,8 @@ import ModifyQueueView from '@/views/ModifyQueueView.vue'; | |||||
import AdminDashboardView from '@/views/AdminDashboardView.vue'; | import AdminDashboardView from '@/views/AdminDashboardView.vue'; | ||||
import ModifyUserView from '@/views/ModifyUserView.vue'; | import ModifyUserView from '@/views/ModifyUserView.vue'; | ||||
import LogoutView from '@/views/LogoutView.vue'; | import LogoutView from '@/views/LogoutView.vue'; | ||||
import ChangePasswordView from '@/views/ChangePasswordView.vue'; | |||||
import store from '@/store'; | |||||
const routes: Array<RouteRecordRaw> = [ | const routes: Array<RouteRecordRaw> = [ | ||||
{ | { | ||||
@@ -34,6 +36,12 @@ const routes: Array<RouteRecordRaw> = [ | |||||
component: LoginView, | component: LoginView, | ||||
meta: { title: 'Login' }, | meta: { title: 'Login' }, | ||||
}, | }, | ||||
{ | |||||
path: '/changepassword', | |||||
name: 'changepassword', | |||||
component: ChangePasswordView, | |||||
meta: { title: 'Change Password' }, | |||||
}, | |||||
{ | { | ||||
path: '/logout', | path: '/logout', | ||||
name: 'logout', | name: 'logout', | ||||
@@ -129,7 +137,13 @@ const router = createRouter({ | |||||
router.beforeEach(async (to, from, next) => { | router.beforeEach(async (to, from, next) => { | ||||
document.title = `Beignet - ${to.meta.title}`; | document.title = `Beignet - ${to.meta.title}`; | ||||
return next(); | |||||
if (!store.getters.passwordChangeRequired) { | |||||
return next(); | |||||
} | |||||
if (to.name !== 'changepassword') { | |||||
return next('changepassword'); | |||||
} | |||||
return next(undefined); | |||||
}); | }); | ||||
export default router; | export default router; |
@@ -3,9 +3,11 @@ import { createStore } from 'vuex'; | |||||
export default createStore({ | export default createStore({ | ||||
state: { | state: { | ||||
token: undefined as string | undefined, | token: undefined as string | undefined, | ||||
passwordChangeRequired: false, | |||||
}, | }, | ||||
getters: { | getters: { | ||||
getToken: (state) => state.token, | |||||
token: (state) => state.token, | |||||
passwordChangeRequired: (state) => state.passwordChangeRequired, | |||||
}, | }, | ||||
mutations: { | mutations: { | ||||
setToken: (state, token) => { | setToken: (state, token) => { | ||||
@@ -14,6 +16,12 @@ export default createStore({ | |||||
clearToken: (state) => { | clearToken: (state) => { | ||||
state.token = undefined; | state.token = undefined; | ||||
}, | }, | ||||
userNeedsToChangePassword: (state) => { | |||||
state.passwordChangeRequired = true; | |||||
}, | |||||
userHasChangedPassword: (state) => { | |||||
state.passwordChangeRequired = false; | |||||
}, | |||||
}, | }, | ||||
actions: { | actions: { | ||||
setToken: (context) => { | setToken: (context) => { | ||||
@@ -22,7 +30,12 @@ export default createStore({ | |||||
clearToken: (context) => { | clearToken: (context) => { | ||||
context.commit('clearToken'); | context.commit('clearToken'); | ||||
}, | }, | ||||
userNeedsToChangePassword: (context) => { | |||||
context.commit('userNeedsToChangePassword'); | |||||
}, | |||||
userHasChangedPassword: (context) => { | |||||
context.commit('userHasChangedPassword'); | |||||
}, | |||||
}, | }, | ||||
modules: { | |||||
}, | |||||
modules: {}, | |||||
}); | }); |
@@ -0,0 +1,53 @@ | |||||
<template> | |||||
<section class="section is-medium"> | |||||
<div | |||||
class="is-flex is-flex-direction-column is-justify-content-space-around change-password-body"> | |||||
<div> | |||||
<span class="is-size-3-desktop is-size-4-mobile"> | |||||
Welcome, {{ user.username }} | |||||
</span> | |||||
<p>It's time to change your password.</p> | |||||
</div> | |||||
<div> | |||||
<input type="password" class="input is-medium" aria-label="Change Password" | |||||
v-model="password"/> | |||||
<div class="is-flex is-flex-direction-row is-justify-content-flex-end my-3"> | |||||
<button class="button is-success" @click="changeUserPassword">Submit</button> | |||||
</div> | |||||
</div> | |||||
</div> | |||||
</section> | |||||
</template> | |||||
<script setup lang="ts"> | |||||
import jwtDecode from 'jwt-decode'; | |||||
import { Claims } from '@/api/types'; | |||||
import store from '@/store'; | |||||
import { useRouter } from 'vue-router'; | |||||
import { ref } from 'vue'; | |||||
import { changePassword } from '@/api/composed'; | |||||
const router = useRouter(); | |||||
const password = ref<string>(); | |||||
const user = ref<Claims>(); | |||||
if (store.getters.token) { | |||||
user.value = jwtDecode<Claims>(store.getters.token); | |||||
} else { | |||||
await router.push('/'); | |||||
} | |||||
const changeUserPassword = async () => { | |||||
if (!user.value) throw new Error('There is no user!'); | |||||
if (!password.value) throw new Error('You need to type a password!'); | |||||
const resp = await changePassword(user.value?.id, password.value); | |||||
await store.dispatch('userHasChangedPassword'); | |||||
if (resp?.success) await router.push('/'); | |||||
}; | |||||
</script> | |||||
<style scoped lang="stylus"> | |||||
.change-password-body | |||||
height 45vh | |||||
</style> |
@@ -122,7 +122,7 @@ | |||||
</label> | </label> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<p class="py-2" v-if="store.getters.getToken && hasPermission(Privileges.Admin)"> | |||||
<p class="py-2" v-if="store.getters.token && hasPermission(Privileges.Admin)"> | |||||
Enable contribution consolidation in order to select. | Enable contribution consolidation in order to select. | ||||
Click to select a row in order to mark a wallet to receive rewards. | Click to select a row in order to mark a wallet to receive rewards. | ||||
</p> | </p> | ||||
@@ -154,8 +154,8 @@ | |||||
</tbody> | </tbody> | ||||
</table> | </table> | ||||
</div> | </div> | ||||
<div class="my-4" v-if="store.getters.getToken && hasPermission(Privileges.Admin)"> | |||||
<ModifyWithConfirmation | |||||
<div class="my-4" v-if="store.getters.token && hasPermission(Privileges.Admin)"> | |||||
<ButtonWithConfirmation | |||||
:condition="selectedContributions.length === 0" | :condition="selectedContributions.length === 0" | ||||
:inputs="{ button: { label: 'Distribute Rewards', style: 'is-success' }, | :inputs="{ button: { label: 'Distribute Rewards', style: 'is-success' }, | ||||
checkbox: { label: 'Allow Distribution' }}" | checkbox: { label: 'Allow Distribution' }}" | ||||
@@ -165,22 +165,22 @@ | |||||
</div> | </div> | ||||
</section> | </section> | ||||
<section class="section is-small px-0" | <section class="section is-small px-0" | ||||
v-if="store.getters.getToken && hasPermission(Privileges.Admin)"> | |||||
v-if="store.getters.token && hasPermission(Privileges.Admin)"> | |||||
<div class="title is-size-4 has-text-white-ter has-text-centered"> | <div class="title is-size-4 has-text-white-ter has-text-centered"> | ||||
Submit Group Fund | Submit Group Fund | ||||
</div> | </div> | ||||
<ModifyWithConfirmation | |||||
<ButtonWithConfirmation | |||||
:inputs="{ button: { label: 'Submit Fund', style: 'is-success' }, | :inputs="{ button: { label: 'Submit Fund', style: 'is-success' }, | ||||
checkbox: { label: 'Allow Submission' }}" | checkbox: { label: 'Allow Submission' }}" | ||||
:modification="submitFund" | :modification="submitFund" | ||||
@confirmed="setAllowSubmit" | @confirmed="setAllowSubmit" | ||||
/> | /> | ||||
</section> | </section> | ||||
<section v-if="store.getters.getToken && hasPermission(Privileges.AdminPlus)"> | |||||
<section v-if="store.getters.token && hasPermission(Privileges.AdminPlus)"> | |||||
<div class="title is-size-4 has-text-white-ter has-text-centered"> | <div class="title is-size-4 has-text-white-ter has-text-centered"> | ||||
Close Group Fund | Close Group Fund | ||||
</div> | </div> | ||||
<ModifyWithConfirmation | |||||
<ButtonWithConfirmation | |||||
:inputs="{ button: { label: 'Close Fund', style: 'is-danger' }, | :inputs="{ button: { label: 'Close Fund', style: 'is-danger' }, | ||||
checkbox: { label: 'Allow Closing' }}" | checkbox: { label: 'Allow Closing' }}" | ||||
:modification="deleteFund" | :modification="deleteFund" | ||||
@@ -230,7 +230,7 @@ import { | |||||
submitRewardFund, | submitRewardFund, | ||||
} from '@/api/composed'; | } from '@/api/composed'; | ||||
import Decimal from 'decimal.js'; | import Decimal from 'decimal.js'; | ||||
import ModifyWithConfirmation from '@/components/ModifyWithConfirmation.vue'; | |||||
import ButtonWithConfirmation from '@/components/ButtonWithConfirmation.vue'; | |||||
const route = useRoute(); | const route = useRoute(); | ||||
const router = useRouter(); | const router = useRouter(); | ||||
@@ -389,7 +389,7 @@ const calculateReward = (bought: Decimal) => { | |||||
const selectedContributions = ref([] as Contribution[]); | const selectedContributions = ref([] as Contribution[]); | ||||
const selectContribution = (contribution: SelectableContribution) => { | const selectContribution = (contribution: SelectableContribution) => { | ||||
if (!store.getters.getToken || !hasPermission(Privileges.Admin)) return; | |||||
if (!store.getters.token || !hasPermission(Privileges.Admin)) return; | |||||
if (enableConsolidation.value) { | if (enableConsolidation.value) { | ||||
if (!contribution.selected) { | if (!contribution.selected) { | ||||
selectedContributions.value.push(contribution); | selectedContributions.value.push(contribution); | ||||
@@ -34,7 +34,12 @@ const submit = async () => { | |||||
if (resp.token !== null) { | if (resp.token !== null) { | ||||
sessionStorage.setItem('jwt', JSON.stringify({ token: resp.token })); | sessionStorage.setItem('jwt', JSON.stringify({ token: resp.token })); | ||||
store.commit('setToken', resp.token); | store.commit('setToken', resp.token); | ||||
await router.push('/'); | |||||
if (resp.lastLogin === null) { | |||||
await store.dispatch('userNeedsToChangePassword'); | |||||
await router.push('/changepassword'); | |||||
} else { | |||||
await router.push('/'); | |||||
} | |||||
} | } | ||||
}; | }; | ||||
</script> | </script> | ||||
@@ -26,7 +26,9 @@ | |||||
</td> | </td> | ||||
<td class="p-2"> | <td class="p-2"> | ||||
<div class="select is-small"> | <div class="select is-small"> | ||||
<select name="" id="" aria-label="User Privilege"> | |||||
<select name="" id="" | |||||
aria-label="User Privilege" | |||||
@change="setNewUserPermissions(user.id, $event)"> | |||||
<option :value="privilege" | <option :value="privilege" | ||||
:selected="getPrivilege(user.admin) === privilege" | :selected="getPrivilege(user.admin) === privilege" | ||||
v-for="(privilege, i) in Object.values(privileges)" :key="i" | v-for="(privilege, i) in Object.values(privileges)" :key="i" | ||||
@@ -52,7 +54,10 @@ import { | |||||
User, | User, | ||||
} from '@/api/types'; | } from '@/api/types'; | ||||
import { ref } from 'vue'; | import { ref } from 'vue'; | ||||
import { getUsers } from '@/api/composed'; | |||||
import { | |||||
changePrivileges, | |||||
getUsers, | |||||
} from '@/api/composed'; | |||||
import jwtDecode from 'jwt-decode'; | import jwtDecode from 'jwt-decode'; | ||||
import store from '@/store'; | import store from '@/store'; | ||||
@@ -64,12 +69,18 @@ try { | |||||
users.value = undefined; | users.value = undefined; | ||||
} | } | ||||
const getSelectedPrivilege = (evt: Event) => { | |||||
const target = evt.target as HTMLSelectElement; | |||||
const privilege = target.options[target.selectedIndex].value as 'SuperUser' | 'AdminPlus' | 'Admin'; | |||||
return Privileges[privilege]; | |||||
}; | |||||
const userData = ref<Claims>({ | const userData = ref<Claims>({ | ||||
username: '', | username: '', | ||||
privileges: -1, | privileges: -1, | ||||
exp: -1, | exp: -1, | ||||
}); | }); | ||||
userData.value = jwtDecode<Claims>(store.getters.getToken); | |||||
userData.value = jwtDecode<Claims>(store.getters.token); | |||||
const getPrivilege = (privilege: number) => Privileges[privilege]; | const getPrivilege = (privilege: number) => Privileges[privilege]; | ||||
@@ -81,6 +92,11 @@ const getPrivileges = () => Object.fromEntries( | |||||
const privileges = getPrivileges(); | const privileges = getPrivileges(); | ||||
const setNewUserPermissions = (userID: number, evt: Event) => changePrivileges( | |||||
userID, | |||||
getSelectedPrivilege(evt), | |||||
); | |||||
</script> | </script> | ||||
<style scoped lang="stylus"> | <style scoped lang="stylus"> | ||||