@@ -76,7 +76,7 @@ if (state.value.token) { | |||
userData.value = jwtDecode<Claims>(state.value.token); | |||
} | |||
const hasToken = computed(() => !!store.getters.getToken); | |||
const hasToken = computed(() => !!store.getters.token); | |||
interface LogoElement { | |||
letter: string; | |||
@@ -2,6 +2,7 @@ import SignetRequestController from '@/api/requests'; | |||
import { | |||
AuthenticationRequest, | |||
Bonus, | |||
ChangePasswordRequest, | |||
CloseRewardFundRequest, | |||
ContributeRequest, | |||
CreateQueueRequest, | |||
@@ -9,6 +10,7 @@ import { | |||
CreateRewardFundRequest, | |||
DistributeRewardsRequest, | |||
EditQueueRequest, | |||
EscalatePrivilegesRequest, | |||
GetBalanceRequest, | |||
GetBalanceResponse, | |||
GetContributionsRequest, | |||
@@ -24,6 +26,7 @@ import { | |||
LoginResponse, | |||
NearlyCompleteFundsRequest, | |||
NearlyCompleteFundsResponse, | |||
Privileges, | |||
QueueMember, | |||
RewardDistributionInfo, | |||
SubmitRewardFundRequest, | |||
@@ -133,3 +136,13 @@ export const distributeRewardFund = (rewardFundID: number, payments: RewardDistr | |||
}); | |||
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', | |||
headers: setHeaders( | |||
undefined, | |||
store.getters.getToken, | |||
store.getters.token, | |||
), | |||
}, | |||
); | |||
@@ -42,7 +42,7 @@ class SignetRequestController { | |||
body: JSON.stringify(payload), | |||
headers: setHeaders( | |||
{ 'Content-Type': 'application/json' }, | |||
store.getters.getToken, | |||
store.getters.token, | |||
), | |||
}, | |||
); | |||
@@ -1,5 +1,6 @@ | |||
// eslint-disable-next-line no-shadow | |||
import Decimal from 'decimal.js'; | |||
import { DateTime } from 'luxon'; | |||
// eslint-disable-next-line no-shadow | |||
export enum Privileges { | |||
@@ -131,6 +132,7 @@ export interface AuthenticationRequest { | |||
export interface LoginResponse { | |||
token: string | null; | |||
lastLogin: DateTime | null; | |||
} | |||
export interface GetQueueMembersRequest { | |||
@@ -151,6 +153,7 @@ export interface GetRewardFundsResponse { | |||
} | |||
export interface Claims { | |||
id: number; | |||
username: string; | |||
privileges: Privileges; | |||
exp: number; | |||
@@ -210,11 +213,22 @@ export interface DistributeRewardsRequest { | |||
} | |||
export interface User { | |||
username: string, | |||
password: string, | |||
admin: number, | |||
id: number; | |||
username: string; | |||
password: string; | |||
admin: number; | |||
} | |||
export interface GetUsersResponse { | |||
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'); | |||
}; | |||
const hasPermission = (requiredRights: number) => { | |||
const jwt = store.getters.getToken; | |||
const jwt = store.getters.token; | |||
if (jwt !== undefined && requiredRights !== undefined) { | |||
try { | |||
const decoded = jwtDecode<Claims>(store.getters.getToken); | |||
const decoded = jwtDecode<Claims>(store.getters.token); | |||
const expired = luxon.DateTime.now() | |||
.toUnixInteger() > decoded.exp; | |||
jwtDecode(jwt, { header: true }); | |||
@@ -14,6 +14,8 @@ import ModifyQueueView from '@/views/ModifyQueueView.vue'; | |||
import AdminDashboardView from '@/views/AdminDashboardView.vue'; | |||
import ModifyUserView from '@/views/ModifyUserView.vue'; | |||
import LogoutView from '@/views/LogoutView.vue'; | |||
import ChangePasswordView from '@/views/ChangePasswordView.vue'; | |||
import store from '@/store'; | |||
const routes: Array<RouteRecordRaw> = [ | |||
{ | |||
@@ -34,6 +36,12 @@ const routes: Array<RouteRecordRaw> = [ | |||
component: LoginView, | |||
meta: { title: 'Login' }, | |||
}, | |||
{ | |||
path: '/changepassword', | |||
name: 'changepassword', | |||
component: ChangePasswordView, | |||
meta: { title: 'Change Password' }, | |||
}, | |||
{ | |||
path: '/logout', | |||
name: 'logout', | |||
@@ -129,7 +137,13 @@ const router = createRouter({ | |||
router.beforeEach(async (to, from, next) => { | |||
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; |
@@ -3,9 +3,11 @@ import { createStore } from 'vuex'; | |||
export default createStore({ | |||
state: { | |||
token: undefined as string | undefined, | |||
passwordChangeRequired: false, | |||
}, | |||
getters: { | |||
getToken: (state) => state.token, | |||
token: (state) => state.token, | |||
passwordChangeRequired: (state) => state.passwordChangeRequired, | |||
}, | |||
mutations: { | |||
setToken: (state, token) => { | |||
@@ -14,6 +16,12 @@ export default createStore({ | |||
clearToken: (state) => { | |||
state.token = undefined; | |||
}, | |||
userNeedsToChangePassword: (state) => { | |||
state.passwordChangeRequired = true; | |||
}, | |||
userHasChangedPassword: (state) => { | |||
state.passwordChangeRequired = false; | |||
}, | |||
}, | |||
actions: { | |||
setToken: (context) => { | |||
@@ -22,7 +30,12 @@ export default createStore({ | |||
clearToken: (context) => { | |||
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> | |||
</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. | |||
Click to select a row in order to mark a wallet to receive rewards. | |||
</p> | |||
@@ -154,8 +154,8 @@ | |||
</tbody> | |||
</table> | |||
</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" | |||
:inputs="{ button: { label: 'Distribute Rewards', style: 'is-success' }, | |||
checkbox: { label: 'Allow Distribution' }}" | |||
@@ -165,22 +165,22 @@ | |||
</div> | |||
</section> | |||
<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"> | |||
Submit Group Fund | |||
</div> | |||
<ModifyWithConfirmation | |||
<ButtonWithConfirmation | |||
:inputs="{ button: { label: 'Submit Fund', style: 'is-success' }, | |||
checkbox: { label: 'Allow Submission' }}" | |||
:modification="submitFund" | |||
@confirmed="setAllowSubmit" | |||
/> | |||
</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"> | |||
Close Group Fund | |||
</div> | |||
<ModifyWithConfirmation | |||
<ButtonWithConfirmation | |||
:inputs="{ button: { label: 'Close Fund', style: 'is-danger' }, | |||
checkbox: { label: 'Allow Closing' }}" | |||
:modification="deleteFund" | |||
@@ -230,7 +230,7 @@ import { | |||
submitRewardFund, | |||
} from '@/api/composed'; | |||
import Decimal from 'decimal.js'; | |||
import ModifyWithConfirmation from '@/components/ModifyWithConfirmation.vue'; | |||
import ButtonWithConfirmation from '@/components/ButtonWithConfirmation.vue'; | |||
const route = useRoute(); | |||
const router = useRouter(); | |||
@@ -389,7 +389,7 @@ const calculateReward = (bought: Decimal) => { | |||
const selectedContributions = ref([] as Contribution[]); | |||
const selectContribution = (contribution: SelectableContribution) => { | |||
if (!store.getters.getToken || !hasPermission(Privileges.Admin)) return; | |||
if (!store.getters.token || !hasPermission(Privileges.Admin)) return; | |||
if (enableConsolidation.value) { | |||
if (!contribution.selected) { | |||
selectedContributions.value.push(contribution); | |||
@@ -34,7 +34,12 @@ const submit = async () => { | |||
if (resp.token !== null) { | |||
sessionStorage.setItem('jwt', JSON.stringify({ token: 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> | |||
@@ -26,7 +26,9 @@ | |||
</td> | |||
<td class="p-2"> | |||
<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" | |||
:selected="getPrivilege(user.admin) === privilege" | |||
v-for="(privilege, i) in Object.values(privileges)" :key="i" | |||
@@ -52,7 +54,10 @@ import { | |||
User, | |||
} from '@/api/types'; | |||
import { ref } from 'vue'; | |||
import { getUsers } from '@/api/composed'; | |||
import { | |||
changePrivileges, | |||
getUsers, | |||
} from '@/api/composed'; | |||
import jwtDecode from 'jwt-decode'; | |||
import store from '@/store'; | |||
@@ -64,12 +69,18 @@ try { | |||
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>({ | |||
username: '', | |||
privileges: -1, | |||
exp: -1, | |||
}); | |||
userData.value = jwtDecode<Claims>(store.getters.getToken); | |||
userData.value = jwtDecode<Claims>(store.getters.token); | |||
const getPrivilege = (privilege: number) => Privileges[privilege]; | |||
@@ -81,6 +92,11 @@ const getPrivileges = () => Object.fromEntries( | |||
const privileges = getPrivileges(); | |||
const setNewUserPermissions = (userID: number, evt: Event) => changePrivileges( | |||
userID, | |||
getSelectedPrivilege(evt), | |||
); | |||
</script> | |||
<style scoped lang="stylus"> | |||