feat: pick username

* put new registered social login user to session data
* ask to choose username
* simplify checkUsername functions
This commit is contained in:
wancat committed 2023-10-13 00:05:59 +08:00
1 parent 5591689816
commit 182fcf643f
12 files changed
+244 -69

No files matched your search

+11 -13
View File
@@ -34,7 +34,7 @@ const handleLogin = async () => {
try {
const isSuccess = await login(username.value, password.value)
if (isSuccess) success()
else errorMessage.value = 'Invalid username or password.'
else errorMessage.value = 'Wrong username or password.'
} catch (err) {
console.error(err)
errorMessage.value = 'Something went wrong.'
@@ -47,20 +47,18 @@ async function check() {
const res = await api.get('/auth/check-username', {
params: { username: user },
})
// user not exists, try register
const { type } = res.data
if (type === 'email') {
email.value = user
username.value = ''
}
isRegister.value = true
usernameInput.value.focus()
} catch (err) {
if (err.response?.status === 409) {
// user already exists, try login
const { type, available } = res.data
if (available) {
if (type === 'email') {
email.value = user
username.value = ''
}
isRegister.value = true
usernameInput.value.focus()
} else {
await handleLogin()
return
}
} catch (err) {
if (err.response?.status === 400) {
errorMessage.value = 'Invalid username or email.'
return
+82
View File
@@ -0,0 +1,82 @@
<script setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import api from '../utils/api'
const username = ref('')
const errorMessage = ref('')
const router = useRouter()
async function checkUsername() {
const user = username.value.trim()
errorMessage.value = ''
if (user.length > 3) {
try {
const { data } = await api.get('/auth/check-username', {
params: { username: user },
})
const { available } = data
if (!available) {
errorMessage.value = 'Username is already taken.'
} else {
errorMessage.value = ''
}
} catch {
errorMessage.value = 'Invalid username.'
}
}
}
async function handleSubmit() {
if (errorMessage.value) {
return
}
try {
const { data } = await api.post('/auth/register-from-session', {
username: username.value.trim(),
})
console.log(data)
router.push({ name: 'game' })
} catch (err) {
console.error(err.message)
if (err.response?.status === 409) {
errorMessage.value = 'Username is already taken.'
} else {
errorMessage.value = 'Something went wrong.'
}
}
}
</script>
<template>
<div
class="bg-background border-8 border-background-primary flex flex-1 flex-col items-center justify-center rounded-3xl p-10"
>
<h1 class="text-text-primary text-3xl font-black mb-8">Choose username</h1>
<form @submit.prevent="handleSubmit" class="w-full">
<div class="mb-6">
<label for="username" class="text-sm font-semibold text-text"
>Username</label
><input
type="text"
id="username"
ref="usernameInput"
class="my-4 bg-background-primary text-text-primary bg- rounded-md block w-full px-3 h-10 shadow-sm focus:outline-none placeholder:text-text-line focus:ring-2 focus:ring-text-primary ring-1 ring-background-secondary"
placeholder="Enter your email or username"
v-model="username"
@input="checkUsername"
autocomplete="username"
/>
</div>
<p class="text-red-500 flex justify-center" v-if="errorMessage">
{{ errorMessage }}
</p>
<button
class="inline-flex justify-center rounded-lg text-sm font-black py-2 mt-3 bg-text-primary text-background w-full"
type="submit"
>
<span>Sign up</span>
</button>
</form>
</div>
</template>
+7
View File
@@ -1,6 +1,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import GamePage from './views/GamePage.vue'
import AuthPage from './views/AuthPage.vue'
import ChooseUsernamePage from './views/ChooseUsernamePage.vue'
import { isLoggedIn } from './utils/auth'
const routes = [
@@ -20,6 +21,12 @@ const routes = [
component: GamePage,
meta: { requiresAuth: true },
},
{
path: '/choose-username',
name: 'choose-username',
component: ChooseUsernamePage,
meta: { requiresGuest: true },
},
]
const router = createRouter({
+16
View File
@@ -0,0 +1,16 @@
<script setup>
import UsernameForm from '../components/UsernameForm.vue'
import Background from '../components/DynamicBackground.vue'
</script>
<template>
<div class="relative w-screen h-screen">
<Background class="w-full h-full" />
<div
class="h-screen w-screen absolute top-0 left-0 flex flex-wrap justify-center content-center"
>
<div class="h-2/3 w-1/3">
<UsernameForm />
</div>
</div>
</div>
</template>
+66 -23
View File
@@ -16,6 +16,14 @@ export async function issueToken(req, res) {
res.json({ token })
}
async function invalidUsername(username) {
return !isValidUsername(username) || User.exists({ username })
}
async function invalidEmail(email) {
return !isValidEmail(email) || User.exists({ email })
}
export async function checkUsername(req, res) {
const { username, email } = req.query
const cred = username || email
@@ -27,30 +35,16 @@ export async function checkUsername(req, res) {
}
if (isValidEmail(cred)) {
if (await User.exists({ email: cred })) {
res.status(409).json({
type: 'email',
message: `"${cred}" already exists`,
})
return
}
res.status(200).json({
res.json({
type: 'email',
message: `"${cred}" is available`,
available: !(await User.exists({ email: cred })),
})
return
}
if (isValidUsername(cred)) {
if (await User.exists({ username: cred })) {
res.status(409).json({
type: 'username',
message: `"${cred}" already exists`,
})
return
}
res.status(200).json({
res.json({
type: 'username',
message: `"${cred}" is available`,
available: !(await User.exists({ username: cred })),
})
return
}
@@ -68,18 +62,18 @@ export async function register(req, res, next) {
return
}
if (await User.exists({ username })) {
if (await invalidUsername(username)) {
res.status(409).json({
type: 'username',
message: 'username already exists',
message: 'Invalid username',
})
return
}
if (await User.exists({ email })) {
if (await invalidEmail(email)) {
res.status(409).json({
type: 'email',
message: 'email already exists',
message: 'Invalid email',
})
return
}
@@ -126,6 +120,55 @@ export function logout(req, res) {
export function checkSession(req, res) {
res.json({
loggedIn: req.isAuthenticated(),
loggedIn: req.isAuthenticated() && !req.session.newUser,
})
}
export async function socialLogin(req, res) {
const { newUser } = req.user
if (newUser) {
req.session.newUser = newUser
res.redirect('/choose-username')
return
}
res.redirect('/')
}
export async function registerFromSession(req, res) {
const { username } = req.body
const { newUser } = req.session
if (!newUser) {
res.status(400).json({
message: 'no new user found',
})
return
}
if (await invalidUsername(username)) {
res.status(409).json({
type: 'username',
message: 'Invalid username',
})
return
}
newUser.username = username
const user = new User(newUser)
await user.save()
req.login(user, (err) => {
if (err) {
console.error(err)
res.status(500).json({
message: 'error logging in',
})
return
}
delete req.session.newUser
res.status(201).json({
message: 'user created',
})
})
}
+6 -14
View File
@@ -8,6 +8,8 @@ import {
checkUsername,
checkSession,
logout,
socialLogin,
registerFromSession,
} from '../controllers/authController.js'
const router = Router()
@@ -34,24 +36,14 @@ router.get('/available-methods', (req, res) => {
if (enabledMethods.google) {
router.get('/google', passport.authenticate('google'))
router.get(
'/google/callback',
passport.authenticate('google', {
successRedirect: '/',
failureRedirect: '/login',
})
)
router.get('/google/callback', passport.authenticate('google'), socialLogin)
}
if (enabledMethods.github) {
router.get('/github', passport.authenticate('github'))
router.get(
'/github/callback',
passport.authenticate('github', {
successRedirect: '/',
failureRedirect: '/login',
})
)
router.get('/github/callback', passport.authenticate('github'), socialLogin)
}
router.post('/register-from-session', registerFromSession)
export default router
+7 -7
View File
@@ -5,8 +5,6 @@ import User from '../models/user.js'
function verify(accessToken, refreshToken, profile, cb) {
process.nextTick(async () => {
console.log(accessToken, refreshToken, profile)
// cb('todo')
const email = profile.emails[0]?.value
if (!email) {
cb(new Error('No email found'))
@@ -21,16 +19,18 @@ function verify(accessToken, refreshToken, profile, cb) {
displayName: profile.displayName,
}
if (!user) {
user = new User({
const newUser = {
email,
github,
})
} else if (user.github?.id) {
}
cb(null, { newUser })
return
}
if (user.github?.id) {
cb(new Error('Email already used and binded to another account'))
return
} else {
user.github = github
}
user.github = github
await user.save()
}
+6 -4
View File
@@ -19,16 +19,18 @@ function verify(issuer, profile, cb) {
displayName: profile.displayName,
}
if (!user) {
user = new User({
const newUser = new User({
email,
google,
})
} else if (user.google?.id) {
cb(null, { newUser })
return
}
if (user.google?.id) {
cb(new Error('Email already used and binded to another account'))
return
} else {
user.google = google
}
user.google = google
await user.save()
}
+1 -1
View File
@@ -1,5 +1,5 @@
export default function authRequired(req, res, next) {
if (req.isAuthenticated()) {
if (req.isAuthenticated() && !req.session.newUser) {
next()
return
}
+1 -2
View File
@@ -4,8 +4,7 @@ const User = model('User', {
username: {
type: String,
unique: true,
required: false, // should be required,
// but need to let social login users to pick one after login
required: true,
},
email: {
type: String,
+1 -1
View File
@@ -56,7 +56,7 @@ export function isValidUsername(username) {
if (!usernamePattern.test(username)) return false
// check if the username is between 1 and 32 characters
if (username.length < 1 || username.length > 32) {
if (username.length < 4 || username.length > 32) {
return false
}
+40 -4
View File
@@ -211,6 +211,31 @@ paths:
type: string
example: User already exists
/auth/register-from-session:
post:
summary: Register a new user from a session, for social login to pick a username
security:
- cookieAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
properties:
username:
type: string
format: username
example: alex
responses:
'201':
description: User created
headers:
Set-Cookie:
schema:
type: string
example: connect.sid=s%3Aq1Z; Path=/; HttpOnly; SameSite=Strict; Secure
/auth/login:
post:
summary: Log in a user
@@ -297,19 +322,30 @@ paths:
type: string
format: username
example: alex
- in: query
name: email
description: email to check
required: true
schema:
type: string
format: email
example: alex@example.com
responses:
'200':
description: Username is available
description: Username / Email checking result
content:
application/json:
schema:
type: object
properties:
message:
type:
type: string
example: Username is available
'409':
description: Username is already taken
available:
type: boolean
example: true
'400':
description: Bad Request
content:
application/json:
schema: