Browse Source

Force change password for new users

master
Jared 1 year ago
parent
commit
d79ed3f6de
12 changed files with 153 additions and 25 deletions
  1. +1
    -1
      src/App.vue
  2. +13
    -0
      src/api/composed.ts
  3. +2
    -2
      src/api/requests.ts
  4. +17
    -3
      src/api/types.ts
  5. +0
    -0
      src/components/ButtonWithConfirmation.vue
  6. +2
    -2
      src/lib/auth.ts
  7. +15
    -1
      src/router/index.ts
  8. +16
    -3
      src/store/index.ts
  9. +53
    -0
      src/views/ChangePasswordView.vue
  10. +9
    -9
      src/views/FundView.vue
  11. +6
    -1
      src/views/LoginView.vue
  12. +19
    -3
      src/views/ModifyUserView.vue

+ 1
- 1
src/App.vue View File

@@ -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;


+ 13
- 0
src/api/composed.ts View File

@@ -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,
});

+ 2
- 2
src/api/requests.ts View File

@@ -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,
), ),
}, },
); );


+ 17
- 3
src/api/types.ts View File

@@ -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;
}

src/components/ModifyWithConfirmation.vue → src/components/ButtonWithConfirmation.vue View File


+ 2
- 2
src/lib/auth.ts View File

@@ -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 });


+ 15
- 1
src/router/index.ts View File

@@ -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;

+ 16
- 3
src/store/index.ts View File

@@ -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: {},
}); });

+ 53
- 0
src/views/ChangePasswordView.vue View File

@@ -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>

+ 9
- 9
src/views/FundView.vue View File

@@ -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);


+ 6
- 1
src/views/LoginView.vue View File

@@ -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>


+ 19
- 3
src/views/ModifyUserView.vue View File

@@ -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">


Loading…
Cancel
Save