-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #19 from AstraSurge/feat/add-entry-to-admin-page
feat: improve user authentication and access control
- Loading branch information
Showing
28 changed files
with
352 additions
and
280 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.