mirror of
https://github.com/linux-odyssey/linux-odyssey.git
synced 2026-11-08 03:20:12 +08:00
feat: add rate limit for authentication
* improve error handling in frontend
This commit is contained in:
1 parent
203114cd5e
commit
f65b72c5b4
10 files changed
+146
-42
No files matched your search
+47
-7
@@ -1,5 +1,11 @@
|
|||||||
import api from './api'
|
import api from './api'
|
||||||
|
|
||||||
|
import {
|
||||||
|
TooManyRequestsError,
|
||||||
|
UnauthorizedError,
|
||||||
|
ValidationError,
|
||||||
|
} from './errors'
|
||||||
|
|
||||||
export async function isLoggedIn() {
|
export async function isLoggedIn() {
|
||||||
const res = await api.get('/auth/check-session')
|
const res = await api.get('/auth/check-session')
|
||||||
const { loggedIn } = res.data
|
const { loggedIn } = res.data
|
||||||
@@ -12,18 +18,52 @@ export const login = async (username, password) => {
|
|||||||
return true
|
return true
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err.response?.status === 401) {
|
if (err.response?.status === 401) {
|
||||||
return false
|
throw new UnauthorizedError(err.response.data.message)
|
||||||
|
}
|
||||||
|
if (err.response?.status === 429) {
|
||||||
|
throw new TooManyRequestsError(err.response.data.message)
|
||||||
|
}
|
||||||
|
if (err.response?.status === 400) {
|
||||||
|
throw new ValidationError(err.response.data.message)
|
||||||
}
|
}
|
||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const register = (username, email, password) => {
|
export const register = async (username, email, password) => {
|
||||||
return api.post('/auth/register', {
|
try {
|
||||||
username,
|
await api.post('/auth/register', {
|
||||||
password,
|
username,
|
||||||
email,
|
password,
|
||||||
})
|
email,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} catch (err) {
|
||||||
|
const status = err.response?.status
|
||||||
|
if (status === 429) {
|
||||||
|
throw new TooManyRequestsError(err.response.data.message)
|
||||||
|
}
|
||||||
|
if (status === 401) {
|
||||||
|
throw new UnauthorizedError(err.response.data.message)
|
||||||
|
}
|
||||||
|
if (status === 400) {
|
||||||
|
const { errors } = err.response.data
|
||||||
|
if (errors?.length > 0) {
|
||||||
|
const { path, msg } = errors[0]
|
||||||
|
switch (path) {
|
||||||
|
case 'username':
|
||||||
|
throw new ValidationError('Username is already taken')
|
||||||
|
case 'email':
|
||||||
|
throw new ValidationError('Email is already taken')
|
||||||
|
case 'password':
|
||||||
|
throw new ValidationError('Password is too weak')
|
||||||
|
default:
|
||||||
|
throw new ValidationError(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const logout = () => {
|
export const logout = () => {
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
|
export class TooManyRequestsError extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'TooManyRequestsError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ValidationError extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'ValidationError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UnauthorizedError extends Error {
|
||||||
|
constructor(message) {
|
||||||
|
super(message)
|
||||||
|
this.name = 'UnauthorizedError'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
TooManyRequestsError,
|
||||||
|
ValidationError,
|
||||||
|
UnauthorizedError,
|
||||||
|
}
|
||||||
@@ -2,14 +2,30 @@
|
|||||||
import AuthForm from '../components/AuthForm.vue'
|
import AuthForm from '../components/AuthForm.vue'
|
||||||
import Background from '../components/DynamicBackground.vue'
|
import Background from '../components/DynamicBackground.vue'
|
||||||
|
|
||||||
|
import {
|
||||||
|
TooManyRequestsError,
|
||||||
|
UnauthorizedError,
|
||||||
|
ValidationError,
|
||||||
|
} from '../utils/errors'
|
||||||
import { login } from '../utils/auth'
|
import { login } from '../utils/auth'
|
||||||
|
|
||||||
const handleLogin = async ({ username, password, success, error }) => {
|
const handleLogin = async ({ username, password, success, error }) => {
|
||||||
try {
|
try {
|
||||||
const isSuccess = await login(username, password)
|
const isSuccess = await login(username, password)
|
||||||
if (isSuccess) success()
|
if (isSuccess) success()
|
||||||
else error('Wrong username or password.')
|
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
if (err instanceof TooManyRequestsError) {
|
||||||
|
error('Too many requests. Try again in 2 minutes.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (err instanceof UnauthorizedError) {
|
||||||
|
error('Wrong username or password.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (err instanceof ValidationError) {
|
||||||
|
error('Invalid username or password.')
|
||||||
|
return
|
||||||
|
}
|
||||||
console.error(err)
|
console.error(err)
|
||||||
error('Something went wrong.')
|
error('Something went wrong.')
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,25 +7,29 @@ import Background from '../components/DynamicBackground.vue'
|
|||||||
|
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
import { register } from '../utils/auth'
|
import { register } from '../utils/auth'
|
||||||
|
import {
|
||||||
|
UnauthorizedError,
|
||||||
|
TooManyRequestsError,
|
||||||
|
ValidationError,
|
||||||
|
} from '../utils/errors'
|
||||||
|
|
||||||
function handleRegister({ username, email, password, success, error }) {
|
function handleRegister({ username, email, password, success, error }) {
|
||||||
register(username, email, password)
|
register(username, email, password)
|
||||||
.then(success)
|
.then(success)
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (err.response?.status === 409) {
|
if (err instanceof TooManyRequestsError) {
|
||||||
// Handle username or email already exists error here
|
error('Too many requests. Try again in 2 minutes.')
|
||||||
if (err.response.data.type === 'username') {
|
return
|
||||||
error('Username already exists.')
|
|
||||||
} else if (err.response.data.type === 'email') {
|
|
||||||
error('Email already exists.')
|
|
||||||
} else {
|
|
||||||
error('Something went wrong.')
|
|
||||||
}
|
|
||||||
} else if (err.response?.status === 400) {
|
|
||||||
error('Invalid username or email.')
|
|
||||||
} else {
|
|
||||||
console.error(err)
|
|
||||||
}
|
}
|
||||||
|
if (err instanceof UnauthorizedError) {
|
||||||
|
error('Invalid username or password.')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (err instanceof ValidationError) {
|
||||||
|
error(err.message)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
console.error(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,11 +16,13 @@ import {
|
|||||||
registerFromSessionValidators,
|
registerFromSessionValidators,
|
||||||
registerValidators,
|
registerValidators,
|
||||||
} from '../validators/authValidator.js'
|
} from '../validators/authValidator.js'
|
||||||
|
import { authenticateRateLimit } from '../../middleware/rateLimiter.js'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/login',
|
'/login',
|
||||||
|
authenticateRateLimit,
|
||||||
loginValidators,
|
loginValidators,
|
||||||
passport.authenticate('local', { failureMessage: true }),
|
passport.authenticate('local', { failureMessage: true }),
|
||||||
(req, res) => {
|
(req, res) => {
|
||||||
@@ -28,9 +30,9 @@ router.post(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
router.post('/register', registerValidators, register)
|
router.post('/register', authenticateRateLimit, registerValidators, register)
|
||||||
|
|
||||||
router.post('/logout', logout)
|
router.post('/logout', authenticateRateLimit, logout)
|
||||||
|
|
||||||
router.get('/check-username', checkUsernameValidators, (req, res) =>
|
router.get('/check-username', checkUsernameValidators, (req, res) =>
|
||||||
res.json({ available: true })
|
res.json({ available: true })
|
||||||
@@ -43,16 +45,27 @@ router.get('/available-methods', (req, res) => {
|
|||||||
|
|
||||||
if (enabledMethods.google) {
|
if (enabledMethods.google) {
|
||||||
router.get('/google', passport.authenticate('google'))
|
router.get('/google', passport.authenticate('google'))
|
||||||
router.get('/google/callback', passport.authenticate('google'), socialLogin)
|
router.get(
|
||||||
|
'/google/callback',
|
||||||
|
authenticateRateLimit,
|
||||||
|
passport.authenticate('google'),
|
||||||
|
socialLogin
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enabledMethods.github) {
|
if (enabledMethods.github) {
|
||||||
router.get('/github', passport.authenticate('github'))
|
router.get('/github', passport.authenticate('github'))
|
||||||
router.get('/github/callback', passport.authenticate('github'), socialLogin)
|
router.get(
|
||||||
|
'/github/callback',
|
||||||
|
authenticateRateLimit,
|
||||||
|
passport.authenticate('github'),
|
||||||
|
socialLogin
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
router.post(
|
router.post(
|
||||||
'/register-from-session',
|
'/register-from-session',
|
||||||
|
authenticateRateLimit,
|
||||||
registerFromSessionValidators,
|
registerFromSessionValidators,
|
||||||
registerFromSession
|
registerFromSession
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Router } from 'express'
|
import { Router } from 'express'
|
||||||
import * as sessionController from '../controllers/sessionController.js'
|
import * as sessionController from '../controllers/sessionController.js'
|
||||||
import sessionRateLimit from '../../middleware/sessionRateLimit.js'
|
import { sessionRateLimit } from '../../middleware/rateLimiter.js'
|
||||||
|
|
||||||
const router = Router()
|
const router = Router()
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@ import apiRouter from './api/routes/index.js'
|
|||||||
import loadAndUpdateQuests from './utils/quest.js'
|
import loadAndUpdateQuests from './utils/quest.js'
|
||||||
import config from './config.js'
|
import config from './config.js'
|
||||||
import errorHandler from './middleware/error.js'
|
import errorHandler from './middleware/error.js'
|
||||||
import globalRateLimit from './middleware/globalRateLimit.js'
|
import { globalRateLimit } from './middleware/rateLimiter.js'
|
||||||
import sessionMiddleware from './middleware/session.js'
|
import sessionMiddleware from './middleware/session.js'
|
||||||
import expiryRemovalScheduler from './containers/expiryChecker.js'
|
import expiryRemovalScheduler from './containers/expiryChecker.js'
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
import { rateLimit } from 'express-rate-limit'
|
|
||||||
|
|
||||||
export default rateLimit({
|
|
||||||
windowMs: 60 * 1000, // 1 minute
|
|
||||||
max: 100,
|
|
||||||
})
|
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import rateLimit from 'express-rate-limit'
|
||||||
|
|
||||||
|
export const globalRateLimit = rateLimit({
|
||||||
|
windowMs: 60 * 1000, // 1 minute
|
||||||
|
max: 100,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const authenticateRateLimit = rateLimit({
|
||||||
|
windowMs: 2 * 60 * 1000,
|
||||||
|
max: 5,
|
||||||
|
})
|
||||||
|
|
||||||
|
export const sessionRateLimit = rateLimit({
|
||||||
|
windowMs: 10 * 1000,
|
||||||
|
max: 1,
|
||||||
|
keyGenerator: (req) => {
|
||||||
|
return req.user ? req.user._id : req.ip
|
||||||
|
},
|
||||||
|
})
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import { rateLimit } from 'express-rate-limit'
|
|
||||||
|
|
||||||
export default rateLimit({
|
|
||||||
windowMs: 10 * 1000,
|
|
||||||
max: 1,
|
|
||||||
keyGenerator: (req) => {
|
|
||||||
return req.user ? req.user._id : req.ip
|
|
||||||
},
|
|
||||||
})
|
|
||||||
Reference in new issue
Block a user