Recover password
Database
- Create two methods for password recovery and reset
/database/app/pck_app.pks
plsql
PROCEDURE post_recoverpassword( -- Procedure initiates sending of email to recover password
p_username APP_USERS.USERNAME%TYPE, -- Username (e-mail address)
r_error OUT VARCHAR2 -- Error (NULL if sucess)
);
PROCEDURE post_resetpassword( -- Procedure resets user password
p_username APP_USERS.USERNAME%TYPE, -- Username (e-mail address)
p_password APP_USERS.PASSWORD%TYPE, -- Password
p_recovertoken APP_TOKENS.TOKEN%TYPE, -- Password recovery token (sent by e-mail)
r_accesstoken OUT VARCHAR2, -- Access token
r_refreshtoken OUT VARCHAR2, -- Refresh token
r_user OUT SYS_REFCURSOR, -- User data
r_error OUT VARCHAR2 -- Error (NULL if success)
);
/database/app/pck_app.pkb
plsql
PROCEDURE post_recoverpassword(
p_username APP_USERS.USERNAME%TYPE,
r_error OUT VARCHAR2
) AS
v_id_email APP_EMAILS.ID%TYPE;
v_uuid APP_USERS.UUID%TYPE;
v_fullname APP_USERS.FULLNAME%TYPE;
v_password_token APP_TOKENS.TOKEN%TYPE;
BEGIN
UPDATE app_users
SET status = 'N'
WHERE username = TRIM(UPPER(p_username))
RETURNING uuid, fullname INTO v_uuid, v_fullname;
IF SQL%ROWCOUNT = 1 THEN
v_password_token := pck_api_auth.token(v_uuid, 'R');
pck_api_emails.mail(v_id_email,TRIM(p_username),v_fullname,'Recover password!','<h1>Recover password!</h1><p>Recover your password address by pressing this <a href="' || pck_api_settings.read('APP_DOMAIN') || '/reset-password/' || v_password_token || '">link</a></p>');
BEGIN
pck_api_emails.send(v_id_email);
EXCEPTION
WHEN OTHERS THEN
NULL;
END;
pck_api_audit.inf('Recover password request successful', pck_api_audit.mrg('username', p_username), v_uuid);
ELSE
r_error := 'Wrong username';
pck_api_audit.wrn('Recover password request failed', pck_api_audit.mrg('username', p_username), v_uuid);
END IF;
EXCEPTION
WHEN OTHERS THEN
r_error := 'Internal error';
pck_api_audit.err('Recover password request error', pck_api_audit.mrg('username', p_username), v_uuid);
END;
PROCEDURE post_resetpassword(
p_username APP_USERS.USERNAME%TYPE,
p_password APP_USERS.PASSWORD%TYPE,
p_recovertoken APP_TOKENS.TOKEN%TYPE,
r_accesstoken OUT VARCHAR2,
r_refreshtoken OUT VARCHAR2,
r_user OUT SYS_REFCURSOR,
r_error OUT VARCHAR2
) AS
v_id_user APP_USERS.ID%TYPE;
v_uuid APP_USERS.UUID%TYPE;
c_salt VARCHAR2(32 CHAR) := DBMS_RANDOM.STRING('X', 32);
v_password app_users.password%TYPE := c_salt || DBMS_CRYPTO.HASH(UTL_RAW.CAST_TO_RAW(TRIM(p_password) || c_salt),4);
BEGIN
BEGIN
SELECT id, uuid
INTO v_id_user, v_uuid
FROM app_users
WHERE id IN (
SELECT id_user
FROM app_tokens
WHERE token = p_recovertoken
AND id_token_type = 'R'
AND expiration > SYSTIMESTAMP
);
EXCEPTION
WHEN NO_DATA_FOUND THEN r_error := 'Invalid token';
END;
IF r_error IS NOT NULL THEN
pck_api_audit.wrn('Reset password', pck_api_audit.mrg('username', p_username,'password','********','recover_token','********'), v_uuid);
ELSE -- valid token
UPDATE app_users SET
password = v_password,
attempts = 0,
status = 'A',
accessed = SYSTIMESTAMP
WHERE id = v_id_user;
COMMIT;
pck_api_auth.reset(v_uuid, 'A');
pck_api_auth.reset(v_uuid, 'R');
r_accesstoken := pck_api_auth.token(v_uuid, 'A');
r_refreshtoken := pck_api_auth.token(v_uuid, 'R');
app_user(v_uuid, r_user);
pck_api_audit.inf('Reset password successful', pck_api_audit.mrg('username', p_username,'password','********','recoverytoken','********'), v_uuid);
END IF;
EXCEPTION
WHEN OTHERS THEN
r_error := 'Internal error';
r_accesstoken := NULL;
r_refreshtoken := NULL;
pck_api_audit.err('Reset password error', pck_api_audit.mrg('username', p_username,'password','********','recoverytoken','********'), v_uuid);
END;
- Add methods to
@/api/index.ts
ts
async recoverPassword(username: string): Promise<HttpResponse<VoidResponse>> {
return await http.post('recoverpassword/', { username })
},
async resetPassword(username: string, password: string, recovertoken: string): Promise<HttpResponse<AuthResponse>> {
return await http.post('resetpassword/', { username, password, recovertoken })
},
Store
ts
async function recoverPassword(username: string): Promise<boolean> {
startLoading()
const { data } = await appApi.recoverPassword(username)
if (data?.error) {
setError('password.recovery.failed')
} else {
setInfo('password.recovery.email.sent')
}
stopLoading()
return !data?.error
}
async function resetPassword(
username: string,
password: string,
recoverToken: string,
): Promise<boolean> {
startLoading()
const { data, status, error } = await appApi.resetPassword(username, password, recoverToken)
if (error) {
accessToken.value = ''
Cookies.remove('refresh_token', refreshCookieOptions)
isAuthenticated.value = false
if (status == 401) {
setError('invalid.username.or.password')
} else {
setWarning(error.message)
}
} else if (data) {
accessToken.value = data.accesstoken
Cookies.set('refresh_token', data.refreshtoken, refreshCookieOptions)
isAuthenticated.value = !!accessToken.value
user.value = {
...defaultUser,
...data.user?.[0],
privileges: data.user?.[0]?.privileges || [],
}
setInfo('password.reset')
}
stopLoading()
return isAuthenticated.value
}
Views
@/pages/recover-password.vue
vue
<template>
<v-container>
<v-row justify="center">
<v-col cols="12" :md="4">
<h1 class="mb-4">{{ t('recover.password') }}</h1>
<v-bsb-form v-if="!sent" :options :data @submit="submit" />
<v-btn v-if="sent" @click="router.push('/')" class="mt-4">{{ t('ok') }}</v-btn>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
definePage({ meta: { role: 'guest' } })
const appStore = useAppStore()
const router = useRouter()
const { t } = useI18n()
const sent = ref(false)
const options = {
fields: [
{
type: 'text',
name: 'username',
label: 'Username',
placeholder: 'Username',
rules: [
{ type: 'required', value: true, message: 'username.is.required' },
{ type: 'email', value: true, message: 'username.must.be.a.valid.e-mail.address' },
],
},
],
actions: [
{
type: 'submit',
title: 'send',
color: 'primary',
},
],
actionsAlign: 'right',
actionsClass: 'ml-2',
}
const data = ref({
username: '',
})
const submit = async (newData: typeof data.value) => {
sent.value = await appStore.auth.recoverPassword(newData.username)
}
</script>
<i18n scope="global">
{
"en": {
"Recover password": "Recover password",
"Username": "Username",
"Username is required": "Username is required",
"Username must be a valid e-mail address": "Username must be a valid e-mail address",
"Submit": "Send",
"Ok": "Ok"
},
"fr": {
"Recover password": "Récupérer le mot de passe",
"Username": "Nom d'utilisateur",
"Username is required": "Le nom d'utilisateur est requis",
"Username must be a valid e-mail address": "Le nom d'utilisateur doit être une adresse e-mail valide",
"Submit": "Envoyer",
"Ok": "Ok"
}
}
</i18n>
@/pages/reset-password/[token].vue
vue
<template>
<v-container>
<v-row justify="center">
<v-col cols="12" :md="4">
<h1 class="mb-4">{{ t('reset.password') }}</h1>
<v-bsb-form v-if="!done" :options :data @submit="submit" @action="dev" />
<v-btn v-if="done" @click="router.push('/')" class="mt-4">{{ t('continue') }}</v-btn>
</v-col>
</v-row>
</v-container>
</template>
<script setup lang="ts">
definePage({ meta: { role: 'guest' } })
const appStore = useAppStore()
const router = useRouter()
const route = useRoute()
const { t } = useI18n()
const done = ref(false)
const devAction = import.meta.env.DEV
? [
{
type: 'dev',
title: 'Dev',
variant: 'outlined',
},
]
: []
const options = {
fields: [
{
type: 'text',
name: 'username',
label: 'username',
placeholder: 'username',
rules: [
{ type: 'required', value: true, message: 'username.is.required' },
{ type: 'email', value: true, message: 'username.must.be.a.valid.e-mail.address' },
],
},
{
type: 'password',
name: 'password',
label: 'password',
placeholder: 'password',
rules: [{ type: 'required', value: true, message: 'password.is.required' }],
},
{
type: 'password',
name: 'password2',
label: 'password.repeat',
placeholder: 'password.repeat',
rules: [
{ type: 'required', value: true, message: 'password.is.required' },
{ type: 'same-as', value: 'password', message: 'passwords.must.match' },
],
},
],
actions: [
{
type: 'submit',
title: 'reset.password',
color: 'primary',
},
...devAction,
],
actionsAlign: 'right',
actionsClass: 'ml-2',
}
const data = ref({
username: '',
password: '',
password2: '',
})
const submit = async (newData: typeof data.value) => {
const recoverToken = (route.params as { token: string }).token
done.value = await appStore.auth.resetPassword(newData.username, newData.password, recoverToken)
}
const dev = async () => {
data.value = {
username: import.meta.env.VITE_USERNAME,
password: import.meta.env.VITE_PASSWORD,
password2: import.meta.env.VITE_PASSWORD,
}
}
</script>
<i18n>
{
"en": {
"Reset password": "Reset password",
"Username": "Username",
"Username is required": "Username is required",
"Username must be a valid e-mail address": "Username must be a valid e-mail address",
"Password": "Password",
"Password (repeat)": "Password (repeat)",
"Password is required": "Password is required",
"Passwords must match": "Passwords must match",
"Continue": "Continue"
},
"fr": {
"Reset password": "Réinitialiser le mot de passe",
"Username": "Nom d'utilisateur",
"Username is required": "Le nom d'utilisateur est requis",
"Username must be a valid e-mail address": "Le nom d'utilisateur doit être une adresse e-mail valide",
"Password": "Mot de passe",
"Password (repeat)": "Mot de passe (répéter)",
"Password is required": "Le mot de passe est requis",
"Passwords must match": "Les mots de passe doivent correspondre",
"Continue": "Continuer"
}
}
</i18n>
Login view
Add link to start password recovery in @/src/pages/login.vue
vue
{{ t('not.registered.yet') }}
<a href="/signup">{{ t('sign.up') }}</a>
|
<a href="/recover-password">{{ t('forgot.password') }}</a>