Skip to content

Commit

Permalink
feat: add refresh token methods
Browse files Browse the repository at this point in the history
  • Loading branch information
LorexIQ committed Nov 21, 2023
1 parent 4aa97ac commit 53a106a
Show file tree
Hide file tree
Showing 8 changed files with 154 additions and 75 deletions.
52 changes: 18 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,4 @@
<!--
Get your module up and running quickly.
Find and replace all on all files (CMD+SHIFT+F):
- Name: My Module
- Package name: my-module
- Description: My new Nuxt module
-->

# My Module
# Nuxt Local Auth

[![npm version][npm-version-src]][npm-version-href]
[![npm downloads][npm-downloads-src]][npm-downloads-href]
Expand All @@ -23,36 +14,36 @@ My new Nuxt module for doing amazing things.
## Features

<!-- Highlight some of the features your module provide here -->
- &nbsp;Foo
- 🚠 &nbsp;Bar
- 🌲 &nbsp;Baz
- Refresh token
- SignUp user registration
- SignIn with url context token

## Quick Setup

1. Add `my-module` dependency to your project
1. Add `nuxt-local-auth` dependency to your project

```bash
# Using pnpm
pnpm add -D my-module
pnpm add -D nuxt-local-auth

# Using yarn
yarn add --dev my-module
yarn add --dev nuxt-local-auth

# Using npm
npm install --save-dev my-module
npm install --save-dev nuxt-local-auth
```

2. Add `my-module` to the `modules` section of `nuxt.config.ts`
2. Add `nuxt-local-auth` to the `modules` section of `nuxt.config.ts`

```js
```ts
export default defineNuxtConfig({
modules: [
'my-module'
'nuxt-local-auth'
]
})
```

That's it! You can now use My Module in your Nuxt app ✨
That's it! You can now use Nuxt Local Auth in your Nuxt app ✨

## Development

Expand All @@ -69,26 +60,19 @@ npm run dev
# Build the playground
npm run dev:build

# Run ESLint
npm run lint

# Run Vitest
npm run test
npm run test:watch

# Release new version
npm run release
```

<!-- Badges -->
[npm-version-src]: https://img.shields.io/npm/v/my-module/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-version-href]: https://npmjs.com/package/my-module
[npm-version-src]: https://img.shields.io/npm/v/nuxt-local-auth/latest.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-version-href]: https://npmjs.com/package/nuxt-local-auth

[npm-downloads-src]: https://img.shields.io/npm/dm/my-module.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-downloads-href]: https://npmjs.com/package/my-module
[npm-downloads-src]: https://img.shields.io/npm/dm/nuxt-local-auth.svg?style=flat&colorA=18181B&colorB=28CF8D
[npm-downloads-href]: https://npmjs.com/package/nuxt-local-auth

[license-src]: https://img.shields.io/npm/l/my-module.svg?style=flat&colorA=18181B&colorB=28CF8D
[license-href]: https://npmjs.com/package/my-module
[license-src]: https://img.shields.io/npm/l/nuxt-local-auth.svg?style=flat&colorA=18181B&colorB=28CF8D
[license-href]: https://npmjs.com/package/nuxt-local-auth

[nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js
[nuxt-href]: https://nuxt.com
12 changes: 8 additions & 4 deletions playground/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
export default defineNuxtConfig({
ssr: false,
//modules: ['../src/module'],
modules: ['nuxt-local-auth'],
modules: ['../src/module'],
//modules: ['nuxt-local-auth'],

localAuth: {
origin: 'https://catman-dev.atrinix.ru/api/v1/',
token: {
path: 'access',
refreshPath: 'refresh',
lifetime: 5,
path: 'access'
},
refreshToken: {
enabled: true
},
endpoints: {
signIn: { path: '/auth/', method: 'POST' },
getMe: { path: 'users/me/', method: 'GET' },
refreshToken: { path: '/auth/refresh/', method: 'POST' }
}
},

Expand Down
8 changes: 2 additions & 6 deletions playground/pages/index.vue
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
<script setup lang="ts">
const route = useRoute()
const auth = useLocalAuth();
function testSignOut() {
auth.signOut();
}
definePageMeta({
localAuth: true
})
Expand All @@ -14,6 +9,7 @@ definePageMeta({
<template>
<div>
{{auth}}
<button @click="testSignOut">testSignOut</button>
<button @click="() => auth.signOut()">testSignOut</button>
<button @click="() => auth.refreshTokenWithCheck()">testRefreshToken</button>
</div>
</template>
31 changes: 25 additions & 6 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ interface ModuleOptionsEndpoints {
* method: 'GET'
* */
getMe: ModuleOptionsEndpointConfig;
/* Refresh token config. Default:
* path: 'auth/refresh
* method: 'POST'
* */
refreshToken?: ModuleOptionsEndpointConfig;
/* Registration user config. Default: undefined -> disabled
* */
signUp?: ModuleOptionsEndpointConfig;
Expand All @@ -43,15 +48,23 @@ interface ModuleOptionsToken {
* Example #2: { data: { accessToken: '...' } } > value: 'data/accessToken'
* */
path: string;
/* Path to refresh token data. Default: undefined -> refresh disabled
/* Token type. Default: 'Bearer'
* */
type?: string;
}
interface ModuleOptionsRefreshToken {
/* Enabled refresh sessions in app. Default: false
* */
enabled: boolean;
/* Path to refresh token data. Default: 'refresh'
* Example #1: { refresh: '...' } > value: 'refresh'
* Example #2: { data: { refreshToken: '...' } } > value: 'data/refreshToken'
* Example #3: { } > value: undefined
* */
refreshPath?: string;
/* Token type. Default: 'Bearer'
path?: string;
/* Name of key token in body request. Default: 'refresh'
* Example: { refresh '...' } > value: 'refresh'
* */
type?: string;
bodyKey?: string;
}
interface ModuleOptionsPages {
/* Page for authorization in the system. Default: '/login'
Expand All @@ -72,6 +85,7 @@ export interface ModuleOptions {
* */
cookiePrefix?: string;
token: ModuleOptionsToken;
refreshToken: ModuleOptionsRefreshToken;
endpoints: ModuleOptionsEndpoints;
pages: ModuleOptionsPages;
}
Expand All @@ -87,12 +101,17 @@ export default defineNuxtModule<ModuleOptions>({
token: {
lifetime: 86400,
path: 'token',
refreshPath: undefined,
type: 'Bearer'
},
refreshToken: {
enabled: false,
path: 'refresh',
bodyKey: 'refresh'
},
endpoints: {
signIn: { path: 'auth/signIn', method: 'POST' },
getMe: { path: 'users/me', method: 'GET' },
refreshToken: { path: 'auth/refresh', method: 'POST' },
signUp: undefined,
signOut: undefined
},
Expand Down
66 changes: 57 additions & 9 deletions src/runtime/composables/useLocalAuth.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useRouter, useNuxtApp, callWithNuxt, useRuntimeConfig } from '#app';
import { computed } from 'vue';
import useLocalAuthState from './useLocalAuthState';
import { LocalAuthError } from '../errors';
import type {ModuleOptions} from "../../module";
import type {
UseLocalAuthData,
Expand All @@ -10,13 +11,6 @@ import useUtils from "./useUtils";

const { trimStartWithSymbol } = useUtils();

class LocalAuthError extends Error {
constructor(message: string = '') {
super(message);
this.name = 'LocalAuthError';
}
}

async function getContext() {
const nuxt = useNuxtApp();
const options = (await callWithNuxt(nuxt, useRuntimeConfig)).public.localAuth as ModuleOptions;
Expand Down Expand Up @@ -101,7 +95,59 @@ async function getMe<T>(): Promise<T> {
throw new LocalAuthError(`getMe > [${e.statusCode}] > ${JSON.stringify(e.response._data)}`);
}
}
// async function refreshToken()
async function refreshToken<T>(): Promise<T> {
const { options, state: { origin, meta, saveSession, clearSession } } = await getContext();
const endpointConfig = options.endpoints.refreshToken!;
const refreshConfig = options.refreshToken;

if (refreshConfig.enabled) {
try {
const refreshData = await $fetch(
`${origin}/${trimStartWithSymbol(endpointConfig.path, '/')}`,
{
method: endpointConfig.method,
body: {
[`${refreshConfig.bodyKey}`]: meta.value.refreshToken
}
}
);

saveSession({
[`${refreshConfig.path}`]: meta.value.refreshToken,
...refreshData
}, true);

return refreshData;
} catch (e: any) {
throw new LocalAuthError(`refreshToken > [${e.statusCode}] > ${JSON.stringify(e.response._data)}`);
}

} else {
throw new LocalAuthError('refreshToken > refresh token is disabled. Enable it in refreshToken/enabled');
}
}
async function refreshTokenWithCheck<T>(): Promise<T | null> {
const {
options: {
refreshToken: refreshTokenConfig,
token: tokenConfig
},
state: {
meta
}
} = await getContext();
const metaData = meta.value;

try {
if (!refreshTokenConfig.enabled) throw Error('refresh token is disabled. Enable it in refreshToken/enabled');
if (metaData.status !== 'authorized') throw Error('session is not found. Use signIn');
if (Date.now() < +meta.value.exp! * 1000) return null;

return await refreshToken();
} catch (e: any) {
throw new LocalAuthError(`refreshTokenWithCheck > ${e.message}`);
}
}

export function useLocalAuth() {
const { data, meta, token } = useLocalAuthState()
Expand All @@ -115,7 +161,9 @@ export function useLocalAuth() {
const actions = {
signIn,
signOut,
getMe
getMe,
refreshToken,
refreshTokenWithCheck
};

return {
Expand Down
52 changes: 37 additions & 15 deletions src/runtime/composables/useLocalAuthState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default function () {
const sessionMetaInfo: Ref<UseLocalAuthSession> = useState('localAuth:Meta', () => ({
token: null,
refreshToken: null,
lastSessionUpdate: null,
exp: null,
status: 'unknown'
}));

Expand All @@ -33,7 +33,7 @@ export default function () {

value.token = decodeCookie.token;
value.refreshToken = decodeCookie.refreshToken;
value.lastSessionUpdate = decodeCookie.lastSessionUpdate;
value.exp = decodeCookie.exp;
value.status = decodeCookie.status;
}
} catch (e) {}
Expand All @@ -43,37 +43,59 @@ export default function () {
const token = computed(() => sessionMetaInfo.value.token ? `${options.token.type} ${sessionMetaInfo.value.token}`.trim() : null);
const origin = trimWithSymbol(options.origin, '/');

function softClearSession(): void {
sessionMetaInfo.value = {
token: null,
refreshToken: null,
exp: null,
status: sessionMetaInfo.value.status
};
}
function clearSession(): void {
sessionMetaInfo.value = {
token: null,
refreshToken: null,
lastSessionUpdate: null,
exp: null,
status: 'unauthorized'
};

for (const key of Object.keys(sessionData.value)) {
delete sessionData.value[key];
}
}
function saveSession(data: UseLocalAuthResponse): void {
clearSession();
function saveSession(data: UseLocalAuthResponse, metaUpdate: boolean = false): void {
function parseValueWithPath(data: UseLocalAuthResponse, path: string): string | undefined {
return path
.split('/')
.reduce((accum, current) => {
if (!accum) return undefined;
accum = accum[current];
return accum;
}, data as UseLocalAuthResponse | undefined) as string | undefined;
}

const parsedToken = options.token.path
.split('/')
.reduce((accum, current) => accum = accum[current], data) as unknown as string | undefined;
metaUpdate ? softClearSession() : clearSession();
let parsedToken, parsedRefreshToken, parsedLifetime;

parsedToken = parseValueWithPath(data, options.token.path);
if (!parsedToken) throw new Error('error parse auth token. Check current token/path');
sessionMetaInfo.value.token = parsedToken as string;

if (options.token.refreshPath) {
const parsedRefreshToken = options.token.refreshPath
.split('/')
.reduce((accum, current) => accum = accum[current], data) as unknown as string | undefined;
if (options.refreshToken.enabled) {
parsedRefreshToken = parseValueWithPath(data, options.refreshToken.path!);
if (!parsedToken) throw new Error('error parse refresh token. Check current token/refreshPath');
sessionMetaInfo.value.refreshToken = parsedRefreshToken as string;
}

sessionMetaInfo.value.lastSessionUpdate = `${Math.round(Date.now() / 1000)}`;
const lifetime = options.token.lifetime as string | number;
if (typeof options.token.lifetime === 'string') {
parsedLifetime = parseValueWithPath(data, options.token.lifetime!);
if (!parsedLifetime) throw new Error('error parse lifetime token. Check current token/lifetime');
} else {
parsedLifetime = `${Math.round(Date.now() / 1000) + (lifetime as number)}`;
}

sessionMetaInfo.value.token = parsedToken as string;
sessionMetaInfo.value.refreshToken = parsedRefreshToken as string;
sessionMetaInfo.value.exp = `${parsedLifetime}`;
}

const getters = {
Expand Down
Loading

0 comments on commit 53a106a

Please sign in to comment.