diff --git a/package.json b/package.json index 4cf83e1..dd28be2 100644 --- a/package.json +++ b/package.json @@ -15,11 +15,12 @@ "build": "vite build", "lint": "eslint", "favicon": "tsx ./scripts/favicon.ts", - "start": "PORT=4444 node --env-file .env.vars dist/output/server/index.mjs", + "start": "PORT=4444 node --env-file .env.server dist/output/server/index.mjs", "preview": "CF_PAGES=1 pnpm run build && wrangler pages dev", "deploy": "CF_PAGES=1 pnpm run build && wrangler pages deploy", "release": "bumpp", "prepare": "simple-git-hooks", + "log": "wrangler pages deployment tail --project-name newsnow", "test": "vitest -c vitest.config.ts" }, "dependencies": { @@ -34,6 +35,7 @@ "cheerio": "^1.0.0", "clsx": "^2.1.1", "consola": "^3.2.3", + "cookie-es": "^1.2.2", "dayjs": "1.11.13", "db0": "npm:@ourongxing/db0@0.1.6", "defu": "^6.1.4", @@ -96,7 +98,8 @@ }, "resolutions": { "dayjs": "1.11.13", - "db0": "npm:@ourongxing/db0@0.1.6" + "db0": "npm:@ourongxing/db0@0.1.6", + "nitropack": "^2.9.7" }, "simple-git-hooks": { "pre-commit": "npx lint-staged" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 56d7165..aa5aca7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,6 +7,7 @@ settings: overrides: dayjs: 1.11.13 db0: npm:@ourongxing/db0@0.1.6 + nitropack: ^2.9.7 patchedDependencies: dayjs: @@ -50,6 +51,9 @@ importers: consola: specifier: ^3.2.3 version: 3.2.3 + cookie-es: + specifier: ^1.2.2 + version: 1.2.2 dayjs: specifier: 1.11.13 version: 1.11.13(patch_hash=vxjypqxmsykboavgqknf3tdbfa) @@ -2514,8 +2518,8 @@ packages: resolution: {integrity: sha512-piICUB6ei4IlTv1+653yq5+KoqfBYmj9bw6LqXoOneTMDXk5nM1qt12mFW1caG3LlJXEKW1Bp0WggEmIfQB34g==} engines: {node: '>= 14'} - croner@8.1.1: - resolution: {integrity: sha512-1VdUuRnQP4drdFkS8NKvDR1NBgevm8TOuflcaZEKsxw42CxonjW/2vkj1AKlinJb4ZLwBcuWF9GiPr7FQc6AQA==} + croner@8.1.2: + resolution: {integrity: sha512-ypfPFcAXHuAZRCzo3vJL6ltENzniTjwe/qsLleH1V2/7SRDjgvRQyrLmumFTLmjFax4IuSxfGXEn79fozXcJog==} engines: {node: '>=18.0'} cross-spawn@7.0.3: @@ -3997,8 +4001,8 @@ packages: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} - package-json-from-dist@1.0.0: - resolution: {integrity: sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} package-manager-detector@0.2.0: resolution: {integrity: sha512-E385OSk9qDcXhcM9LNSe4sdhx8a9mAPrZ4sMLW+tmxl5ZuGtPUcdFu+MPP2jbgiWAZ6Pfe5soGFMd+0Db5Vrog==} @@ -7300,7 +7304,7 @@ snapshots: crc-32: 1.2.2 readable-stream: 4.5.2 - croner@8.1.1: {} + croner@8.1.2: {} cross-spawn@7.0.3: dependencies: @@ -8222,7 +8226,7 @@ snapshots: jackspeak: 3.4.3 minimatch: 9.0.5 minipass: 7.1.2 - package-json-from-dist: 1.0.0 + package-json-from-dist: 1.0.1 path-scurry: 1.11.1 glob@7.2.3: @@ -8893,7 +8897,7 @@ snapshots: citty: 0.1.6 consola: 3.2.3 cookie-es: 1.2.2 - croner: 8.1.1 + croner: 8.1.2 crossws: 0.2.4 db0: '@ourongxing/db0@0.1.6(@libsql/client@0.14.0)(libsql@0.4.6)' defu: 6.1.4 @@ -9100,7 +9104,7 @@ snapshots: p-try@2.2.0: {} - package-json-from-dist@1.0.0: {} + package-json-from-dist@1.0.1: {} package-manager-detector@0.2.0: {} diff --git a/server/database/user.ts b/server/database/user.ts index 9a73c5a..b5eacb9 100644 --- a/server/database/user.ts +++ b/server/database/user.ts @@ -1,4 +1,4 @@ -import type { Database } from "@ourongxing/db0" +import type { Database } from "db0" import type { UserInfo } from "#/types" export class UserTable { @@ -31,6 +31,8 @@ export class UserTable { } else if (u.email !== email && u.type !== type) { await this.db.prepare(`REPLACE INTO user (id, email, updated) VALUES (?, ?, ?)`).run(id, email, now) logger.success(`update user ${id} email`) + } else { + logger.info(`user ${id} already exists`) } } diff --git a/server/middleware/auth.ts b/server/middleware/auth.ts index 2e51e2c..7c0a2be 100644 --- a/server/middleware/auth.ts +++ b/server/middleware/auth.ts @@ -2,15 +2,19 @@ import process from "node:process" import { jwtVerify } from "jose" export default defineEventHandler(async (event) => { - const token = getCookie(event, "jwt") - if (token && process.env.JWT_SECRET) { - try { - const { payload } = await jwtVerify(token, new TextEncoder().encode(process.env.JWT_SECRET)) as { payload?: { id: string, type: string } } - if (payload?.id) { - event.context.user = payload.id + if (["JWT_SECRET", "G_CLIENT_ID", "G_CLIENT_SECRET"].find(k => !process.env[k])) { + event.context.user = true + } else { + const token = getHeader(event, "Authorization") + if (token && process.env.JWT_SECRET) { + try { + const { payload } = await jwtVerify(token.replace("Bearer ", ""), new TextEncoder().encode(process.env.JWT_SECRET)) as { payload?: { id: string, type: string } } + if (payload?.id) { + event.context.user = payload.id + } + } catch { + logger.error("JWT verification failed") } - } catch { - logger.error("JWT verification failed") } } }) diff --git a/server/routes/me.ts b/server/routes/me.ts index bbebce2..073260f 100644 --- a/server/routes/me.ts +++ b/server/routes/me.ts @@ -1,3 +1,2 @@ -export default defineEventHandler(async (event) => { - return event +export default defineEventHandler(() => { }) diff --git a/server/routes/oauth-callback/github.ts b/server/routes/oauth-callback/github.ts deleted file mode 100644 index 896c58e..0000000 --- a/server/routes/oauth-callback/github.ts +++ /dev/null @@ -1,75 +0,0 @@ -import process from "node:process" -import { SignJWT } from "jose" -import { UserTable } from "#/database/user" - -export default defineEventHandler(async (event) => { - ["JWT_SECRET", "G_CLIENT_ID", "G_CLIENT_SECRET"].forEach((k) => { - if (!process.env[k]) throw new Error(`${k} is not defined`) - }) - - const db = useDatabase() - const userTable = db ? new UserTable(db) : undefined - if (!userTable) throw new Error("db is not defined") - await userTable.init() - const body = { - client_id: process.env.G_CLIENT_ID, - client_secret: process.env.G_CLIENT_SECRET, - code: getQuery(event).code, - } - /** - * 获取access_token - * 接下来的操作都需要使用access_token - */ - const response: { - access_token: string - token_type: string - scope: string - } = await $fetch( - `https://github.com/login/oauth/access_token`, - { - method: "POST", - body, - headers: { accept: "application/json" }, - }, - ) - const token = response.access_token - - console.log(token) - const userInfo: { - id: number - name: string - avatar_url: string - } = await $fetch(`https://api.github.com/user`, { - headers: { - Authorization: `token ${token}`, - }, - }) - - const emailinfo: { - email: string - primary: boolean - }[] = await $fetch("https://api.github.com/user/emails", { - headers: { - Accept: "application/vnd.github+json", - Authorization: `token ${token}`, - }, - }) - - const userID = String(userInfo.id) - await userTable.addUser(userID, emailinfo.find(item => item.primary)?.email || "", "github") - - const jwtToken = await new SignJWT({ - id: userID, - type: "github", - }) - .setExpirationTime("65d") - .setProtectedHeader({ alg: "HS256" }) - .sign(new TextEncoder().encode(process.env.JWT_SECRET!)) - - // seconds - const maxAge = 60 * 24 * 60 * 60 - setCookie(event, "jwt", jwtToken, { maxAge }) - setCookie(event, "avatar", userInfo.avatar_url, { maxAge }) - setCookie(event, "name", userInfo.name, { maxAge }) - return sendRedirect(event, `/?login=github`) -}) diff --git a/server/routes/oauth/github.ts b/server/routes/oauth/github.ts new file mode 100644 index 0000000..46fd9b0 --- /dev/null +++ b/server/routes/oauth/github.ts @@ -0,0 +1,73 @@ +import process from "node:process" +import { SignJWT } from "jose" +import { UserTable } from "#/database/user" + +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 userTable = db ? new UserTable(db) : undefined + if (!userTable) throw new Error("db is not defined") + if (process.env.INIT_TABLE !== "false") await userTable.init() + + const response: { + access_token: string + token_type: string + scope: string + } = await $fetch( + `https://github.com/login/oauth/access_token`, + { + method: "POST", + body: { + client_id: process.env.G_CLIENT_ID, + client_secret: process.env.G_CLIENT_SECRET, + code: getQuery(event).code, + }, + headers: { + accept: "application/json", + }, + }, + ) + + const userInfo: { + id: number + name: string + avatar_url: string + email: string + notification_email: string + } = await $fetch(`https://api.github.com/user`, { + headers: { + "Accept": "application/vnd.github+json", + "Authorization": `token ${response.access_token}`, + // 必须有 user-agent,在 cloudflare worker 会报错 + "User-Agent": "NewsNow App", + }, + }) + + const userID = String(userInfo.id) + await userTable.addUser(userID, userInfo.notification_email || userInfo.email, "github") + + const jwtToken = await new SignJWT({ + id: userID, + type: "github", + }) + .setExpirationTime("60d") + .setProtectedHeader({ alg: "HS256" }) + .sign(new TextEncoder().encode(process.env.JWT_SECRET!)) + + // nitro 有 bug,在 cloudflare 里没法 set cookie + // seconds + // const maxAge = 60 * 24 * 60 * 60 + // setCookie(event, "user_jwt", jwtToken, { maxAge }) + // setCookie(event, "user_avatar", userInfo.avatar_url, { maxAge }) + // setCookie(event, "user_name", userInfo.name, { maxAge }) + + const params = new URLSearchParams({ + login: "github", + user_jwt: jwtToken, + user_info: JSON.stringify({ + avatar: userInfo.avatar_url, + name: userInfo.name, + }), + }) + return sendRedirect(event, `/?${params.toString()}`) +}) diff --git a/server/routes/s/[id].ts b/server/routes/s/[id].ts index 6a6a842..010b18c 100644 --- a/server/routes/s/[id].ts +++ b/server/routes/s/[id].ts @@ -1,3 +1,4 @@ +import process from "node:process" import { TTL } from "@shared/consts" import type { SourceID, SourceResponse } from "@shared/types" import { sources } from "@shared/sources" @@ -21,7 +22,7 @@ export default defineEventHandler(async (event): Promise => { const cacheTable = db ? new Cache(db) : undefined const now = Date.now() if (cacheTable) { - await cacheTable.init() + if (process.env.INIT_TABLE !== "false") await cacheTable.init() const cache = await cacheTable.get(id) if (cache) { // interval 刷新间隔,对于缓存失效也要执行的。本质上表示本来内容更新就很慢,这个间隔内可能内容压根不会更新。 diff --git a/server/sources/36kr.ts b/server/sources/36kr.ts index 878dc17..c72091e 100644 --- a/server/sources/36kr.ts +++ b/server/sources/36kr.ts @@ -1,10 +1,9 @@ import type { NewsItem } from "@shared/types" import { load } from "cheerio" -import { $fetch } from "ofetch" export default defineSource(async () => { const url = "https://www.36kr.com/newsflashes" - const response = await $fetch(url) + const response = await $fetch(url) as any const $ = load(response) const news: NewsItem[] = [] const $items = $(".newsflash-item") diff --git a/src/components/column/card.tsx b/src/components/column/card.tsx index 53f712a..f1ac085 100644 --- a/src/components/column/card.tsx +++ b/src/components/column/card.tsx @@ -105,7 +105,12 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) { if (Date.now() - _refetchTime < 1000) { url = `/api/s/${_id}?latest` } - const response: SourceResponse = await ofetch(url, { timeout: 5000 }) + const response: SourceResponse = await ofetch(url, { + timeout: 10000, + headers: { + Authorization: `Bearer ${localStorage.getItem("user_jwt")}`, + }, + }) if (response.status === "error") { throw new Error(response.message) } else { diff --git a/src/components/column/index.tsx b/src/components/column/index.tsx index e2162ff..a7f4543 100644 --- a/src/components/column/index.tsx +++ b/src/components/column/index.tsx @@ -4,6 +4,7 @@ import { Link } from "@tanstack/react-router" import clsx from "clsx" import { useAtom } from "jotai" import { useEffect } from "react" +import { useTitle } from "react-use" import { Dnd } from "./dnd" import { currentColumnIDAtom } from "~/atoms" @@ -12,7 +13,7 @@ export function Column({ id }: { id: ColumnID }) { useEffect(() => { setCurrentColumnID(id) }, [id, setCurrentColumnID]) - + useTitle(`NewsNow ${metadata[id].name}`) return ( <>
diff --git a/src/components/header.tsx b/src/components/header.tsx index 35cc5ec..e2828cc 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -5,7 +5,7 @@ import { useIsFetching } from "@tanstack/react-query" import clsx from "clsx" import type { SourceID } from "@shared/types" import { Homepage, Version } from "@shared/consts" -import { useCookie } from "react-use" +import { useLocalStorage } from "react-use" import { useDark } from "~/hooks/useDark" import { currentColumnAtom, goToTopAtom, refetchSourcesAtom } from "~/atoms" @@ -22,24 +22,26 @@ function ThemeToggle() { } function LoginIn() { - const [name] = useCookie("name") - const [avatar] = useCookie("avatar") - const [jwt, setJwt] = useCookie("jwt") + // useLocalStorage 默认会自动序列化 + const [info] = useLocalStorage<{ name: string, avatar: string }>("user_info") + const [jwt, _setJwt] = useLocalStorage("user_jwt", undefined, { + raw: true, + }) if (jwt) { return (