diff --git a/app/src/components/AuthenticationPart.vue b/app/src/components/AuthenticationPart.vue
index 3f2126a..5ceadf9 100644
--- a/app/src/components/AuthenticationPart.vue
+++ b/app/src/components/AuthenticationPart.vue
@@ -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
diff --git a/app/src/components/UsernameForm.vue b/app/src/components/UsernameForm.vue
new file mode 100644
index 0000000..05e9e6f
--- /dev/null
+++ b/app/src/components/UsernameForm.vue
@@ -0,0 +1,82 @@
+
+
+
+
diff --git a/app/src/router.js b/app/src/router.js
index 40dd4dc..726f439 100644
--- a/app/src/router.js
+++ b/app/src/router.js
@@ -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({
diff --git a/app/src/views/ChooseUsernamePage.vue b/app/src/views/ChooseUsernamePage.vue
new file mode 100644
index 0000000..cec4a14
--- /dev/null
+++ b/app/src/views/ChooseUsernamePage.vue
@@ -0,0 +1,16 @@
+
+
+
+
diff --git a/server/src/api/controllers/authController.js b/server/src/api/controllers/authController.js
index 2d8cf15..acca879 100644
--- a/server/src/api/controllers/authController.js
+++ b/server/src/api/controllers/authController.js
@@ -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',
+ })
})
}
diff --git a/server/src/api/routes/auth.js b/server/src/api/routes/auth.js
index 4687610..8b9954d 100644
--- a/server/src/api/routes/auth.js
+++ b/server/src/api/routes/auth.js
@@ -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
diff --git a/server/src/auth/githubStrategy.js b/server/src/auth/githubStrategy.js
index 7b2120a..ae4e5d7 100644
--- a/server/src/auth/githubStrategy.js
+++ b/server/src/auth/githubStrategy.js
@@ -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()
}
diff --git a/server/src/auth/googleStrategy.js b/server/src/auth/googleStrategy.js
index adf27c0..749baa3 100644
--- a/server/src/auth/googleStrategy.js
+++ b/server/src/auth/googleStrategy.js
@@ -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()
}
diff --git a/server/src/middleware/authRequired.js b/server/src/middleware/authRequired.js
index 8a5bd4f..df0c937 100644
--- a/server/src/middleware/authRequired.js
+++ b/server/src/middleware/authRequired.js
@@ -1,5 +1,5 @@
export default function authRequired(req, res, next) {
- if (req.isAuthenticated()) {
+ if (req.isAuthenticated() && !req.session.newUser) {
next()
return
}
diff --git a/server/src/models/user.js b/server/src/models/user.js
index 5d910c7..3280c3e 100644
--- a/server/src/models/user.js
+++ b/server/src/models/user.js
@@ -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,
diff --git a/server/src/utils/auth.js b/server/src/utils/auth.js
index fc8478d..ec95600 100644
--- a/server/src/utils/auth.js
+++ b/server/src/utils/auth.js
@@ -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
}
diff --git a/server/swagger.yaml b/server/swagger.yaml
index b9f09b3..d52d7bc 100644
--- a/server/swagger.yaml
+++ b/server/swagger.yaml
@@ -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: