Skip to content

Commit

Permalink
Merge pull request #142 from ranseur92/rfc/136
Browse files Browse the repository at this point in the history
feat: implement rfc#136 (oauth, token refresh)
  • Loading branch information
Intevel authored May 13, 2023
2 parents cf16f2b + 9ff4c19 commit d9e42d1
Show file tree
Hide file tree
Showing 6 changed files with 124 additions and 26 deletions.
8 changes: 7 additions & 1 deletion playground/app.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@
<button style="margin-top: 25px" @click="onSubmit">
Login with Directus
</button>
<button style="margin-top: 25px" @click="loginWithProvider('discord')">
Login with OAuth
</button>
<button style="margin-top: 25px" @click="logout">
Logout
</button>
<button style="margin-top: 25px" @click="fetchSingleArticle">
Fetch Single Article
</button>
Expand Down Expand Up @@ -47,7 +53,7 @@
<script setup lang="ts">
import { DirectusUserRequest, DirectusUserUpdate } from '../src/runtime/types'
const { login } = useDirectusAuth()
const { login, loginWithProvider, logout } = useDirectusAuth()
const user = useDirectusUser()
const { getItems, getItemById, createItems, deleteItems } = useDirectusItems()
const { getCollections } = useDirectusCollections()
Expand Down
33 changes: 30 additions & 3 deletions src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,18 @@ export interface ModuleOptions {
* @type boolean
*/
autoFetch?: boolean;
/**
* Auto refesh tokens
* @default true
* @type boolean
*/
autoRefresh?: boolean;
/**
* Auto refesh tokens
* @default true
* @type boolean
*/
onAutoRefreshFailure?: () => Promise<void>;
/**
* fetch user params
* @type boolean
Expand Down Expand Up @@ -46,6 +58,13 @@ export interface ModuleOptions {
* @default 'directus_refresh_token'
*/
cookieNameRefreshToken?: string;

/**
* The max age for the refresh token cookie in seconds.
* This should match your directus env key REFRESH_TOKEN_TTL
* @type string
*/
maxAgeRefreshToken?: number;
}

export default defineNuxtModule<ModuleOptions>({
Expand All @@ -60,9 +79,11 @@ export default defineNuxtModule<ModuleOptions>({
defaults: {
url: process.env.NUXT_DIRECTUS_URL,
autoFetch: true,
autoRefresh: false,
devtools: false,
cookieNameToken: 'directus_token',
cookieNameRefreshToken: 'directus_refresh_token'
cookieNameRefreshToken: 'directus_refresh_token',
maxAgeRefreshToken: 604800
},
setup (options, nuxt) {
// Nuxt 2 / Bridge
Expand All @@ -72,11 +93,14 @@ export default defineNuxtModule<ModuleOptions>({
{
url: options.url,
autoFetch: options.autoFetch,
autoRefresh: options.autoRefresh,
onAutoRefreshFailure: options.onAutoRefreshFailure,
fetchUserParams: options.fetchUserParams,
token: options.token,
devtools: options.devtools,
cookieNameToken: options.cookieNameToken,
cookieNameRefreshToken: options.cookieNameRefreshToken
cookieNameRefreshToken: options.cookieNameRefreshToken,
maxAgeRefreshToken: options.maxAgeRefreshToken
}
)
}
Expand All @@ -86,11 +110,14 @@ export default defineNuxtModule<ModuleOptions>({
nuxt.options.runtimeConfig.public.directus = defu(nuxt.options.runtimeConfig.public.directus, {
url: options.url,
autoFetch: options.autoFetch,
autoRefresh: options.autoRefresh,
onAutoRefreshFailure: options.onAutoRefreshFailure,
fetchUserParams: options.fetchUserParams,
token: options.token,
devtools: options.devtools,
cookieNameToken: options.cookieNameToken,
cookieNameRefreshToken: options.cookieNameRefreshToken
cookieNameRefreshToken: options.cookieNameRefreshToken,
maxAgeRefreshToken: options.maxAgeRefreshToken
})

const runtimeDir = fileURLToPath(new URL('./runtime', import.meta.url))
Expand Down
6 changes: 4 additions & 2 deletions src/runtime/composables/useDirectus.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { useDirectusToken } from './useDirectusToken'
export const useDirectus = () => {
const baseURL = useDirectusUrl()
const config = useRuntimeConfig()
const { token } = useDirectusToken()
const { token, token_expired, refreshToken, refreshTokens, checkAutoRefresh } = useDirectusToken()

return async <T>(
url: string,
Expand All @@ -15,7 +15,9 @@ export const useDirectus = () => {
): Promise<T> => {
const headers: HeadersInit = {}

if (token && token.value) {
await checkAutoRefresh();

if (token?.value && !token_expired.value) {
headers.Authorization = `Bearer ${token.value}`
} else if (config.public.directus.token && useStaticToken) {
headers.Authorization = `Bearer ${config.public.directus.token}`
Expand Down
36 changes: 24 additions & 12 deletions src/runtime/composables/useDirectusAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ import { useDirectusToken } from './useDirectusToken'
export const useDirectusAuth = () => {
const config = useRuntimeConfig()
const directus = useDirectus()
const baseUrl = useDirectusUrl()
const user = useDirectusUser()
const route = useRoute();
const { token, refreshToken, expires } = useDirectusToken()

const setAuthCookies = (_token: string, _refreshToken: string, _expires: number) => {
Expand Down Expand Up @@ -69,14 +71,11 @@ export const useDirectusAuth = () => {
): Promise<DirectusAuthResponse> => {
removeTokens()

const response: { data: DirectusAuthResponse } = await directus(
'/auth/login',
{
method: 'POST',
body: data
},
useStaticToken
)
const response = await $fetch<{data: DirectusAuthResponse}>('/auth/login', {
baseURL: baseUrl,
body: data,
method: 'POST'
})

if (!response.data.access_token) { throw new Error('Login failed, please check your credentials.') }
setAuthCookies(response.data.access_token, response.data.refresh_token, response.data.expires)
Expand All @@ -91,6 +90,15 @@ export const useDirectusAuth = () => {
}
}

const loginWithProvider = async (
provider: string,
redirectOnLogin?: string
) => {
removeTokens()
const redirect = `${window.location.origin}${redirectOnLogin ?? route.fullPath}`;
await navigateTo(`${baseUrl}/auth/login/${provider}?redirect=${encodeURIComponent(redirect)}`, { external: true })
}

const createUser = async (
data: DirectusRegisterCredentials,
useStaticToken?: boolean
Expand Down Expand Up @@ -130,10 +138,13 @@ export const useDirectusAuth = () => {
}

const logout = async (): Promise<void> => {
await directus('/auth/logout', {
method: 'POST',
body: { refresh_token: refreshToken.value }

await $fetch('/auth/logout', {
baseURL: baseUrl,
body: { refresh_token: refreshToken.value },
method: 'POST'
})

removeTokens()
setUser(null)
await fetchUser()
Expand All @@ -147,6 +158,7 @@ export const useDirectusAuth = () => {
resetPassword,
logout,
createUser,
register
register,
loginWithProvider
}
}
31 changes: 29 additions & 2 deletions src/runtime/composables/useDirectusToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const useDirectusToken = () => {
return nuxtApp._cookies[config.directus.cookieNameRefreshToken]
}

const cookie = useCookie<string | null>(config.directus.cookieNameRefreshToken)
const cookie = useCookie<string | null>(config.directus.cookieNameRefreshToken, { maxAge: config.directus.maxAgeRefreshToken })
nuxtApp._cookies[config.directus.cookieNameRefreshToken] = cookie
return cookie
}
Expand Down Expand Up @@ -59,5 +59,32 @@ export const useDirectusToken = () => {
}
}

return { token: token(), refreshToken: refreshToken(), refreshTokens, expires: expires() }
const token_expires_in = computed(() => Math.max(0, (expires().value ?? 0) - new Date().getTime()));

const token_expired = computed(() => !token().value || token_expires_in.value == 0);

const checkAutoRefresh = async () => {
if (config.directus.autoRefresh) {
if (token_expired.value) {
try {
await refreshTokens();
} catch (e) {
refreshToken().value = null;
if (config.directus.onAutoRefreshError) {
await config.directus.onAutoRefreshError();
}
}
}
}
}

return {
token: token(),
refreshToken: refreshToken(),
expires: expires(),
token_expires_in,
token_expired,
refreshTokens,
checkAutoRefresh
}
}
36 changes: 30 additions & 6 deletions src/runtime/plugin.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,35 @@
import { defineNuxtPlugin, useRuntimeConfig } from '#app'
import { useDirectusAuth } from './composables/useDirectusAuth'

import { useDirectusToken } from './composables/useDirectusToken';
import { useDirectusUser } from './composables/useDirectusUser';
import { useDirectusAuth } from './composables/useDirectusAuth';

export default defineNuxtPlugin(async (nuxtApp) => {
const config = useRuntimeConfig()
if (config.public.directus.autoFetch) {
const { fetchUser } = useDirectusAuth()

await fetchUser()
const config = useRuntimeConfig();
const { fetchUser } = useDirectusAuth();
const { token, checkAutoRefresh } = useDirectusToken();
const user = useDirectusUser();

async function checkIfUserExists() {
if (config.public.directus.autoFetch) {
if (!user.value && token.value) {
await fetchUser();
}
}
}
})

nuxtApp.hook('app:created', async () => {
if (process.server) {
await checkAutoRefresh();
await checkIfUserExists();
}
})

nuxtApp.hook('page:start', async () => {
if (process.client) {
await checkAutoRefresh();
await checkIfUserExists();
}
})
})

0 comments on commit d9e42d1

Please sign in to comment.