Log in with social account
diff --git a/app/src/components/HeaderPart.vue b/app/src/components/HeaderPart.vue
index ebf0ee5..900767f 100644
--- a/app/src/components/HeaderPart.vue
+++ b/app/src/components/HeaderPart.vue
@@ -6,7 +6,7 @@ const router = useRouter()
const handleLogout = async () => {
await logout()
- router.push({ name: 'auth' })
+ router.push({ name: 'login' })
}
diff --git a/app/src/router.js b/app/src/router.js
index 40dd4dc..84dfc8c 100644
--- a/app/src/router.js
+++ b/app/src/router.js
@@ -1,6 +1,8 @@
import { createRouter, createWebHistory } from 'vue-router'
import GamePage from './views/GamePage.vue'
-import AuthPage from './views/AuthPage.vue'
+import LoginPage from './views/LoginPage.vue'
+import RegisterPage from './views/RegisterPage.vue'
+import ChooseUsernamePage from './views/ChooseUsernamePage.vue'
import { isLoggedIn } from './utils/auth'
const routes = [
@@ -10,8 +12,14 @@ const routes = [
},
{
path: '/login',
- name: 'auth',
- component: AuthPage,
+ name: 'login',
+ component: LoginPage,
+ meta: { requiresGuest: true },
+ },
+ {
+ path: '/register',
+ name: 'register',
+ component: RegisterPage,
meta: { requiresGuest: true },
},
{
@@ -20,6 +28,12 @@ const routes = [
component: GamePage,
meta: { requiresAuth: true },
},
+ {
+ path: '/choose-username',
+ name: 'choose-username',
+ component: ChooseUsernamePage,
+ meta: { requiresGuest: true },
+ },
]
const router = createRouter({
@@ -34,7 +48,7 @@ router.beforeEach(async (to, from, next) => {
// Check if the user isn't authenticated
// Redirect to the login page
next({
- name: 'auth',
+ name: 'register',
query: { redirect: to.fullPath }, // Optionally pass a redirect parameter
})
} else if (to.meta.requiresGuest && loggedIn) {
diff --git a/app/src/views/AuthPage.vue b/app/src/views/AuthPage.vue
deleted file mode 100644
index 749522b..0000000
--- a/app/src/views/AuthPage.vue
+++ /dev/null
@@ -1,16 +0,0 @@
-
-
-
-
diff --git a/app/src/views/ChooseUsernamePage.vue b/app/src/views/ChooseUsernamePage.vue
new file mode 100644
index 0000000..decc2b4
--- /dev/null
+++ b/app/src/views/ChooseUsernamePage.vue
@@ -0,0 +1,61 @@
+
+
+
+
diff --git a/app/src/views/LoginPage.vue b/app/src/views/LoginPage.vue
new file mode 100644
index 0000000..af38994
--- /dev/null
+++ b/app/src/views/LoginPage.vue
@@ -0,0 +1,28 @@
+
+
+
+
diff --git a/app/src/views/RegisterPage.vue b/app/src/views/RegisterPage.vue
new file mode 100644
index 0000000..b34bcde
--- /dev/null
+++ b/app/src/views/RegisterPage.vue
@@ -0,0 +1,75 @@
+
+
+
+
diff --git a/docker-compose.yml b/docker-compose.yml
index 195f55f..93ca8c9 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -10,7 +10,7 @@ services:
- internal
volumes:
- ./app:/app/app
- - ./packages/file-graph:/app/packages/file-graph
+ - ./packages:/app/packages
backend:
build:
context: .
diff --git a/packages/utils/auth.js b/packages/utils/auth.js
new file mode 100644
index 0000000..cd26d47
--- /dev/null
+++ b/packages/utils/auth.js
@@ -0,0 +1,38 @@
+export function isValidUsername(username) {
+ // Define a regular expression pattern for a valid Linux username.
+ const usernamePattern = /^[a-z][a-z0-9_-]*$/
+
+ // Test the username against the pattern.
+ if (!usernamePattern.test(username)) return false
+
+ // check if the username is between 1 and 32 characters
+ if (username.length < 4 || username.length > 32) {
+ return false
+ }
+
+ const reservedWords = [
+ 'root',
+ 'admin',
+ 'administrator',
+ 'sysadmin',
+ 'system',
+ 'daemon',
+ 'bin',
+ 'nobody',
+ 'nogroup',
+ 'guest',
+ 'user',
+ ]
+ if (reservedWords.includes(username)) {
+ return false
+ }
+ return true
+}
+
+export function isValidEmail(email) {
+ // Define a regular expression pattern for a valid email address.
+ const emailPattern = /^[\w-.+]+@([\w][\w-]*\.)+[\w-]{2,}$/
+
+ // Test the email against the pattern.
+ return emailPattern.test(email)
+}
diff --git a/server/tests/utils/auth.test.js b/packages/utils/auth.test.js
similarity index 92%
rename from server/tests/utils/auth.test.js
rename to packages/utils/auth.test.js
index 4254ed8..e7d45a7 100644
--- a/server/tests/utils/auth.test.js
+++ b/packages/utils/auth.test.js
@@ -1,4 +1,4 @@
-import { isValidEmail, isValidUsername } from '../../src/utils/auth'
+import { isValidEmail, isValidUsername } from './auth.js'
test('check valid username', () => {
const valid = ['alex', 'alex123', 'john_doe', 'john-doe', 'rudeus']
diff --git a/packages/utils/index.js b/packages/utils/index.js
new file mode 100644
index 0000000..ab747e8
--- /dev/null
+++ b/packages/utils/index.js
@@ -0,0 +1 @@
+export { isValidEmail, isValidUsername } from './auth.js'
diff --git a/packages/utils/package.json b/packages/utils/package.json
new file mode 100644
index 0000000..09e9205
--- /dev/null
+++ b/packages/utils/package.json
@@ -0,0 +1,8 @@
+{
+ "name": "@linux-odyssey/utils",
+ "version": "1.0.0",
+ "main": "index.js",
+ "type": "module",
+ "exports": "./index.js",
+ "license": "MIT"
+}
diff --git a/server/Dockerfile b/server/Dockerfile
index 078876b..5bee61f 100644
--- a/server/Dockerfile
+++ b/server/Dockerfile
@@ -4,10 +4,12 @@ WORKDIR /app
COPY ./package.json yarn.lock ./
COPY server/package.json ./server/package.json
COPY packages/file-graph/package.json ./packages/file-graph/package.json
+COPY packages/utils/package.json ./packages/utils/package.json
RUN yarn install --frozen-lockfile --production
COPY ./server ./server
COPY packages/file-graph ./packages/file-graph
+COPY packages/utils ./packages/utils
COPY quests ./quests
WORKDIR /app/server
diff --git a/server/Dockerfile.dev b/server/Dockerfile.dev
index d6c2d35..488c1ca 100644
--- a/server/Dockerfile.dev
+++ b/server/Dockerfile.dev
@@ -4,6 +4,7 @@ WORKDIR /app
COPY package.json yarn.lock ./
COPY server/package.json ./server/package.json
COPY packages/file-graph/package.json ./packages/file-graph/package.json
+COPY packages/utils/package.json ./packages/utils/package.json
RUN yarn install --frozen-lockfile
COPY server/nodemon.json ./server/nodemon.json
diff --git a/server/package.json b/server/package.json
index 317b6bb..7ba551b 100644
--- a/server/package.json
+++ b/server/package.json
@@ -11,6 +11,7 @@
},
"dependencies": {
"@linux-odyssey/file-graph": "*",
+ "@linux-odyssey/utils": "*",
"bcrypt": "^5.1.1",
"commander": "^11.0.0",
"connect-mongo": "^5.0.0",
diff --git a/server/src/api/controllers/authController.js b/server/src/api/controllers/authController.js
index 2d8cf15..1a12555 100644
--- a/server/src/api/controllers/authController.js
+++ b/server/src/api/controllers/authController.js
@@ -1,10 +1,6 @@
+import { isValidEmail, isValidUsername } from '@linux-odyssey/utils'
import User from '../../models/user.js'
-import {
- genJWT,
- hashPassword,
- isValidEmail,
- isValidUsername,
-} from '../../utils/auth.js'
+import { genJWT, hashPassword } from '../../utils/auth.js'
export async function issueToken(req, res) {
const { user } = req
@@ -16,6 +12,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 +31,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 +58,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 +116,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..d3af989 100644
--- a/server/src/auth/githubStrategy.js
+++ b/server/src/auth/githubStrategy.js
@@ -1,41 +1,10 @@
import GitHubStrategy from 'passport-github2'
import config from '../config.js'
-import User from '../models/user.js'
+import oauthVerify from './oauthVerify.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'))
- return
- }
-
- let user = await User.findOne({ 'github.id': profile.id })
- if (!user) {
- user = await User.findOne({ email })
- const github = {
- id: profile.id,
- displayName: profile.displayName,
- }
- if (!user) {
- user = new User({
- email,
- github,
- })
- } else if (user.github?.id) {
- cb(new Error('Email already used and binded to another account'))
- return
- } else {
- user.github = github
- }
- await user.save()
- }
-
- cb(null, user)
- })
+ oauthVerify('github', profile, { 'github.id': profile.id }, cb)
}
const enabled = config.github.clientID && config.github.clientSecret
diff --git a/server/src/auth/googleStrategy.js b/server/src/auth/googleStrategy.js
index adf27c0..566a09e 100644
--- a/server/src/auth/googleStrategy.js
+++ b/server/src/auth/googleStrategy.js
@@ -1,39 +1,10 @@
import GoogleStrategy from 'passport-google-oidc'
import config from '../config.js'
-import User from '../models/user.js'
+import oauthVerify from './oauthVerify.js'
function verify(issuer, profile, cb) {
- process.nextTick(async () => {
- const email = profile.emails[0]?.value
- if (!email) {
- cb(new Error('No email found'))
- return
- }
-
- let user = await User.findOne({ 'google.id': profile.id })
- if (!user) {
- user = await User.findOne({ email })
- const google = {
- id: profile.id,
- displayName: profile.displayName,
- }
- if (!user) {
- user = new User({
- email,
- google,
- })
- } else if (user.google?.id) {
- cb(new Error('Email already used and binded to another account'))
- return
- } else {
- user.google = google
- }
- await user.save()
- }
-
- cb(null, user)
- })
+ oauthVerify('google', profile, { 'google.id': profile.id }, cb)
}
const enabled = config.google.clientID && config.google.clientSecret
diff --git a/server/src/auth/oauthVerify.js b/server/src/auth/oauthVerify.js
new file mode 100644
index 0000000..55b95ee
--- /dev/null
+++ b/server/src/auth/oauthVerify.js
@@ -0,0 +1,24 @@
+import User from '../models/user.js'
+
+export default async function oauthVerify(provider, profile, query, cb) {
+ process.nextTick(async () => {
+ const email = profile.emails[0]?.value
+ if (!email) {
+ return cb(new Error('No email found'))
+ }
+
+ let user = await User.findOne(query)
+ if (!user) {
+ user = await User.findOne({ email })
+ if (!user) {
+ const newUser = { email }
+ newUser[provider] = profile
+ return cb(null, { newUser })
+ }
+ user[provider] = profile
+ await user.save()
+ }
+
+ return cb(null, user)
+ })
+}
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..66fe0cb 100644
--- a/server/src/utils/auth.js
+++ b/server/src/utils/auth.js
@@ -47,42 +47,3 @@ export function verifyJWT(token) {
})
})
}
-
-export function isValidUsername(username) {
- // Define a regular expression pattern for a valid Linux username.
- const usernamePattern = /^[a-z][a-z0-9_-]*$/
-
- // Test the username against the pattern.
- if (!usernamePattern.test(username)) return false
-
- // check if the username is between 1 and 32 characters
- if (username.length < 1 || username.length > 32) {
- return false
- }
-
- const reservedWords = [
- 'root',
- 'admin',
- 'administrator',
- 'sysadmin',
- 'system',
- 'daemon',
- 'bin',
- 'nobody',
- 'nogroup',
- 'guest',
- 'user',
- ]
- if (reservedWords.includes(username)) {
- return false
- }
- return true
-}
-
-export function isValidEmail(email) {
- // Define a regular expression pattern for a valid email address.
- const emailPattern = /^[\w-.+]+@([\w][\w-]*\.)+[\w-]{2,}$/
-
- // Test the email against the pattern.
- return emailPattern.test(email)
-}
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: