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: