Skip to content

Enh(#843): Allow signup flow return data when preventLoginFlow is true #903

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking β€œSign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 21 commits into from
Apr 10, 2025
Merged
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
e0136bd
added enhancment to allow signUp function return response if SignUpOp…
iamKiNG-Fr Aug 24, 2024
7a4ed34
Merge branch 'main' into enh/843-signup-flow-enhancement
iamKiNG-Fr Sep 8, 2024
e3bf0fc
sign up function to return data, test written
iamKiNG-Fr Sep 8, 2024
1bdf144
Discard changes to pnpm-lock.yaml
zoey-kaiser Sep 12, 2024
d9951f6
fix: oxlint issue
zoey-kaiser Sep 12, 2024
5477cde
Merge branch 'main' into enh/843-signup-flow-enhancement
iamKiNG-Fr Sep 17, 2024
0caeefc
enh: added generic type support to signUp function for flexible retur…
iamKiNG-Fr Sep 17, 2024
a76b601
Merge branch 'enh/843-signup-flow-enhancement' of https://github.com/…
iamKiNG-Fr Sep 17, 2024
9cff966
Resolved lint errors and formatting issues
iamKiNG-Fr Sep 17, 2024
e5373a4
Merge branch 'main' into enh/843-signup-flow-enhancement
zoey-kaiser Sep 18, 2024
6ee85a0
Merge branch 'main' into enh/843-signup-flow-enhancement
iamKiNG-Fr Sep 19, 2024
b25597b
Update src/runtime/composables/local/useAuth.ts
iamKiNG-Fr Sep 19, 2024
86fd25a
Update playground-local/pages/register.vue by aligning test-ids
iamKiNG-Fr Sep 19, 2024
94cd4a3
Update playground-local/pages/register.vue to remove unused test-id r…
iamKiNG-Fr Sep 19, 2024
f11f9c4
Update playground-local/tests/local.spec.ts by unifying updated test-ids
iamKiNG-Fr Sep 19, 2024
98e33a9
Update src/runtime/composables/local/useAuth.ts
iamKiNG-Fr Sep 19, 2024
75ed74a
Merge branch 'main' into enh/843-signup-flow-enhancement
phoenix-ru Apr 10, 2025
8fd0e24
fix: fix a type in `signUp` function
phoenix-ru Apr 10, 2025
f9ac88f
refact: make demo implementation more unified
phoenix-ru Apr 10, 2025
916956f
test: fix E2E tests
phoenix-ru Apr 10, 2025
653f80c
chore: remove unused imports
phoenix-ru Apr 10, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion playground-local/nuxt.config.ts
Original file line number Diff line number Diff line change
@@ -8,7 +8,8 @@ export default defineNuxtConfig({
provider: {
type: 'local',
endpoints: {
getSession: { path: '/user' }
getSession: { path: '/user' },
signUp: { path: '/signup', method: 'post' }
},
pages: {
login: '/'
4 changes: 4 additions & 0 deletions playground-local/pages/index.vue
Original file line number Diff line number Diff line change
@@ -10,6 +10,10 @@ definePageMeta({ auth: false })
-> manual login, logout, refresh button
</nuxt-link>
<br>
<nuxt-link to="/register">
-> Click to signup
</nuxt-link>
<br>
<nuxt-link to="/protected/globally">
-> globally protected page
</nuxt-link>
45 changes: 45 additions & 0 deletions playground-local/pages/register.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<script setup>
import { ref } from 'vue'
import { definePageMeta, useAuth } from '#imports'

const { signUp } = useAuth()

const username = ref('')
const password = ref('')
const response = ref()

async function register() {
try {
const signUpResponse = await signUp({ username: username.value, password: password.value }, undefined, { preventLoginFlow: true })
response.value = signUpResponse
}
catch (error) {
response.value = { error: 'Failed to sign up' }
console.error(error)
}
}

definePageMeta({
auth: {
unauthenticatedOnly: true,
navigateAuthenticatedTo: '/',
},
})
</script>

<template>
<div>
<form @submit.prevent="register">
<p><i>*password should have at least 6 characters</i></p>
<input v-model="username" type="text" placeholder="Username" data-testid="register-username">
<input v-model="password" type="password" placeholder="Password" data-testid="register-password">
<button type="submit" data-testid="register-submit">
sign up
</button>
</form>
<div v-if="response">
<h2>Response</h2>
<pre>{{ response }}</pre>
</div>
</div>
</template>
82 changes: 6 additions & 76 deletions playground-local/server/api/auth/login.post.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,11 @@
import { createError, eventHandler, readBody } from 'h3'
import { z } from 'zod'
import { sign } from 'jsonwebtoken'
import { createUserTokens, credentialsSchema, getUser } from '~/server/utils/session'

/*
* DISCLAIMER!
* This is a demo implementation, please create your own handlers
*/

/**
* This is a demo secret.
* Please ensure that your secret is properly protected.
*/
export const SECRET = 'dummy'

/** 30 seconds */
export const ACCESS_TOKEN_TTL = 30

export interface User {
username: string
name: string
picture: string
}

export interface JwtPayload extends User {
scope: Array<'test' | 'user'>
exp?: number
}

interface TokensByUser {
access: Map<string, string>
refresh: Map<string, string>
}

/**
* Tokens storage.
* You will need to implement your own, connect with DB/etc.
*/
export const tokensByUser: Map<string, TokensByUser> = new Map()

/**
* We use a fixed password for demo purposes.
* You can use any implementation fitting your usecase.
*/
const credentialsSchema = z.object({
username: z.string().min(1),
password: z.literal('hunter2')
})

export default eventHandler(async (event) => {
const result = credentialsSchema.safeParse(await readBody(event))
if (!result.success) {
@@ -56,42 +15,13 @@ export default eventHandler(async (event) => {
})
}

// Emulate login
const { username } = result.data
const user = {
username,
picture: 'https://github.com/nuxt.png',
name: `User ${username}`
}
// Emulate successful login
const user = await getUser(result.data.username)

const tokenData: JwtPayload = { ...user, scope: ['test', 'user'] }
const accessToken = sign(tokenData, SECRET, {
expiresIn: ACCESS_TOKEN_TTL
})
const refreshToken = sign(tokenData, SECRET, {
// 1 day
expiresIn: 60 * 60 * 24
})

// Naive implementation - please implement properly yourself!
const userTokens: TokensByUser = tokensByUser.get(username) ?? {
access: new Map(),
refresh: new Map()
}
userTokens.access.set(accessToken, refreshToken)
userTokens.refresh.set(refreshToken, accessToken)
tokensByUser.set(username, userTokens)
// Sign the tokens
const tokens = await createUserTokens(user)

return {
token: {
accessToken,
refreshToken
}
token: tokens
}
})

export function extractToken(authorizationHeader: string) {
return authorizationHeader.startsWith('Bearer ')
? authorizationHeader.slice(7)
: authorizationHeader
}
38 changes: 11 additions & 27 deletions playground-local/server/api/auth/refresh.post.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { createError, eventHandler, getRequestHeader, readBody } from 'h3'
import { sign, verify } from 'jsonwebtoken'
import { type JwtPayload, SECRET, type User, extractToken, tokensByUser } from './login.post'
import { checkUserTokens, decodeToken, extractTokenFromAuthorizationHeader, getTokensByUser, refreshUserAccessToken } from '~/server/utils/session'

/*
* DISCLAIMER!
@@ -20,16 +19,16 @@ export default eventHandler(async (event) => {
}

// Verify
const decoded = verify(refreshToken, SECRET) as JwtPayload | undefined
const decoded = decodeToken(refreshToken)
if (!decoded) {
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized, refreshToken can\'t be verified'
})
}

// Get tokens
const userTokens = tokensByUser.get(decoded.username)
// Get the helper (only for demo, use a DB in your implementation)
const userTokens = getTokensByUser(decoded.username)
if (!userTokens) {
throw createError({
statusCode: 401,
@@ -38,12 +37,12 @@ export default eventHandler(async (event) => {
}

// Check against known token
const requestAccessToken = extractToken(authorizationHeader)
const knownAccessToken = userTokens.refresh.get(body.refreshToken)
if (!knownAccessToken || knownAccessToken !== requestAccessToken) {
const requestAccessToken = extractTokenFromAuthorizationHeader(authorizationHeader)
const tokensValidityCheck = checkUserTokens(userTokens, requestAccessToken, refreshToken)
if (!tokensValidityCheck.valid) {
console.log({
msg: 'Tokens mismatch',
knownAccessToken,
knownAccessToken: tokensValidityCheck.knownAccessToken,
requestAccessToken
})
throw createError({
@@ -52,25 +51,10 @@ export default eventHandler(async (event) => {
})
}

// Invalidate old access token
userTokens.access.delete(knownAccessToken)

const user: User = {
username: decoded.username,
picture: decoded.picture,
name: decoded.name
}

const accessToken = sign({ ...user, scope: ['test', 'user'] }, SECRET, {
expiresIn: 60 * 5 // 5 minutes
})
userTokens.refresh.set(refreshToken, accessToken)
userTokens.access.set(accessToken, refreshToken)
// Call the token refresh logic
const tokens = await refreshUserAccessToken(userTokens, refreshToken)

return {
token: {
accessToken,
refreshToken
}
token: tokens
}
})
24 changes: 24 additions & 0 deletions playground-local/server/api/auth/signup.post.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { createError, eventHandler, readBody } from 'h3'
import { createUserTokens, credentialsSchema, getUser } from '~/server/utils/session'

export default eventHandler(async (event) => {
const result = credentialsSchema.safeParse(await readBody(event))
if (!result.success) {
throw createError({
statusCode: 400,
statusMessage: `Invalid input, please provide a valid username, and a password must be 'hunter2' for this demo.`
})
}

// Emulate successful registration
const user = await getUser(result.data.username)

// Create the sign-in tokens
const tokens = await createUserTokens(user)

// Return a success response with the email and the token
return {
user,
token: tokens
}
})
25 changes: 19 additions & 6 deletions playground-local/server/api/auth/user.get.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { createError, eventHandler, getRequestHeader } from 'h3'
import { verify } from 'jsonwebtoken'
import { type JwtPayload, SECRET, extractToken, tokensByUser } from './login.post'
import { type JwtPayload, checkUserAccessToken, decodeToken, extractTokenFromAuthorizationHeader, getTokensByUser } from '~/server/utils/session'

export default eventHandler((event) => {
const authorizationHeader = getRequestHeader(event, 'Authorization')
if (typeof authorizationHeader === 'undefined') {
throw createError({ statusCode: 403, statusMessage: 'Need to pass valid Bearer-authorization header to access this endpoint' })
}

const extractedToken = extractToken(authorizationHeader)
const requestAccessToken = extractTokenFromAuthorizationHeader(authorizationHeader)
let decoded: JwtPayload
try {
decoded = verify(extractedToken, SECRET) as JwtPayload
const decodeTokenResult = decodeToken(requestAccessToken)

if (!decodeTokenResult) {
throw new Error('Expected decoded JwtPayload to be non-empty')
}
decoded = decodeTokenResult
}
catch (error) {
console.error({
@@ -21,9 +25,18 @@ export default eventHandler((event) => {
throw createError({ statusCode: 403, statusMessage: 'You must be logged in to use this endpoint' })
}

// Get tokens of a user (only for demo, use a DB in your implementation)
const userTokens = getTokensByUser(decoded.username)
if (!userTokens) {
throw createError({
statusCode: 404,
statusMessage: 'User not found'
})
}

// Check against known token
const userTokens = tokensByUser.get(decoded.username)
if (!userTokens || !userTokens.access.has(extractedToken)) {
const tokensValidityCheck = checkUserAccessToken(userTokens, requestAccessToken)
if (!tokensValidityCheck.valid) {
throw createError({
statusCode: 401,
statusMessage: 'Unauthorized, user is not logged in'
Loading