feat: optimize auth logic

This commit is contained in:
Ou 2024-10-24 20:50:30 +08:00
parent 99b06e51cb
commit 4a3c9f586a
10 changed files with 66 additions and 46 deletions

5
server/api/login.ts Normal file
View File

@ -0,0 +1,5 @@
import process from "node:process"
export default defineEventHandler(async (event) => {
return sendRedirect(event, `https://github.com/login/oauth/authorize?client_id=${process.env.G_CLIENT_ID}`)
})

View File

@ -1,3 +1,4 @@
import process from "node:process"
import { verifyPrimitiveMetadata } from "@shared/verify" import { verifyPrimitiveMetadata } from "@shared/verify"
import { UserTable } from "#/database/user" import { UserTable } from "#/database/user"
@ -7,6 +8,7 @@ export default defineEventHandler(async (event) => {
const db = useDatabase() const db = useDatabase()
if (!db) throw new Error("Not found database") if (!db) throw new Error("Not found database")
const userTable = new UserTable(db) const userTable = new UserTable(db)
if (process.env.INIT_TABLE !== "false") await userTable.init()
if (event.method === "GET") { if (event.method === "GET") {
const { data, updated } = await userTable.getData(id) const { data, updated } = await userTable.getData(id)
return { return {

View File

@ -3,7 +3,6 @@ import { SignJWT } from "jose"
import { UserTable } from "#/database/user" import { UserTable } from "#/database/user"
export default defineEventHandler(async (event) => { export default defineEventHandler(async (event) => {
if (["JWT_SECRET", "G_CLIENT_ID", "G_CLIENT_SECRET"].find(k => !process.env[k])) throw new Error("Missing environment variables")
const db = useDatabase() const db = useDatabase()
const userTable = db ? new UserTable(db) : undefined const userTable = db ? new UserTable(db) : undefined
if (!userTable) throw new Error("db is not defined") if (!userTable) throw new Error("db is not defined")

View File

@ -5,11 +5,12 @@ export default defineEventHandler(async (event) => {
const url = getRequestURL(event) const url = getRequestURL(event)
if (["JWT_SECRET", "G_CLIENT_ID", "G_CLIENT_SECRET"].find(k => !process.env[k])) { if (["JWT_SECRET", "G_CLIENT_ID", "G_CLIENT_SECRET"].find(k => !process.env[k])) {
event.context.disabledLogin = true event.context.disabledLogin = true
if (url.pathname.startsWith("/api/me")) throw createError({ statusCode: 506, message: "Server not configured" }) if (!url.pathname.startsWith("/api/s"))
throw createError({ statusCode: 506, message: "Server not configured, disable login" })
} else { } else {
if (/^\/api\/(?:me|s)\//.test(url.pathname)) { if (["/api/s", "/api/me"].find(p => url.pathname.startsWith(p))) {
const token = getHeader(event, "Authorization")?.replace("Bearer ", "")?.trim() const token = getHeader(event, "Authorization")?.replace("Bearer ", "")?.trim()
if (token && process.env.JWT_SECRET) { if (token) {
try { try {
const { payload } = await jwtVerify(token, new TextEncoder().encode(process.env.JWT_SECRET)) as { payload?: { id: string, type: string } } const { payload } = await jwtVerify(token, new TextEncoder().encode(process.env.JWT_SECRET)) as { payload?: { id: string, type: string } }
if (payload?.id) { if (payload?.id) {
@ -19,9 +20,12 @@ export default defineEventHandler(async (event) => {
} }
} }
} catch { } catch {
if (url.pathname.startsWith("/api/me")) throw createError({ statusCode: 401, message: "JWT verification failed" }) if (url.pathname.startsWith("/api/me"))
logger.warn("JWT verification failed") throw createError({ statusCode: 401, message: "JWT verification failed" })
else logger.warn("JWT verification failed")
} }
} else if (url.pathname.startsWith("/api/me")) {
throw createError({ statusCode: 401, message: "JWT verification failed" })
} }
} }
} }

View File

@ -19,7 +19,7 @@ function ThemeToggle() {
} }
export function Menu() { export function Menu() {
const { loggedIn, login, logout, enabledLogin, userInfo } = useLogin() const { loggedIn, login, logout, userInfo } = useLogin()
const [shown, show] = useState(false) const [shown, show] = useState(false)
const ref = useRef<HTMLElement>(null) const ref = useRef<HTMLElement>(null)
const isHover = useHoverDirty(ref) const isHover = useHoverDirty(ref)
@ -30,7 +30,7 @@ export function Menu() {
<span ref={ref} className="relative"> <span ref={ref} className="relative">
<span className="flex items-center scale-90"> <span className="flex items-center scale-90">
{ {
enabledLogin && loggedIn && userInfo.avatar loggedIn && userInfo.avatar
? ( ? (
<button <button
type="button" type="button"
@ -62,18 +62,19 @@ export function Menu() {
}} }}
> >
<ol className="bg-base bg-op-70! backdrop-blur-md p-2 rounded-lg color-base text-base"> <ol className="bg-base bg-op-70! backdrop-blur-md p-2 rounded-lg color-base text-base">
{enabledLogin && !loggedIn && ( {loggedIn
<li onClick={login}> ? (
<span className="i-ph:sign-in-duotone inline-block" /> <li onClick={logout}>
<span>Github </span> <span className="i-ph:sign-out-duotone inline-block" />
</li> <span>退</span>
)} </li>
{enabledLogin && loggedIn && ( )
<li onClick={logout}> : (
<span className="i-ph:sign-out-duotone inline-block" /> <li onClick={login}>
<span>退</span> <span className="i-ph:sign-in-duotone inline-block" />
</li> <span>Github </span>
)} </li>
)}
<ThemeToggle /> <ThemeToggle />
<li onClick={() => window.open(Homepage)}> <li onClick={() => window.open(Homepage)}>
<span className="i-ph:github-logo-duotone inline-block" /> <span className="i-ph:github-logo-duotone inline-block" />

View File

@ -12,13 +12,11 @@ const jwtAtom = atomWithStorage("jwt", "")
export function useLogin() { export function useLogin() {
const [userInfo] = useAtom(userAtom) const [userInfo] = useAtom(userAtom)
const [jwt, setJwt] = useAtom(jwtAtom) const [jwt, setJwt] = useAtom(jwtAtom)
const enabledLogin = __ENABLE_LOGIN__
const login = useCallback(() => { const login = useCallback(() => {
window.location.href = `https://github.com/login/oauth/authorize?client_id=${__G_CLIENT_ID__}` window.location.href = __LOGIN_URL__
}, []) }, [])
return { return {
enabledLogin,
loggedIn: !!jwt, loggedIn: !!jwt,
userInfo, userInfo,
logout: () => setJwt(""), logout: () => setJwt(""),

View File

@ -8,27 +8,21 @@ import { preprocessMetadata, primitiveMetadataAtom } from "~/atoms"
import { safeParseString } from "~/utils" import { safeParseString } from "~/utils"
export async function uploadMetadata(metadata: PrimitiveMetadata) { export async function uploadMetadata(metadata: PrimitiveMetadata) {
if (!__ENABLE_LOGIN__) return
const jwt = safeParseString(localStorage.getItem("jwt")) const jwt = safeParseString(localStorage.getItem("jwt"))
if (!jwt) return if (!jwt) return
try { await ofetch("/api/me/sync", {
await ofetch("/api/me/sync", { method: "POST",
method: "POST", headers: {
headers: { Authorization: `Bearer ${jwt}`,
Authorization: `Bearer ${jwt}`, },
}, body: {
body: { data: metadata.data,
data: metadata.data, updatedTime: metadata.updatedTime,
updatedTime: metadata.updatedTime, },
}, })
})
} catch (e) {
console.error(e)
}
} }
export async function downloadMetadata(): Promise<PrimitiveMetadata | undefined> { export async function downloadMetadata(): Promise<PrimitiveMetadata | undefined> {
if (!__ENABLE_LOGIN__) return
const jwt = safeParseString(localStorage.getItem("jwt")) const jwt = safeParseString(localStorage.getItem("jwt"))
if (!jwt) return if (!jwt) return
const { data, updatedTime } = await ofetch("/api/me/sync", { const { data, updatedTime } = await ofetch("/api/me/sync", {
@ -52,8 +46,25 @@ export function useSync() {
const toaster = useToast() const toaster = useToast()
useDebounce(async () => { useDebounce(async () => {
const fn = async () => {
try {
await uploadMetadata(primitiveMetadata)
} catch (e: any) {
if (e.statusCode !== 506) {
toaster("身份校验失败,无法同步,请重新登录", {
type: "error",
action: {
label: "登录",
onClick: login,
},
})
logout()
}
}
}
if (primitiveMetadata.action === "manual") { if (primitiveMetadata.action === "manual") {
uploadMetadata(primitiveMetadata) fn()
} }
}, 10000, [primitiveMetadata]) }, 10000, [primitiveMetadata])
useMount(() => { useMount(() => {
@ -64,8 +75,8 @@ export function useSync() {
setPrimitiveMetadata(preprocessMetadata(metadata)) setPrimitiveMetadata(preprocessMetadata(metadata))
} }
} catch (e: any) { } catch (e: any) {
if (e.statusCode === 401) { if (e.statusCode !== 506) {
toaster("身份校验失败,请重新登录", { toaster("身份校验失败,无法同步,请重新登录", {
type: "error", type: "error",
action: { action: {
label: "登录", label: "登录",

View File

@ -17,11 +17,13 @@ import { Route as CColumnImport } from './routes/c.$column'
// Create/Update Routes // Create/Update Routes
const IndexRoute = IndexImport.update({ const IndexRoute = IndexImport.update({
id: '/',
path: '/', path: '/',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)
const CColumnRoute = CColumnImport.update({ const CColumnRoute = CColumnImport.update({
id: '/c/$column',
path: '/c/$column', path: '/c/$column',
getParentRoute: () => rootRoute, getParentRoute: () => rootRoute,
} as any) } as any)

3
src/vite-env.d.ts vendored
View File

@ -2,5 +2,4 @@
/// <reference types="vite-plugin-pwa/react" /> /// <reference types="vite-plugin-pwa/react" />
/// <reference types="vite-plugin-pwa/info" /> /// <reference types="vite-plugin-pwa/info" />
/// <reference lib="webworker" /> /// <reference lib="webworker" />
declare const __G_CLIENT_ID__: string declare const __LOGIN_URL__: string
declare const __ENABLE_LOGIN__: boolean

View File

@ -97,8 +97,7 @@ if (process.env.VERCEL) {
export default defineConfig({ export default defineConfig({
define: { define: {
__G_CLIENT_ID__: `"${process.env.G_CLIENT_ID}"`, __LOGIN_URL__: process.env.G_CLIENT_ID ? `"https://github.com/login/oauth/authorize?client_id=${process.env.G_CLIENT_ID}"` : `"/api/login"`,
__ENABLE_LOGIN__: ["JWT_SECRET", "G_CLIENT_ID", "G_CLIENT_SECRET"].every(k => process.env[k]),
}, },
plugins: [ plugins: [
tsconfigPath(), tsconfigPath(),