Skip to content

Commit fba0eea

Browse files
iamKiNG-Frzoey-kaiserphoenix-ru
authored
Enh(#843): Allow signup flow return data when preventLoginFlow is true (#903)
* added enhancment to allow signUp function return response if SignUpOption?.PreventLoginFlow is true * sign up function to return data, test written * Discard changes to pnpm-lock.yaml * fix: oxlint issue * enh: added generic type support to signUp function for flexible return types * Resolved lint errors and formatting issues * Update src/runtime/composables/local/useAuth.ts Co-authored-by: Zoey <[email protected]> * Update playground-local/pages/register.vue by aligning test-ids Co-authored-by: Marsel Shayhin <[email protected]> * Update playground-local/pages/register.vue to remove unused test-id regResponse Co-authored-by: Marsel Shayhin <[email protected]> * Update playground-local/tests/local.spec.ts by unifying updated test-ids Co-authored-by: Marsel Shayhin <[email protected]> * Update src/runtime/composables/local/useAuth.ts Co-authored-by: Marsel Shayhin <[email protected]> * fix: fix a type in `signUp` function * refact: make demo implementation more unified * test: fix E2E tests * chore: remove unused imports --------- Co-authored-by: Zoey <[email protected]> Co-authored-by: Marsel Shayhin <[email protected]> Co-authored-by: Marsel Shaikhin <[email protected]>
1 parent 5e6182f commit fba0eea

File tree

10 files changed

+328
-114
lines changed

10 files changed

+328
-114
lines changed

playground-local/nuxt.config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ export default defineNuxtConfig({
88
provider: {
99
type: 'local',
1010
endpoints: {
11-
getSession: { path: '/user' }
11+
getSession: { path: '/user' },
12+
signUp: { path: '/signup', method: 'post' }
1213
},
1314
pages: {
1415
login: '/'

playground-local/pages/index.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@ definePageMeta({ auth: false })
1010
-> manual login, logout, refresh button
1111
</nuxt-link>
1212
<br>
13+
<nuxt-link to="/register">
14+
-> Click to signup
15+
</nuxt-link>
16+
<br>
1317
<nuxt-link to="/protected/globally">
1418
-> globally protected page
1519
</nuxt-link>

playground-local/pages/register.vue

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<script setup>
2+
import { ref } from 'vue'
3+
import { definePageMeta, useAuth } from '#imports'
4+
5+
const { signUp } = useAuth()
6+
7+
const username = ref('')
8+
const password = ref('')
9+
const response = ref()
10+
11+
async function register() {
12+
try {
13+
const signUpResponse = await signUp({ username: username.value, password: password.value }, undefined, { preventLoginFlow: true })
14+
response.value = signUpResponse
15+
}
16+
catch (error) {
17+
response.value = { error: 'Failed to sign up' }
18+
console.error(error)
19+
}
20+
}
21+
22+
definePageMeta({
23+
auth: {
24+
unauthenticatedOnly: true,
25+
navigateAuthenticatedTo: '/',
26+
},
27+
})
28+
</script>
29+
30+
<template>
31+
<div>
32+
<form @submit.prevent="register">
33+
<p><i>*password should have at least 6 characters</i></p>
34+
<input v-model="username" type="text" placeholder="Username" data-testid="register-username">
35+
<input v-model="password" type="password" placeholder="Password" data-testid="register-password">
36+
<button type="submit" data-testid="register-submit">
37+
sign up
38+
</button>
39+
</form>
40+
<div v-if="response">
41+
<h2>Response</h2>
42+
<pre>{{ response }}</pre>
43+
</div>
44+
</div>
45+
</template>
Lines changed: 6 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,11 @@
11
import { createError, eventHandler, readBody } from 'h3'
2-
import { z } from 'zod'
3-
import { sign } from 'jsonwebtoken'
2+
import { createUserTokens, credentialsSchema, getUser } from '~/server/utils/session'
43

54
/*
65
* DISCLAIMER!
76
* This is a demo implementation, please create your own handlers
87
*/
98

10-
/**
11-
* This is a demo secret.
12-
* Please ensure that your secret is properly protected.
13-
*/
14-
export const SECRET = 'dummy'
15-
16-
/** 30 seconds */
17-
export const ACCESS_TOKEN_TTL = 30
18-
19-
export interface User {
20-
username: string
21-
name: string
22-
picture: string
23-
}
24-
25-
export interface JwtPayload extends User {
26-
scope: Array<'test' | 'user'>
27-
exp?: number
28-
}
29-
30-
interface TokensByUser {
31-
access: Map<string, string>
32-
refresh: Map<string, string>
33-
}
34-
35-
/**
36-
* Tokens storage.
37-
* You will need to implement your own, connect with DB/etc.
38-
*/
39-
export const tokensByUser: Map<string, TokensByUser> = new Map()
40-
41-
/**
42-
* We use a fixed password for demo purposes.
43-
* You can use any implementation fitting your usecase.
44-
*/
45-
const credentialsSchema = z.object({
46-
username: z.string().min(1),
47-
password: z.literal('hunter2')
48-
})
49-
509
export default eventHandler(async (event) => {
5110
const result = credentialsSchema.safeParse(await readBody(event))
5211
if (!result.success) {
@@ -56,42 +15,13 @@ export default eventHandler(async (event) => {
5615
})
5716
}
5817

59-
// Emulate login
60-
const { username } = result.data
61-
const user = {
62-
username,
63-
picture: 'https://github.com/nuxt.png',
64-
name: `User ${username}`
65-
}
18+
// Emulate successful login
19+
const user = await getUser(result.data.username)
6620

67-
const tokenData: JwtPayload = { ...user, scope: ['test', 'user'] }
68-
const accessToken = sign(tokenData, SECRET, {
69-
expiresIn: ACCESS_TOKEN_TTL
70-
})
71-
const refreshToken = sign(tokenData, SECRET, {
72-
// 1 day
73-
expiresIn: 60 * 60 * 24
74-
})
75-
76-
// Naive implementation - please implement properly yourself!
77-
const userTokens: TokensByUser = tokensByUser.get(username) ?? {
78-
access: new Map(),
79-
refresh: new Map()
80-
}
81-
userTokens.access.set(accessToken, refreshToken)
82-
userTokens.refresh.set(refreshToken, accessToken)
83-
tokensByUser.set(username, userTokens)
21+
// Sign the tokens
22+
const tokens = await createUserTokens(user)
8423

8524
return {
86-
token: {
87-
accessToken,
88-
refreshToken
89-
}
25+
token: tokens
9026
}
9127
})
92-
93-
export function extractToken(authorizationHeader: string) {
94-
return authorizationHeader.startsWith('Bearer ')
95-
? authorizationHeader.slice(7)
96-
: authorizationHeader
97-
}
Lines changed: 11 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { createError, eventHandler, getRequestHeader, readBody } from 'h3'
2-
import { sign, verify } from 'jsonwebtoken'
3-
import { type JwtPayload, SECRET, type User, extractToken, tokensByUser } from './login.post'
2+
import { checkUserTokens, decodeToken, extractTokenFromAuthorizationHeader, getTokensByUser, refreshUserAccessToken } from '~/server/utils/session'
43

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

2221
// Verify
23-
const decoded = verify(refreshToken, SECRET) as JwtPayload | undefined
22+
const decoded = decodeToken(refreshToken)
2423
if (!decoded) {
2524
throw createError({
2625
statusCode: 401,
2726
statusMessage: 'Unauthorized, refreshToken can\'t be verified'
2827
})
2928
}
3029

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

4039
// Check against known token
41-
const requestAccessToken = extractToken(authorizationHeader)
42-
const knownAccessToken = userTokens.refresh.get(body.refreshToken)
43-
if (!knownAccessToken || knownAccessToken !== requestAccessToken) {
40+
const requestAccessToken = extractTokenFromAuthorizationHeader(authorizationHeader)
41+
const tokensValidityCheck = checkUserTokens(userTokens, requestAccessToken, refreshToken)
42+
if (!tokensValidityCheck.valid) {
4443
console.log({
4544
msg: 'Tokens mismatch',
46-
knownAccessToken,
45+
knownAccessToken: tokensValidityCheck.knownAccessToken,
4746
requestAccessToken
4847
})
4948
throw createError({
@@ -52,25 +51,10 @@ export default eventHandler(async (event) => {
5251
})
5352
}
5453

55-
// Invalidate old access token
56-
userTokens.access.delete(knownAccessToken)
57-
58-
const user: User = {
59-
username: decoded.username,
60-
picture: decoded.picture,
61-
name: decoded.name
62-
}
63-
64-
const accessToken = sign({ ...user, scope: ['test', 'user'] }, SECRET, {
65-
expiresIn: 60 * 5 // 5 minutes
66-
})
67-
userTokens.refresh.set(refreshToken, accessToken)
68-
userTokens.access.set(accessToken, refreshToken)
54+
// Call the token refresh logic
55+
const tokens = await refreshUserAccessToken(userTokens, refreshToken)
6956

7057
return {
71-
token: {
72-
accessToken,
73-
refreshToken
74-
}
58+
token: tokens
7559
}
7660
})
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { createError, eventHandler, readBody } from 'h3'
2+
import { createUserTokens, credentialsSchema, getUser } from '~/server/utils/session'
3+
4+
export default eventHandler(async (event) => {
5+
const result = credentialsSchema.safeParse(await readBody(event))
6+
if (!result.success) {
7+
throw createError({
8+
statusCode: 400,
9+
statusMessage: `Invalid input, please provide a valid username, and a password must be 'hunter2' for this demo.`
10+
})
11+
}
12+
13+
// Emulate successful registration
14+
const user = await getUser(result.data.username)
15+
16+
// Create the sign-in tokens
17+
const tokens = await createUserTokens(user)
18+
19+
// Return a success response with the email and the token
20+
return {
21+
user,
22+
token: tokens
23+
}
24+
})

playground-local/server/api/auth/user.get.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
11
import { createError, eventHandler, getRequestHeader } from 'h3'
2-
import { verify } from 'jsonwebtoken'
3-
import { type JwtPayload, SECRET, extractToken, tokensByUser } from './login.post'
2+
import { type JwtPayload, checkUserAccessToken, decodeToken, extractTokenFromAuthorizationHeader, getTokensByUser } from '~/server/utils/session'
43

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

11-
const extractedToken = extractToken(authorizationHeader)
10+
const requestAccessToken = extractTokenFromAuthorizationHeader(authorizationHeader)
1211
let decoded: JwtPayload
1312
try {
14-
decoded = verify(extractedToken, SECRET) as JwtPayload
13+
const decodeTokenResult = decodeToken(requestAccessToken)
14+
15+
if (!decodeTokenResult) {
16+
throw new Error('Expected decoded JwtPayload to be non-empty')
17+
}
18+
decoded = decodeTokenResult
1519
}
1620
catch (error) {
1721
console.error({
@@ -21,9 +25,18 @@ export default eventHandler((event) => {
2125
throw createError({ statusCode: 403, statusMessage: 'You must be logged in to use this endpoint' })
2226
}
2327

28+
// Get tokens of a user (only for demo, use a DB in your implementation)
29+
const userTokens = getTokensByUser(decoded.username)
30+
if (!userTokens) {
31+
throw createError({
32+
statusCode: 404,
33+
statusMessage: 'User not found'
34+
})
35+
}
36+
2437
// Check against known token
25-
const userTokens = tokensByUser.get(decoded.username)
26-
if (!userTokens || !userTokens.access.has(extractedToken)) {
38+
const tokensValidityCheck = checkUserAccessToken(userTokens, requestAccessToken)
39+
if (!tokensValidityCheck.valid) {
2740
throw createError({
2841
statusCode: 401,
2942
statusMessage: 'Unauthorized, user is not logged in'

0 commit comments

Comments
 (0)