Skip to content
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

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

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
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
Expand Up @@ -8,7 +8,8 @@ export default defineNuxtConfig({
provider: {
type: 'local',
endpoints: {
getSession: { path: '/user' }
getSession: { path: '/user' },
signUp: { path: '/signup', method: 'post' }
},
pages: {
login: '/'
Expand Down
4 changes: 4 additions & 0 deletions playground-local/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
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="regUsername">
<input v-model="password" type="password" placeholder="Password" data-testid="regPassword">
<button type="submit" data-testid="regSubmit">
iamKiNG-Fr marked this conversation as resolved.
Show resolved Hide resolved
sign up
</button>
</form>
<div v-if="response">
<h2>Response</h2>
<pre data-testid="regResponse">{{ response }}</pre>
iamKiNG-Fr marked this conversation as resolved.
Show resolved Hide resolved
</div>
</div>
</template>
40 changes: 40 additions & 0 deletions playground-local/server/api/auth/signup.post.ts
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you please update your handler to be closer to a demo implementation introduced in #901?

https://github.com/sidebase/nuxt-auth/blob/main/playground-local/server/api/auth/login.post.ts

I think you either can:

  1. return a "user" object and no access token (we are doing preventLoginFlow, right?) in
    const user = {
    username,
    picture: 'https://github.com/nuxt.png',
    name: `User ${username}`
    }
    ;
  2. or do both "user" object and access tokens as in signIn handler so that reference is usable by others as inspiration and also by tests.
    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)
    return {
    token: {
    accessToken,
    refreshToken
    }
    }

It is fine for me if you don't want to implement option 2, I can take it over.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should take over to implement option 2.

I'm not so confident in working with tokens, i would like to learn by seeing your implementation.
I don't mind trying to implement it though if you don't mind, although it might take some trial and error on on my part.

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { createError, eventHandler, readBody } from 'h3'
import { z } from 'zod'
import { sign } from 'jsonwebtoken'

export const SECRET = 'dummy'

export default eventHandler(async (event) => {
// Define the schema for validating the incoming data
const result = z.object({
username: z.string(),
password: z.string().min(6)
}).safeParse(await readBody(event))

// If validation fails, return an error
if (!result.success) {
throw createError({
statusCode: 400,
statusMessage: 'Invalid input, please provide a valid email and a password of at least 6 characters.'
})
}

const { username } = result.data

const expiresIn = '1h' // token expiry (1 hour)
const user = { username } // Payload for the token, includes the email

// Sign the JWT with the user payload and secret
const accessToken = sign(user, SECRET, { expiresIn })

// Return a success response with the email and the token
return {
message: 'Signup successful!',
user: {
username
},
token: {
accessToken
}
}
})
26 changes: 26 additions & 0 deletions playground-local/tests/local.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,4 +59,30 @@ describe('local Provider', async () => {
await signoutButton.click()
await playwrightExpect(status).toHaveText(STATUS_UNAUTHENTICATED)
})

it('should sign up and return signup data when preventLoginFlow: true', async () => {
const page = await createPage('/register') // Navigate to signup page

const [
usernameInput,
passwordInput,
submitButton,
] = await Promise.all([
page.getByTestId('regUsername'),
page.getByTestId('regPassword'),
page.getByTestId('regSubmit')
iamKiNG-Fr marked this conversation as resolved.
Show resolved Hide resolved
])

await usernameInput.fill('newuser')
await passwordInput.fill('hunter2')

// Click button and wait for API to finish
const responsePromise = page.waitForResponse(/\/api\/auth\/signup/)
await submitButton.click()
const response = await responsePromise

// Expect the response to return signup data
const responseBody = await response.json() // Parse response
playwrightExpect(responseBody).toBeDefined() // Ensure data is returned
})
})
10 changes: 6 additions & 4 deletions src/runtime/composables/local/useAuth.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { type Ref, readonly } from 'vue'

import type { CommonUseAuthReturn, GetSessionOptions, SecondarySignInOptions, SignInFunc, SignOutFunc, SignUpOptions } from '../../types'
import { jsonPointerGet, objectFromJsonPointer, useTypedBackendConfig } from '../../helpers'
import { _fetch } from '../../utils/fetch'
Expand Down Expand Up @@ -160,17 +159,20 @@ async function getSession(getSessionOptions?: GetSessionOptions): Promise<Sessio
return data.value
}

async function signUp(credentials: Credentials, signInOptions?: SecondarySignInOptions, signUpOptions?: SignUpOptions) {
async function signUp<T>(credentials: Credentials, signInOptions?: SecondarySignInOptions, signUpOptions?: SignUpOptions): Promise<T> {
const nuxt = useNuxtApp()

const { path, method } = useTypedBackendConfig(useRuntimeConfig(), 'local').endpoints.signUp
await _fetch(nuxt, path, {

// Holds result from fetch to be returned if signUpOptions?.preventLoginFlow is true
const result = await _fetch<T>(nuxt, path, {
method,
body: credentials
})

if (signUpOptions?.preventLoginFlow) {
return
// Returns result
iamKiNG-Fr marked this conversation as resolved.
Show resolved Hide resolved
return result
}

return signIn(credentials, signInOptions)
Expand Down
Loading