Skip to content

Commit

Permalink
feat: add reset, change password endopints and optout endpoint (#9)
Browse files Browse the repository at this point in the history
* feat: add reset and change endpoints and related method in useBeditaAuth() composable

* feat: optout endpoint and related method in useBeditaAuth composable

* refactor: change signature

* chore: add type in import

* feat(playground): add examples for reset password and optout
  • Loading branch information
batopa authored Jan 9, 2024
1 parent a222f73 commit d487707
Show file tree
Hide file tree
Showing 11 changed files with 347 additions and 6 deletions.
2 changes: 1 addition & 1 deletion playground/components/recaptcha-badge.vue
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
</p>
</template>

<style>
<style scoped>
p {
font-size: 12px;
}
Expand Down
54 changes: 54 additions & 0 deletions playground/pages/forgot-password.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<template>
<div>
<h2>Forgot Password Page</h2>
<div v-if="!isLogged">
<form>
<div>
<label>
<div>Insert your email</div>
<input v-model="email" type="email" required placeholder="john.smith@example.com">
</label>
</div>
<div style="margin-top: 10px;">
<RecaptchaBadge v-if="showCustomBadge" />
<button :disabled="isLoading" @click.prevent="send()">Send</button>
</div>
<p v-if="error" style="color: red;">An error occured. Please, try again.</p>
<p v-if="done" style="color: green;">An email for reset password has sent to you. Follow the instructions.</p>
</form>
</div>

<div v-else>
<p>Hello {{ user?.name }} {{ user?.surname }}.</p>
<p>To test the "forgot password" you need to logout.</p>
<div>
<button @click="logout">Logout</button>
</div>
</div>
</div>
</template>

<script setup lang="ts">
const { user, isLogged, resetPassword, logout } = useBeditaAuth();

const email = ref('');
const error = ref(false);
const isLoading = ref(false);
const done = ref(false);
const showCustomBadge = useRuntimeConfig().public.recaptcha.hideBadge;

const send = async () => {
error.value = false;
isLoading.value = true;
done.value = false;
try {
await resetPassword(email.value);
done.value = true;
email.value = '';
} catch (e) {
error.value = true;
}
isLoading.value = false;
}

</script>
2 changes: 2 additions & 0 deletions playground/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,7 @@
<li><NuxtLink to="/login">Login</NuxtLink></li>
<li><NuxtLink to="/signup">Signup</NuxtLink></li>
<li><NuxtLink to="/signup-activation">Signup Activation</NuxtLink></li>
<li><NuxtLink to="/forgot-password">Forgot password</NuxtLink></li>
<li><NuxtLink to="/optout">Opt-out</NuxtLink></li>
</ul>
</template>
52 changes: 52 additions & 0 deletions playground/pages/optout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<template>
<div>
<h2>Opt-out Page</h2>
<div>
<p v-if="isLogged">Hello {{ user?.name }} {{ user?.surname }}</p>
<p>Insert user e password to delete your account.</p>
</div>
<div>
<form>
<div>
<label>
<div>Username</div>
<input v-model="username" type="text">
</label>
</div>
<div>
<label>
<div>Password</div>
<input v-model="password" type="password">
</label>
</div>
<div style="margin-top: 10px;">
<RecaptchaBadge v-if="showCustomBadge" />
<button :disabled="isLoading" @click.prevent="deleteAccount()">Delete account</button>
</div>
<p v-if="error" style="color: red;">An error occured. Please, try again.</p>
</form>
</div>
</div>
</template>

<script setup lang="ts">
const { user, isLogged, optOut } = useBeditaAuth();

const username = ref('');
const password = ref('');
const error = ref(false);
const isLoading = ref(false);
const showCustomBadge = useRuntimeConfig().public.recaptcha.hideBadge;

const deleteAccount = async () => {
error.value = false;
isLoading.value = true;
try {
await optOut(username.value, password.value);
} catch (e) {
error.value = true;
}
isLoading.value = false;
}

</script>
54 changes: 54 additions & 0 deletions playground/pages/reset-password.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<template>
<div>
<h2>Reset Password Page</h2>
<div v-if="!isLogged">
<form>
<label>
<div>New password</div>
<input v-model="password" type="password" required>
</label>
<div style="margin-top: 10px;">
<RecaptchaBadge v-if="showCustomBadge" />
<button :disabled="isLoading" @click.prevent="change()">Change password</button>
<button :disabled="isLoading" style="margin-left: 10px;" @click.prevent="change(true)">Change password and login</button>
</div>
<p v-if="error" style="color: red;">An error occured. Please, try again.</p>
<p v-if="done" style="color: green;">Password change successful. Go to <NuxtLink to="/login">Login</NuxtLink></p>
</form>
</div>

<div v-else>
<p>Hello {{ user?.name }} {{ user?.surname }}.</p>
<p v-if="done">Password change successful.</p>
<p v-else>To test the "reset password" you need to logout.</p>
<div>
<button @click="logout">Logout</button>
</div>
</div>
</div>
</template>

<script setup lang="ts">
const { user, isLogged, changePassword, logout } = useBeditaAuth();

const password = ref('');
const error = ref(false);
const isLoading = ref(false);
const done = ref(false);
const showCustomBadge = useRuntimeConfig().public.recaptcha.hideBadge;

const change = async (login = false) => {
error.value = false;
isLoading.value = true;
done.value = false;
try {
await changePassword(password.value, login);
done.value = true;
password.value = '';
} catch (e) {
error.value = true;
}
isLoading.value = false;
}

</script>
50 changes: 46 additions & 4 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface ModuleOptions {
hideBadge?: boolean,
useRecaptchaNet?: boolean,
},
resetPasswordPath?: string,
session: {
name: string,
secret: string,
Expand All @@ -45,6 +46,7 @@ export default defineNuxtModule<ModuleOptions>({
hideBadge: false,
useRecaptchaNet: false,
},
resetPasswordPath: '/reset-password',
session: {
name: 'bedita',
secret: '',
Expand All @@ -59,6 +61,7 @@ export default defineNuxtModule<ModuleOptions>({
apiBaseUrl: options.apiBaseUrl,
apiKey: options.apiKey,
recaptchaSecretKey: options.recaptcha.secretKey,
resetPasswordPath: options.resetPasswordPath,
session: options.session,
});
runtimeConfig.public = defu(runtimeConfig.public || {}, {
Expand All @@ -78,7 +81,11 @@ export default defineNuxtModule<ModuleOptions>({
resolver.resolve('../node_modules/tslib'), // transpile tslib used by @atlasconsulting/bedita-sdk
);

// Server utils
/*
****************
* Server utils *
****************
*/
// addServerImportsDir(resolver.resolve('./runtime/server/utils'));
addServerImports([
{
Expand All @@ -99,7 +106,12 @@ export default defineNuxtModule<ModuleOptions>({
},
]);

// Server API
/*
**************
* Server API *
**************
*/
// auth endpoints
addServerHandler({
route: '/api/bedita/auth/login',
handler: resolver.resolve('./runtime/server/api/bedita/auth/login.post'),
Expand All @@ -110,6 +122,23 @@ export default defineNuxtModule<ModuleOptions>({
handler: resolver.resolve('./runtime/server/api/bedita/auth/logout'),
});
logger.info('API endpoint /api/bedita/auth/logout added.');
addServerHandler({
route: '/api/bedita/auth/reset',
handler: resolver.resolve('./runtime/server/api/bedita/auth/reset.post'),
});
logger.info('API endpoint /api/bedita/auth/reset added.');
addServerHandler({
route: '/api/bedita/auth/change',
handler: resolver.resolve('./runtime/server/api/bedita/auth/change.patch'),
});
logger.info('API endpoint /api/bedita/auth/change added.');
addServerHandler({
route: '/api/bedita/auth/optout',
handler: resolver.resolve('./runtime/server/api/bedita/auth/optout.post'),
});
logger.info('API endpoint /api/bedita/auth/optout added.');

// signup endpoints
addServerHandler({
route: '/api/bedita/signup',
handler: resolver.resolve('./runtime/server/api/bedita/signup/signup.post'),
Expand All @@ -121,17 +150,30 @@ export default defineNuxtModule<ModuleOptions>({
});
logger.info('API endpoint /api/bedita/signup/activation added.');

// middlewares
/*
**************
* Middlewares *
**************
*/
addRouteMiddleware({
name: 'beditaAuth',
path: resolver.resolve('./runtime/middleware/auth'),
global: true,
});

// composables and client utils
/*
********************************
* Composables and client utils *
********************************
*/
addImportsDir(resolver.resolve('./runtime/utils'));
addImportsDir(resolver.resolve('./runtime/composables'));

/*
*****************
* Type template *
*****************
*/
addTypeTemplate({
filename: 'types/nuxt-bedita.d.ts',
getContents: () => [
Expand Down
51 changes: 50 additions & 1 deletion src/runtime/composables/useBeditaAuth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useRecaptcha } from '../composables/useRecaptcha';
import { useUserState } from '../states/user';
import { computed, type ComputedRef } from '#imports';
import { computed, type ComputedRef, useRoute } from '#imports';
import type { UserAuth } from '../types';
import { filterUserDataToStore } from '../utils/user-data-store';
import { RecaptchaActions } from '../utils/recaptcha-helpers';
Expand Down Expand Up @@ -33,10 +33,59 @@ export const useBeditaAuth = () => {
user.value = null;
};

const resetPassword = async (contact: string) => {
const recaptcha_token = await executeRecaptcha(RecaptchaActions.RESET_PASSWORD);

return await $fetch('/api/bedita/auth/reset', {
method: 'POST',
body: {
contact,
recaptcha_token
},
});
};

const changePassword = async (password: string, login = false, uuid?: string) => {
const recaptcha_token = await executeRecaptcha(RecaptchaActions.CHANGE_PASSWORD);
const route = useRoute();

const data = await $fetch<UserAuth>('/api/bedita/auth/change', {
method: 'PATCH',
body: {
uuid: uuid || route.query?.uuid,
password,
login,
recaptcha_token
},
});

if (login === true) {
user.value = filterUserDataToStore(data);
}

return data;
};

const optOut = async (username: string, password: string) => {
const recaptcha_token = await executeRecaptcha(RecaptchaActions.OPTOUT);

return await $fetch('/api/bedita/auth/optout', {
method: 'POST',
body: {
username,
password,
recaptcha_token
},
});
};

return {
user,
isLogged,
login,
logout,
resetPassword,
changePassword,
optOut,
};
}
37 changes: 37 additions & 0 deletions src/runtime/server/api/bedita/auth/change.patch.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { defineEventHandler, readBody } from 'h3';
import { recaptchaVerifyToken } from '../../../utils/recaptcha';
import { beditaClient, handleBeditaApiError } from '../../../utils/bedita-client';
import { RecaptchaActions } from '../../../../utils/recaptcha-helpers';
import type { UserAuth } from '../../../../types';
import { filterUserDataToStore } from '../../../../utils/user-data-store';
import { type BEditaClientRequestConfig, FormatUserInterceptor } from '@atlasconsulting/bedita-sdk';

export default defineEventHandler(async (event) => {
try {
const body = await readBody(event);
await recaptchaVerifyToken(body?.recaptcha_token, RecaptchaActions.CHANGE_PASSWORD);
const client = await beditaClient(event);
const payload = {
uuid: body?.uuid,
password: body?.password,
login: body?.login === true,
};
const requestConfig: BEditaClientRequestConfig = {
responseInterceptors: [ new FormatUserInterceptor(client) ]
}
const response = await client.patch('/auth/change', payload, requestConfig);

// if login is true it fills session with tokens and user data
if (body?.login === true) {
const storageService = client.getStorageService();
await storageService.setAccessToken(response.data?.meta?.jwt);
await storageService.setRefreshToken(response.data?.meta?.renew);
await storageService.set('user', filterUserDataToStore(response?.formattedData));
}

return response.formattedData as UserAuth;
} catch (error) {
return handleBeditaApiError(event, error);
}
});

Loading

0 comments on commit d487707

Please sign in to comment.