Skip to content

Recover password ​

Overview ​

The password recovery feature allows users to reset forgotten passwords securely. The process involves two steps: first, users request a recovery email using their username, which sends a verification token; then, they use this token to set a new password. Tokens expire after a set time and can only be used once. Passwords are validated against configured requirements and securely hashed before storage.

API ​

Template for password recovery email

source
sql
MERGE INTO app_settings d
USING (SELECT 
    'APP_EMAIL_RECOVER_TEMPLATE' AS id, 
    'Password recovery template with {{APP_DOMAIN_NAME}} and {{APP_PASSWORD_RESET_TOKEN}}' AS name, 
    q'[<!DOCTYPE html><html><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>body{font-family:Arial,sans-serif;color:#333;background:#f5f5f5;margin:0;padding:0}.container{max-width:600px;margin:20px auto;background:#fff;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);overflow:hidden}.header{background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;padding:40px 20px;text-align:center}.header h1{margin:0;font-size:28px;font-weight:bold}.content{padding:40px 20px;text-align:center}.content p{margin:15px 0;font-size:16px;line-height:1.6}.button{display:inline-block;background:linear-gradient(135deg,#667eea 0%,#764ba2 100%);color:#fff;padding:14px 32px;text-decoration:none;border-radius:6px;font-weight:bold;font-size:16px;margin:20px 0;transition:opacity .3s}.button:hover{opacity:.9}.footer{background:#f9f9f9;padding:20px;text-align:center;border-top:1px solid #eee;font-size:12px}.footer a{color:#999;text-decoration:none}.footer a:hover{text-decoration:underline}</style></head><body><div class="container"><div class="header"><h1>Reset Your Password</h1></div><div class="content"><p>We received a request to reset your password. Click the button below to create a new password.</p><a href="{{APP_DOMAIN_NAME}}/reset-password/{{APP_PASSWORD_RESET_TOKEN}}" class="button">Reset Password</a><p style="font-size:14px;color:#666">Or copy and paste this link in your browser:</p><p style="font-size:12px;color:#999;word-break:break-all">{{APP_DOMAIN_NAME}}/reset-password/{{APP_PASSWORD_RESET_TOKEN}}</p></div><div class="footer"><p>If you did not request a password reset, you can safely ignore this email.</p></div></div></body></html>]' AS value FROM dual) s
ON (d.id = s.id)
WHEN MATCHED THEN
    UPDATE SET
        d.name = s.name,
        d.value = s.value
WHEN NOT MATCHED THEN
    INSERT (id, name, value)
    VALUES (s.id, s.name, s.value);

Methods for password recovery and reset

./db/src/database/odbvue/package_specs/pck_app.sql ​

specification
plsql
    PROCEDURE post_recover_password ( -- 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_reset_password ( -- Procedure resets user password
        p_username     app_users.username%TYPE, -- Username (e-mail address)
        p_password     app_users.password%TYPE, -- Password
        p_token        app_tokens.token%TYPE, --  Password recovery token (sent by e-mail)
        r_accesstoken  OUT VARCHAR2, -- Access token
        r_refreshtoken OUT VARCHAR2, -- Refresh token
        r_errors       OUT SYS_REFCURSOR, -- Errors [{name, message}] (NULL if success)
        r_error        OUT VARCHAR2 -- Error (NULL if success)
    );

./db/src/database/odbvue/package_bodies/pck_app.sql ​

implementation
plsql
    PROCEDURE post_recover_password (
        p_username app_users.username%TYPE,
        r_error    OUT VARCHAR2
    ) AS

        v_uuid     app_users.uuid%TYPE;
        v_fullname app_users.fullname%TYPE;
        v_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 = 0 THEN
            r_error := 'wrong.username';
            pck_api_audit.warn('Recover password',
                               pck_api_audit.attributes('username', p_username, 'error', r_error));

            RETURN;
        END IF;

        pck_api_auth.revoke_token(v_uuid, 'VERIFY');
        v_token := pck_api_auth.issue_token(v_uuid, 'VERIFY');
        send_email(
            p_template   => 'APP_EMAIL_RECOVER_TEMPLATE',
            p_username   => p_username,
            p_fullname   => v_fullname,
            p_subject    => 'Recover your password',
            p_attributes => JSON_ARRAY(
                JSON_OBJECT(
                    'key' VALUE 'APP_PASSWORD_RESET_TOKEN',
                    'value' VALUE v_token
                ),
           JSON_OBJECT(
                    'key' VALUE 'APP_DOMAIN_NAME',
                       'value' VALUE pck_api_settings.read('APP_DOMAIN_NAME')
                )
            )
        );

        pck_api_audit.info('Recover password',
                           pck_api_audit.attributes('username', p_username, 'uuid', v_uuid));

    EXCEPTION
        WHEN OTHERS THEN
            r_error := 'something.went.wrong';
            pck_api_audit.error('Recover password',
                                pck_api_audit.attributes('username', p_username, 'uuid', v_uuid, 'error',
                                                         r_error));

    END post_recover_password;

    PROCEDURE post_reset_password (
        p_username     app_users.username%TYPE,
        p_password     app_users.password%TYPE,
        p_token        app_tokens.token%TYPE,
        r_accesstoken  OUT VARCHAR2,
        r_refreshtoken OUT VARCHAR2,
        r_errors       OUT SYS_REFCURSOR,
        r_error        OUT VARCHAR2
    ) AS

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

        PROCEDURE errors (
            p_name    VARCHAR2,
            p_message VARCHAR2
        ) AS
        BEGIN
            OPEN r_errors FOR SELECT
                                  p_name    AS "name",
                                  p_message AS "message"
                              FROM
                                  dual;

        END errors;

    BEGIN
        r_error := pck_api_validate.validate(p_password,
                                             JSON_ARRAY(
                                        JSON_OBJECT(
                                            'type' VALUE 'regexp',
                                                    'params' VALUE pck_api_settings.read('APP_AUTH_PASSWORD_REQUIREMENTS'),
                                                    'message' VALUE pck_api_settings.read('APP_AUTH_PASSWORD_MESSAGE')
                                        )
                                    ));

        IF r_error IS NOT NULL THEN
            errors('password', r_error);
            pck_api_audit.warn('Signup',
                               pck_api_audit.attributes('username', p_username, 'error', r_error));

            r_error := NULL;
            RETURN;
        END IF;

        BEGIN
            SELECT
                uuid
            INTO v_uuid
            FROM
                app_users
            WHERE
                uuid IN (
                    SELECT
                        uuid
                    FROM
                        app_tokens
                    WHERE
                            token = p_token
                        AND type_id = 'VERIFY'
                        AND expiration > systimestamp
                )
                AND username = TRIM(upper(p_username));

        EXCEPTION
            WHEN no_data_found THEN
                r_error := 'invalid.token';
                pck_api_audit.warn('Reset password',
                                   pck_api_audit.attributes('username', p_username, 'error', r_error, 'uudi',
                                                            v_uuid));

                RETURN;
        END;

        UPDATE app_users
        SET
            password = v_password,
            attempts = 0,
            attempted = NULL,
            status = 'A',
            accessed = systimestamp
        WHERE
            uuid = v_uuid;

        COMMIT;
        pck_api_auth.revoke_token(v_uuid, 'VERIFY');
        pck_api_auth.revoke_token(v_uuid, 'REFRESH');
        r_accesstoken := pck_api_auth.issue_token(v_uuid, 'ACCESS');
        r_refreshtoken := pck_api_auth.issue_token(v_uuid, 'REFRESH');
        pck_api_audit.info('Reset password successful',
                           pck_api_audit.attributes('username', p_username, 'uuid', v_uuid));

    EXCEPTION
        WHEN OTHERS THEN
            r_accesstoken := NULL;
            r_refreshtoken := NULL;
            r_error := 'something.went.wrong';
            pck_api_audit.error('Reset password error',
                                pck_api_audit.attributes('username', p_username, 'uuid', v_uuid, 'error',
                                                         r_error));

    END post_reset_password;

Store ​

Methods fore resetting password

@/stores/app/auth.ts ​

source
ts
// ...

    const recoverPassword = async (username: string): Promise<boolean> => {
      startLoading()
      const { data, error } = await api.post<{error: string}>('app/recover-password/', {
        username,
      })
      if (data?.error || error) {
        setError(data?.error || 'something.went.wrong')
      } else {
        setInfo('password.recovery.email.sent')
      }
      stopLoading()
      return !data?.error
    }

    type ResetPasswordResponse = {
      access_token: string
      refresh_token: string
      error?: string
      errors?: { name: string; message: string }[]
    }

    const resetPassword = async (
      username: string,
      password: string,
      token: string,
    ): Promise<ResetPasswordResponse | null> => {
      startLoading()
      const { data, error } = await api.post<ResetPasswordResponse>('app/reset-password/', {
        username,
        password,
        token,
      })
      const success = data && !data?.error && !error && !data?.errors
      if (success) {
        accessToken.value = data.access_token
        Cookies.set('refresh_token', data.refresh_token, refreshCookieOptions)
        isAuthenticated.value = true
        clearMessages()
        await useAppStore().init()
      } else {
        if (data?.error || error) setError(data?.error || 'something.went.wrong')
      }
      stopLoading()
      return data
    }

    return {
      // ...
      recoverPassword,
      resetPassword,
    }

Views ​

@/pages/recover-password.vue ​

source
vue
<template>
  <v-container>
    <v-row justify="center">
      <v-col cols="12" :md="4">
        <h1 class="mb-4">{{ t('recover.password') }}</h1>
        <v-ov-form v-if="!sent" :options :data :t @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: {
    visibility: 'never',
    access: 'when-unauthenticated',
  },
})

const app = useAppStore()
const router = useRouter()
const { t } = useI18n()

const sent = ref(false)

const options = <OvFormOptions>{
  fields: [
    {
      type: 'text',
      name: 'username',
      label: 'username',
      placeholder: 'username',
      rules: [
        { type: 'required', params: true, message: 'username.is.required' },
        { type: 'email', params: true, message: 'username.must.be.a.valid.e-mail.address' },
      ],
    },
  ],
  actions: [
    {
      name: 'submit',
      format: { text: 'send' },
    },
  ],
  actionAlign: 'right',
  actionSubmit: 'submit',
}

const data = ref({
  username: '',
})

const submit = async (newData: typeof data.value) => {
  sent.value = await app.auth.recoverPassword(newData.username)
}
</script>

@/pages/reset-password/[token].vue ​

source
vue
<template>
  <v-container>
    <v-row justify="center">
      <v-col cols="12" :md="4">
        <h1 class="mb-4">{{ t('reset.password') }}</h1>
        <v-ov-form v-if="!done" :options :data :t @submit="submit" />
        <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: {
    visibility: 'never',
    access: 'always',
  },
})

const appStore = useAppStore()
const router = useRouter()
const route = useRoute()
const { t } = useI18n()

const done = ref(false)

const options = ref<OvFormOptions>({
  fields: [
    {
      type: 'text',
      name: 'username',
      label: 'username',
      placeholder: 'username',
      rules: [
        { type: 'required', params: true, message: 'username.is.required' },
        { type: 'email', params: true, message: 'username.must.be.a.valid.e-mail.address' },
      ],
    },
    {
      type: 'password',
      name: 'password',
      label: 'password',
      placeholder: 'password',
      rules: [{ type: 'required', params: true, message: 'password.is.required' }],
    },
    {
      type: 'password',
      name: 'password2',
      label: 'password.repeat',
      placeholder: 'password.repeat',
      rules: [
        { type: 'required', params: true, message: 'password.is.required' },
        { type: 'same-as', params: 'password', message: 'passwords.must.match' },
      ],
    },
  ],
  actions: [
    {
      name: 'submit',
      format: { text: 'reset.password' },
    },
  ],
  actionAlign: 'right',
  actionSubmit: 'submit',
})

const data = ref({
  username: '',
  password: '',
  password2: '',
})

const submit = async (newData: typeof data.value) => {
  const recoverToken = (route.params as { token: string }).token
  const resetResponse = await appStore.auth.resetPassword(
    newData.username,
    newData.password,
    recoverToken,
  )
  options.value.errors = resetResponse?.errors || []
  if (!resetResponse?.error && !resetResponse?.errors) {
    router.push((route.query.redirect as string) || '/')
  } else {
    data.value.password = ''
    data.value.password2 = ''
  }
}
</script>

@/src/pages/login.vue ​

vue
{{ t('not.registered.yet') }}
<a href="/signup">{{ t('sign.up') }}</a>
|
<a href="/recover-password">{{ t('forgot.password') }}</a>