mirror of
https://github.com/ourongxing/newsnow.git
synced 2025-01-31 10:58:04 +08:00
fix: github oauth on cloudflare page
This commit is contained in:
parent
8d8bc41691
commit
4607758dd0
@ -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"
|
||||
|
20
pnpm-lock.yaml
generated
20
pnpm-lock.yaml
generated
@ -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: {}
|
||||
|
||||
|
@ -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`)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,10 +2,13 @@ import process from "node:process"
|
||||
import { jwtVerify } from "jose"
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const token = getCookie(event, "jwt")
|
||||
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, new TextEncoder().encode(process.env.JWT_SECRET)) as { payload?: { id: string, type: string } }
|
||||
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
|
||||
}
|
||||
@ -13,4 +16,5 @@ export default defineEventHandler(async (event) => {
|
||||
logger.error("JWT verification failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
@ -1,3 +1,2 @@
|
||||
export default defineEventHandler(async (event) => {
|
||||
return event
|
||||
export default defineEventHandler(() => {
|
||||
})
|
||||
|
@ -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`)
|
||||
})
|
73
server/routes/oauth/github.ts
Normal file
73
server/routes/oauth/github.ts
Normal file
@ -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()}`)
|
||||
})
|
@ -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<SourceResponse> => {
|
||||
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 刷新间隔,对于缓存失效也要执行的。本质上表示本来内容更新就很慢,这个间隔内可能内容压根不会更新。
|
||||
|
@ -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")
|
||||
|
@ -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 {
|
||||
|
@ -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 (
|
||||
<>
|
||||
<div className="w-full flex justify-center">
|
||||
|
@ -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<string>("user_jwt", undefined, {
|
||||
raw: true,
|
||||
})
|
||||
if (jwt) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className="btn-pure"
|
||||
title={name ?? ""}
|
||||
title={info?.name ?? ""}
|
||||
onClick={() => {
|
||||
setJwt("")
|
||||
// setJwt("")
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="h-5 w-5 rounded-full bg-cover border"
|
||||
style={
|
||||
{
|
||||
backgroundImage: `url(${avatar})`,
|
||||
backgroundImage: `url(${info?.avatar})`,
|
||||
}
|
||||
}
|
||||
/>
|
||||
@ -48,11 +50,9 @@ function LoginIn() {
|
||||
}
|
||||
return (
|
||||
<a
|
||||
type="button"
|
||||
title="Login in with GitHub"
|
||||
className="i-ph:sign-in-duotone btn-pure"
|
||||
// @ts-expect-error >_<
|
||||
href={`https://github.com/login/oauth/authorize?client_id=${__G_CLIENT_ID__}&scope=read:user,user:email`}
|
||||
href={`https://github.com/login/oauth/authorize?client_id=${__G_CLIENT_ID__}`}
|
||||
/>
|
||||
)
|
||||
}
|
||||
@ -124,7 +124,7 @@ export function Header() {
|
||||
<RefreshButton />
|
||||
<ThemeToggle />
|
||||
<GithubIcon />
|
||||
<LoginIn />
|
||||
{ __ENABLE_LOGIN__ && <LoginIn />}
|
||||
</span>
|
||||
</>
|
||||
)
|
||||
|
@ -15,6 +15,13 @@ export const Route = createRootRouteWithContext<{
|
||||
}>()({
|
||||
component: RootComponent,
|
||||
notFoundComponent: NotFoundComponent,
|
||||
beforeLoad: () => {
|
||||
const query = new URLSearchParams(window.location.search)
|
||||
if (query.has("login")) {
|
||||
[...query.entries()].forEach(key => localStorage.setItem(key[0], key[1]))
|
||||
window.history.replaceState({}, document.title, window.location.pathname)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function NotFoundComponent() {
|
||||
|
@ -3,14 +3,10 @@ import { useAtomValue } from "jotai"
|
||||
import { useMemo } from "react"
|
||||
import { localSourcesAtom } from "~/atoms"
|
||||
import { Column } from "~/components/column"
|
||||
import { } from "cookie-es"
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: IndexComponent,
|
||||
loader: async () => {
|
||||
if (window.location.search.includes("login")) {
|
||||
window.history.replaceState(null, "", "/")
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
function IndexComponent() {
|
||||
|
2
src/vite-env.d.ts
vendored
2
src/vite-env.d.ts
vendored
@ -1 +1,3 @@
|
||||
/// <reference types="vite/client" />
|
||||
declare const __G_CLIENT_ID__: string
|
||||
declare const __ENABLE_LOGIN__: boolean
|
||||
|
@ -12,12 +12,13 @@ import { projectDir } from "./shared/dir"
|
||||
const isCF = process.env.CF_PAGES
|
||||
|
||||
dotenv.config({
|
||||
path: join(projectDir, ".env.vars"),
|
||||
path: join(projectDir, ".env.server"),
|
||||
})
|
||||
|
||||
export default defineConfig({
|
||||
define: {
|
||||
__G_CLIENT_ID__: `"${process.env.G_CLIENT_ID}"`,
|
||||
__ENABLE_LOGIN__: ["JWT_SECRET", "G_CLIENT_ID", "G_CLIENT_SECRET"].every(k => process.env[k]),
|
||||
},
|
||||
plugins: [
|
||||
TanStackRouterVite({
|
||||
|
Loading…
x
Reference in New Issue
Block a user