Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initial implementation of the passwordreset page #27

Merged
merged 3 commits into from
Oct 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
VITE_APP_API_BASE=https://sudosos.test.gewis.nl/api/v1
VITE_APP_GEWIS_TOKEN=sudosos
VITE_APP_GEWIS_TOKEN=sudosos-dev
2 changes: 1 addition & 1 deletion src/assets/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@
*::before,
*::after {
box-sizing: border-box;
margin: 0;
/*margin: 0;*/
font-weight: normal;
}

Expand Down
18 changes: 0 additions & 18 deletions src/helper/ApiHelper.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"Email": "Email",
"Enter email": "Enter email",
"Reset": "Reset password",
"Email sent": "If there exists an account on this email address, an email has been sent to {email} with instructions on how to reset your password.",
"Email sent": "If there exists an external account on this email address, an email has been sent to {email} with instructions on how to reset your password.",
"Set new password": "Set new password",
"New password": "New password",
"Confirm password": "Confirm new password",
Expand Down
2 changes: 1 addition & 1 deletion src/locales/nl.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"Email": "Email",
"Enter email": "Enter email",
"Reset": "Reset password",
"Email sent": "If there exists an account on this email address, an email has been sent to {email} with instructions on how to reset your password.",
"Email sent": "If there exists an external account on this email address, an email has been sent to {email} with instructions on how to reset your password.",
"Set new password": "Set new password",
"New password": "New password",
"Confirm password": "Confirm new password",
Expand Down
2 changes: 1 addition & 1 deletion src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Checkbox from "primevue/checkbox";
import TabView from "primevue/tabview";
import ScrollPanel from "primevue/scrollpanel";
import FileUpload from "primevue/fileupload";
import { populateStoresFromToken } from "@/helper/ApiHelper";
import { populateStoresFromToken } from "@sudosos/sudosos-frontend-common";

const app = createApp(App);

Expand Down
13 changes: 10 additions & 3 deletions src/router/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import UserOverView from '../views/UserOverView.vue';
import SingleUserView from "@/views/SingleUserView.vue";
import ProductsContainersView from "@/views/ProductsContainersView.vue";
import apiService from "@/services/ApiService";
import { isAuthenticated } from "@sudosos/sudosos-frontend-common";
import PasswordResetView from "@/views/PasswordResetView.vue";

const router = createRouter({
history: createWebHistory(),
Expand All @@ -24,6 +26,11 @@ const router = createRouter({
path: '',
component: LoginView,
name: 'login'
},
{
path: '/passwordreset',
component: PasswordResetView,
name: 'passwordreset'
}
]
},
Expand Down Expand Up @@ -87,12 +94,12 @@ const router = createRouter({
});

router.beforeEach((to, from, next) => {
const isAuthenticated = apiService.isAuthenticated();
const isAuth = isAuthenticated();

if (to.meta?.requiresAuth && !isAuthenticated) {
if (to.meta?.requiresAuth && !isAuth) {
// If the route requires authentication and the user is not authenticated, redirect to login
next({ name: 'login' });
} else if (!to.meta?.requiresAuth && isAuthenticated) {
} else if (!to.meta?.requiresAuth && isAuth) {
// If the route doesn't require authentication and the user is authenticated, redirect to home
next({ name: 'home' });
} else {
Expand Down
11 changes: 10 additions & 1 deletion src/views/LoginView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
:placeholder="$t('login.Enter password')"
/>
<Button type="submit" id="login-button" severity="danger">{{ $t('login.Login') }}</Button>
<a href="https://wieditleesttrekteenbak.nl/">{{ $t('login.Password reset') }}</a>
<div class="password-reset" @click="resetPassword">{{ $t('login.Password reset') }}</div>
</form>
</main>
<CopyrightBanner />
Expand Down Expand Up @@ -84,6 +84,11 @@ const ldapLogin = async (event: Event) => {
const loginViaGEWIS = () => {
window.location.href = `https://gewis.nl/token/${import.meta.env.VITE_APP_GEWIS_TOKEN}`;
};

const resetPassword = () => {
router.push({ name: 'passwordreset' });
};

</script>

<style scoped lang="scss">
Expand Down Expand Up @@ -145,4 +150,8 @@ hr {
.p-inputtext {
margin-bottom: 0.5rem;
}
.password-reset {
color: black;
text-decoration-line: underline;
}
</style>
234 changes: 234 additions & 0 deletions src/views/PasswordResetView.vue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are some console logs left in there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are now gone in the latest commit to this branch.

Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
<template>
<div>
<main>
<img id="reset-image" src="../assets/img/bier.png" alt="logo"/>
<h1>{{ $t('login.Reset') }}</h1>
<Form v-if="passwordResetMode === 0" class="login-form" @submit="resetPasswordRequest">
<span class="p-float-label with-error">
<InputText v-bind="email" id="email" size="large" name="email" :class="{'p-invalid': emailForm.errors.value.email}"/>
<label :class="{'contains-text': email.modelValue }" for="email">{{ $t('login.Enter email') }}</label>
</span>
<small v-if="emailForm.errors.value.email" class="p-error"><i class="pi pi-exclamation-circle"/>{{ emailForm.errors.value.email }}</small>
<Button type="submit" id="reset-button" severity="danger">{{ $t('login.Reset') }}</Button>
<div class="backtologin" @click="backToLogin">{{ $t('login.Back to login') }}</div>
</Form>
<div v-else-if="passwordResetMode === 1" class="login-form">
<div class="sent-email">{{ $t('login.Email sent') }}</div>
<div class="backtologin" @click="backToLogin">{{ $t('login.Back to login') }}</div>
</div>
<Form v-else class="login-form" @submit="setNewPassword">
<span class="p-float-label with-error">
<InputText v-bind="password" id="password" size="large" name="password" type="password" :class="{'p-invalid': passwordForm.errors.value.password}"/>
<label :class="{'contains-text': password.modelValue }" for="password">{{ $t('login.New password') }}</label>
</span>
<small v-if="passwordForm.errors.value.password" class="p-error"><i class="pi pi-exclamation-circle"/>{{ passwordForm.errors.value.password }}</small>
<span class="p-float-label with-error">
<InputText v-bind="passwordConfirm" id="passwordConfirm" size="large" name="passwordConfirm" type="password" :class="{'p-invalid': passwordForm.errors.value.passwordConfirm}"/>
<label :class="{'contains-text': passwordConfirm.modelValue }" for="passwordConfirm">{{ $t('login.Confirm password') }}</label>
</span>
<small v-if="passwordForm.errors.value.passwordConfirm" class="p-error"><i class="pi pi-exclamation-circle"/>{{ passwordForm.errors.value.passwordConfirm }}</small>
<Button type="submit" id="reset-button" severity="danger">{{ $t('login.Reset') }}</Button>
<div class="backtologin" @click="backToLogin">{{ $t('login.Back to login') }}</div>
</Form>
</main>
<CopyrightBanner/>
</div>
</template>

<script setup lang="ts">
import { Form } from 'vee-validate';
import CopyrightBanner from "@/components/CopyrightBanner.vue";
import apiService from "@/services/ApiService";
import router from "@/router";
import { useForm } from "vee-validate";
import InputText from "primevue/inputtext";
import { onBeforeMount, ref } from "vue";
import { useRoute } from "vue-router";
import * as yup from "yup";
import { toTypedSchema } from "@vee-validate/yup";

const emailSchema = toTypedSchema(
yup.object({
email: yup
.string()
.email()
.required(),
})
);

const atLeastOneUppercase = /^(?=.*[A-Z])/;
const atLeastOneLowercase = /^(?=.*[a-z])/;
const atLeastOneDigit = /^(?=.*\d)/;
const atLeastOneSpecialChar = /^(?=.*[@$!%*?&])/;
const allowedCharacters = /^[A-Za-z\d@$!%*?& ]{8,}$/;

const passwordSchema = toTypedSchema(
yup.object({
password: yup
.string()
.required("This is a required field")
.matches(atLeastOneUppercase, 'At least one uppercase letter is required')
.matches(atLeastOneLowercase, 'At least one lowercase letter is required')
.matches(atLeastOneDigit, 'At least one digit is required')
.matches(atLeastOneSpecialChar, 'At least one special character is required')
.matches(allowedCharacters, 'Password must be at least 8 characters long and only contain allowed characters'),
passwordConfirm: yup
.string()
.required("This is a required field")
.oneOf([yup.ref("password")], "Passwords do not match"),
})
);

const emailForm = useForm({
validationSchema: emailSchema,
});

const passwordForm = useForm({
validationSchema: passwordSchema,
});

const passwordResetMode = ref(0);
const email = emailForm.defineComponentBinds('email');
const password = passwordForm.defineComponentBinds('password');
const passwordConfirm = passwordForm.defineComponentBinds('passwordConfirm');

const route = useRoute();

onBeforeMount(async () => {
if (route.query.token !== undefined && route.query.email !== undefined) {
passwordResetMode.value = 2;
}
});

const resetPasswordRequest = emailForm.handleSubmit(async () => {
await apiService.authenticate.resetLocal({ accountMail: email.value.modelValue })
.then(() => {
passwordResetMode.value = 1;
});
});

const setNewPassword = passwordForm.handleSubmit(async () => {
await apiService.authenticate.resetLocalWithToken({
accountMail: route.query.email as string,
token: route.query.token as string,
password: password.value.modelValue as string,
}).then();
});

const backToLogin = () => {
router.push({ name: 'login' });
};

</script>

<style scoped lang="scss">
//TODO Cleanup and fix, related to issue #14 and #25
form {
display: flex;
flex-direction: column;
}

h1 {
color: black;
max-width: 350px;
width: 100%;
margin: 0 auto;
font-size: 2.5rem;
margin-bottom: 1.5rem;
}

#reset-image {
max-height: 150px;

display: block;
margin: 0 auto;
}

main {
display: flex;
flex-direction: column;
text-align: center;
max-width: 350px;
margin: 4rem auto;
}

.p-button {
margin: 1rem auto;
max-width: 350px;
width: 100%;
max-height: 38px;
display: flex;
align-items: center;
justify-content: center;
}

#gewis-branding {
max-height: 24px;
margin-right: 1rem;
}

.p-error {
display: block;
font-size: 12px;
text-align: left;
line-height:18px;
}

.p-error > i {
font-size:12px;
margin-right: 3.6px;
line-height:12px;
}

#email {
width: 100%;
padding-top: 18px;
padding-left: 12px;
padding-bottom: 0px;
height: 60px;
}

#password {
width: 100%;
padding-top: 18px;
padding-left: 12px;
padding-bottom: 0px;
height: 60px;
}

#passwordConfirm {
width: 100%;
padding-top: 18px;
padding-left: 12px;
padding-bottom: 0px;
height: 60px;
}

.p-float-label label {
top: 30%;
margin-top: 0;
left: 12px;
}

.contains-text, .p-float-label input:focus ~ label, .p-float-label label ~ input:focus {
margin-top: 0;
top: 8px!important;
}

.p-invalid {
background-color: #fef0f0;
}

.p-inputtext {
margin-bottom: 0.5rem;
}

.sent-email {
color: black;
}

.backtologin {
color: black;
text-decoration-line: underline;
}
</style>