Skip to content

Commit

Permalink
Merge pull request #19 from AstraSurge/feat/add-entry-to-admin-page
Browse files Browse the repository at this point in the history
feat: improve user authentication and access control
  • Loading branch information
suikodev authored Mar 28, 2023
2 parents 72fe2b5 + 877b3d6 commit dad0b77
Show file tree
Hide file tree
Showing 28 changed files with 352 additions and 280 deletions.
116 changes: 24 additions & 92 deletions service/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import './utils/loadEnv'
import express from 'express'
import history from 'connect-history-api-fallback'
import type { ChatContext, ChatMessage } from './chatgpt'
import { chatReplyProcess, updateApiKey } from './chatgpt'
import { isAuthenticated, isRoot } from './middleware/auth'
import admin, { adminConfigRef } from './firebaseAdmin'
import addToBlacklist from './utils/addToBlacklist'
import { chatReplyProcess } from './chatgpt'
import { checkAuth, isAdmin, isAuthenticated } from './middleware/auth'
import adminRouter from './routers/adminRouter'
import admin from './firebaseAdmin'

const app = express()
const router = express.Router()
Expand All @@ -23,7 +23,7 @@ app.all('*', (_, res, next) => {
next()
})

router.post('/chat-process', isAuthenticated, async (req, res) => {
router.post('/chat-process', checkAuth, async (req, res) => {
res.setHeader('Content-type', 'application/octet-stream')

try {
Expand All @@ -43,100 +43,30 @@ router.post('/chat-process', isAuthenticated, async (req, res) => {
}
})

router.get('/system-settings', async (req, res) => {
router.post('/verify', async (req, res) => {
try {
const configSnapshot = await adminConfigRef.get()
const configData = configSnapshot.data()
// remove openaiApiKeys from response for security reason
const { openaiApiKeys: _, ...rest } = configData ?? {}
res.status(200).json({
status: 'Success',
data: rest,
} ?? {})
}
catch (error) {
res.status(500).send(error.message)
}
})

router.put('/system-settings', async (req, res) => {
try {
const configData = req.body

if (configData?.openaiApiKeys?.length > 0) {
const apiKey = configData.openaiApiKeys[0]
await updateApiKey(apiKey)
const Authorization = (req.header('Authorization') || '').replace('Bearer ', '').trim()
const decodedToken = await admin.auth().verifyIdToken(Authorization)

if (!isAuthenticated(decodedToken)) {
res.status(401).send({
status: 'Unauthorized',
message: 'Auth Error',
data: null,
})
return
}

await adminConfigRef.set(configData, { merge: true })
res.status(200).send({
message: 'Config updated successfully',
status: 'Success',
})
}
catch (error) {
res.status(500).send(error.message)
}
})

router.get('/users', isAuthenticated, isRoot, async (req, res) => {
try {
const userList = []
let nextPageToken

do {
const result = await admin.auth().listUsers(1000, nextPageToken)
result.users.forEach(user => userList.push(user.toJSON()))
nextPageToken = result.pageToken
} while (nextPageToken)

res.status(200).json({
data: userList,
status: 'Success',
})
}
catch (error) {
res.status(500).send(error.message)
}
})

router.put('/users/:uid/disable', async (req, res) => {
try {
const uid = req.params.uid
await admin.auth().updateUser(uid, { disabled: true })
res.status(200).send({
message: 'User disabled successfully',
status: 'Success',
})
}
catch (error) {
res.status(500).send(error.message)
}
})
let role = 'user'

router.put('/users/:uid/enable', async (req, res) => {
try {
const uid = req.params.uid
await admin.auth().updateUser(uid, { disabled: false })
res.status(200).send({
message: 'User enabled successfully',
status: 'Success',
})
}
catch (error) {
res.status(500).send(error.message)
}
})
if (await isAdmin(decodedToken))
role = 'admin'

app.delete('/users/:uid', async (req, res) => {
try {
const uid = req.params.uid
const userInfo = await admin.auth().getUser(uid)
await addToBlacklist([userInfo.email, userInfo.phoneNumber])
await admin.auth().deleteUser(uid)
res.status(200).send({
message: 'User deleted successfully',
status: 'Success',
data: {
role,
},
})
}
catch (error) {
Expand All @@ -145,6 +75,8 @@ app.delete('/users/:uid', async (req, res) => {
})

app.use('', router)
app.use('', adminRouter)
app.use('/api', router)
app.use('/api', adminRouter)

app.listen(3002, () => globalThis.console.log('Server is running on port 3002'))
64 changes: 39 additions & 25 deletions service/src/middleware/auth.ts
Original file line number Diff line number Diff line change
@@ -1,51 +1,65 @@
import type { Response } from 'express'
import type { DecodedIdToken } from 'firebase-admin/lib/auth/token-verifier'
import admin, { adminConfigRef } from '~/firebaseAdmin'
import { generateRegExp } from '~/utils/generateRegExp'

export async function verifyToken(credential: string) {
const decodedToken = await admin.auth().verifyIdToken(credential)
return decodedToken
}

const isAuthenticated = async (req, res, next) => {
try {
const Authorization = (req.header('Authorization') || '').replace('Bearer ', '').trim()
const decodedToken = await verifyToken(Authorization)
export async function isAdmin(decodedToken: DecodedIdToken) {
const rootAccount = process.env.ROOT_ACCOUNT.trim()
return (decodedToken.email_verified && decodedToken.email === rootAccount) || decodedToken.phone_number === rootAccount
}

// if user is root, skip the rest of the auth process
const rootAccount = process.env.ROOT_ACCOUNT.trim()
if ((decodedToken.email_verified && decodedToken.email === rootAccount) || decodedToken.phone_number === rootAccount) {
res.locals.isRoot = true
return next()
}
export async function isAuthenticated(decodedToken: DecodedIdToken) {
const adminConfig = await adminConfigRef.get()

const whitelist = adminConfig?.data()?.whitelist ?? []
const blacklist = adminConfig?.data()?.blacklist ?? []

const blacklistRegex = blacklist.map((item: string) => generateRegExp(item))

const adminConfig = await adminConfigRef.get()
if (blacklistRegex.some((item: RegExp) => item.test(decodedToken.email) || item.test(decodedToken.phone_number)))
throw new Error('Auth Error')

const whitelist = adminConfig?.data()?.whitelist ?? []
const blacklist = adminConfig?.data()?.blacklist ?? []
const whitelistRegex = whitelist.map((item: string) => generateRegExp(item))

const blacklistRegex = blacklist.map((item: string) => new RegExp(item))
if (!whitelistRegex.some((item: RegExp) => item.test(decodedToken.email) || item.test(decodedToken.phone_number)))
throw new Error('Auth Error')

if (blacklistRegex.some((item: RegExp) => item.test(decodedToken.email) || item.test(decodedToken.phone_number)))
throw new Error('Auth Error')
return true
}

const checkAuth = async (req, res, next) => {
try {
const Authorization = (req.header('Authorization') || '').replace('Bearer ', '').trim()
const decodedToken = await verifyToken(Authorization)

const whitelistRegex = whitelist.map((item: string) => new RegExp(item))
// if user is Admin, skip the rest of the auth process
if (await isAdmin(decodedToken)) {
res.locals.isAdmin = true
return next()
}

if (!whitelistRegex.some((item: RegExp) => item.test(decodedToken.email) || item.test(decodedToken.phone_number)))
throw new Error('Auth Error')
if (await isAuthenticated(decodedToken))
return next()

next()
throw new Error('Auth Error')
}
catch (error) {
console.error(error)
res.send({ status: 'Unauthorized', message: 'Auth Error', data: null })
res.status(401).send({ status: 'Unauthorized', message: 'Auth Error', data: null })
}
}

const isRoot = async (req, res, next) => {
if (!res.locals.isRoot) {
res.send({ status: 'Unauthorized', message: 'Auth Error', data: null })
const checkAdmin = async (req, res: Response, next) => {
if (!res.locals.isAdmin) {
res.status(403).send({ status: 'Unauthorized', message: 'Auth Error', data: null })
return
}
next()
}

export { isAuthenticated, isRoot }
export { checkAuth, checkAdmin }
112 changes: 112 additions & 0 deletions service/src/routers/adminRouter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import express from 'express'
import { updateApiKey } from '~/chatgpt'
import admin, { adminConfigRef } from '~/firebaseAdmin'
import { checkAdmin, checkAuth } from '~/middleware/auth'
import addToBlacklist from '~/utils/addToBlacklist'

const adminRouter = express.Router()

adminRouter.use(checkAuth, checkAdmin)

adminRouter.get('/system-settings', async (req, res) => {
try {
const configSnapshot = await adminConfigRef.get()
const configData = configSnapshot.data()
// remove openaiApiKeys from response for security reason
const { openaiApiKeys: _, ...rest } = configData ?? {}
res.status(200).json({
status: 'Success',
data: rest,
} ?? {})
}
catch (error) {
res.status(500).send(error.message)
}
})

adminRouter.put('/system-settings', async (req, res) => {
try {
const configData = req.body

if (configData?.openaiApiKeys?.length > 0) {
const apiKey = configData.openaiApiKeys[0]
await updateApiKey(apiKey)
}

await adminConfigRef.set(configData, { merge: true })
res.status(200).send({
message: 'Config updated successfully',
status: 'Success',
})
}
catch (error) {
res.status(500).send(error.message)
}
})

adminRouter.get('/users', async (req, res) => {
try {
const userList = []
let nextPageToken

do {
const result = await admin.auth().listUsers(1000, nextPageToken)
result.users.forEach(user => userList.push(user.toJSON()))
nextPageToken = result.pageToken
} while (nextPageToken)

res.status(200).json({
data: userList,
status: 'Success',
})
}
catch (error) {
res.status(500).send(error.message)
}
})

adminRouter.put('/users/:uid/disable', async (req, res) => {
try {
const uid = req.params.uid
await admin.auth().updateUser(uid, { disabled: true })
res.status(200).send({
message: 'User disabled successfully',
status: 'Success',
})
}
catch (error) {
res.status(500).send(error.message)
}
})

adminRouter.put('/users/:uid/enable', async (req, res) => {
try {
const uid = req.params.uid
await admin.auth().updateUser(uid, { disabled: false })
res.status(200).send({
message: 'User enabled successfully',
status: 'Success',
})
}
catch (error) {
res.status(500).send(error.message)
}
})

adminRouter.delete('/users/:uid', async (req, res) => {
try {
const uid = req.params.uid
const userInfo = await admin.auth().getUser(uid)
await addToBlacklist([userInfo.email, userInfo.phoneNumber])
await admin.auth().deleteUser(uid)
res.status(200).send({
message: 'User deleted successfully',
status: 'Success',
})
}
catch (error) {
res.status(500).send(error.message)
}
})

export default adminRouter
10 changes: 10 additions & 0 deletions service/src/utils/generateRegExp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export function generateRegExp(str: string) {
let regex: RegExp
try {
regex = new RegExp(str) // 尝试将字符串转换为正则表达式
}
catch (e) {
regex = new RegExp(`.*${str}.*`) // 生成一个包含该字符串的正则表达式
}
return regex
}
11 changes: 11 additions & 0 deletions src/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import type { AxiosProgressEvent, GenericAbortSignal } from 'axios'
import { deleteFn, get, post, put } from '@/utils/request'

export function verifyIdToken(token: string) {
return post<{
role: 'admin' | 'user'
}>({
url: '/verify',
headers: {
Authorization: `Bearer ${token}`,
},
})
}

export function fetchChatAPI<T = any>(
prompt: string,
options?: { conversationId?: string; parentMessageId?: string },
Expand Down
Loading

0 comments on commit dad0b77

Please sign in to comment.