From 272830f6ae7d64c700a1123108fbf4820d3f6f54 Mon Sep 17 00:00:00 2001 From: Ou Date: Mon, 14 Oct 2024 00:02:58 +0800 Subject: [PATCH] feat: support github oauth --- .gitignore | 4 +- package.json | 9 ++- pnpm-lock.yaml | 103 +++++++++++++++++++++++++ server/{ => database}/cache.ts | 3 +- server/database/user.ts | 65 ++++++++++++++++ server/middleware/auth.ts | 16 ++++ server/routes/me.ts | 3 + server/routes/oauth-callback/github.ts | 69 +++++++++++++++++ server/routes/{ => s}/[id].ts | 15 ++-- server/types.ts | 46 +++++++++++ server/utils/date.ts | 14 ++-- shared/types.ts | 40 +--------- src/assets/icon.svg | 6 -- src/components/column/card.tsx | 8 +- src/components/header.tsx | 47 ++++++++++- src/routes/index.tsx | 5 ++ vite.config.ts | 10 ++- wrangler.toml | 6 +- 18 files changed, 395 insertions(+), 74 deletions(-) rename server/{ => database}/cache.ts (92%) create mode 100644 server/database/user.ts create mode 100644 server/middleware/auth.ts create mode 100644 server/routes/me.ts create mode 100644 server/routes/oauth-callback/github.ts rename server/routes/{ => s}/[id].ts (85%) create mode 100644 server/types.ts delete mode 100644 src/assets/icon.svg diff --git a/.gitignore b/.gitignore index cfacc40..55a6a02 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ dist/ .vinxi .cache .data -.wrangler \ No newline at end of file +.wrangler +.env +.env.* \ No newline at end of file diff --git a/package.json b/package.json index 18e4cdd..0e86e55 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "author": { "url": "https://github.com/ourongxing/", "email": "orongxing@gmail.com", - "name": "Ou Rongxing" + "name": "Ou" }, "homepage": "https://github.com/ourongxing/newsnow", "scripts": { @@ -15,7 +15,7 @@ "build": "vite build", "lint": "eslint", "favicon": "tsx ./scripts/favicon.ts", - "start": "PORT=4444 node dist/output/server/index.mjs", + "start": "PORT=4444 node --env-file .env.vars 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", @@ -32,13 +32,16 @@ "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", "fast-xml-parser": "^4.5.0", "framer-motion": "^11.11.8", "h3": "^1.13.0", "iconv-lite": "^0.6.3", "jotai": "^2.10.0", + "jsonwebtoken": "^9.0.2", "libsql": "^0.4.6", "md5": "^2.3.0", "ofetch": "^1.4.1", @@ -56,12 +59,14 @@ "@tanstack/react-query": "^5.59.9", "@tanstack/router-devtools": "^1.64.0", "@tanstack/router-plugin": "^1.64.0", + "@types/jsonwebtoken": "^9.0.7", "@types/md5": "^2.3.5", "@types/react": "^18.3.11", "@types/react-dom": "^18.3.0", "@unocss/rule-utils": "^0.63.4", "@vitejs/plugin-react-swc": "^3.7.1", "bumpp": "^9.7.1", + "dotenv": "^16.4.5", "eslint": "^9.12.0", "eslint-plugin-react-hooks": "^5.1.0-rc-77f43893-20241010", "eslint-plugin-react-refresh": "^0.4.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d98c3f4..1a1c52a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -44,9 +44,15 @@ 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) + db0: + specifier: npm:@ourongxing/db0@0.1.6 + version: '@ourongxing/db0@0.1.6(@libsql/client@0.14.0)(libsql@0.4.6)' defu: specifier: ^6.1.4 version: 6.1.4 @@ -65,6 +71,9 @@ importers: jotai: specifier: ^2.10.0 version: 2.10.0(@types/react@18.3.11)(react@18.3.1) + jsonwebtoken: + specifier: ^9.0.2 + version: 9.0.2 libsql: specifier: ^0.4.6 version: 0.4.6 @@ -111,6 +120,9 @@ importers: '@tanstack/router-plugin': specifier: ^1.64.0 version: 1.64.0(vite@5.4.8(@types/node@22.6.1)(terser@5.33.0)) + '@types/jsonwebtoken': + specifier: ^9.0.7 + version: 9.0.7 '@types/md5': specifier: ^2.3.5 version: 2.3.5 @@ -129,6 +141,9 @@ importers: bumpp: specifier: ^9.7.1 version: 9.7.1(magicast@0.3.5) + dotenv: + specifier: ^16.4.5 + version: 16.4.5 eslint: specifier: ^9.12.0 version: 9.12.0(jiti@1.21.6) @@ -1817,6 +1832,9 @@ packages: '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/jsonwebtoken@9.0.7': + resolution: {integrity: sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==} + '@types/md5@2.3.5': resolution: {integrity: sha512-/i42wjYNgE6wf0j2bcTX6kuowmdL/6PE4IVitMpm2eYKBUuYCprdcWVK+xEF0gcV6ufMCRhtxmReGfc6hIK7Jw==} @@ -2271,6 +2289,9 @@ packages: resolution: {integrity: sha512-Db1SbgBS/fg/392AblrMJk97KggmvYhr4pB5ZIMTWtaivCPMWLkmb7m21cJvpvgK+J3nsU2CmmixNBZx4vFj/w==} engines: {node: '>=8.0.0'} + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2638,6 +2659,9 @@ packages: eastasianwidth@0.2.0: resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + ee-first@1.1.1: resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==} @@ -3541,6 +3565,16 @@ packages: jsonfile@6.1.0: resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -3609,12 +3643,33 @@ packages: lodash.defaults@4.2.0: resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + lodash.isarguments@3.1.0: resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==} + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} @@ -6381,6 +6436,10 @@ snapshots: '@types/json-schema@7.0.15': {} + '@types/jsonwebtoken@9.0.7': + dependencies: + '@types/node': 22.6.1 + '@types/md5@2.3.5': {} '@types/mdast@3.0.15': @@ -6987,6 +7046,8 @@ snapshots: buffer-crc32@1.0.0: {} + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@6.0.3: @@ -7330,6 +7391,10 @@ snapshots: eastasianwidth@0.2.0: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + ee-first@1.1.1: {} electron-to-chromium@1.5.28: {} @@ -8454,6 +8519,30 @@ snapshots: optionalDependencies: graceful-fs: 4.2.11 + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.6.3 + + jwa@1.4.1: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -8556,10 +8645,24 @@ snapshots: lodash.defaults@4.2.0: {} + lodash.includes@4.3.0: {} + lodash.isarguments@3.1.0: {} + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash@4.17.21: {} log-update@6.1.0: diff --git a/server/cache.ts b/server/database/cache.ts similarity index 92% rename from server/cache.ts rename to server/database/cache.ts index 8a4fecf..c1ba875 100644 --- a/server/cache.ts +++ b/server/database/cache.ts @@ -1,5 +1,6 @@ -import type { CacheInfo, NewsItem } from "@shared/types" +import type { NewsItem } from "@shared/types" import type { Database } from "db0" +import type { CacheInfo } from "../types" export class Cache { private db diff --git a/server/database/user.ts b/server/database/user.ts new file mode 100644 index 0000000..9a73c5a --- /dev/null +++ b/server/database/user.ts @@ -0,0 +1,65 @@ +import type { Database } from "@ourongxing/db0" +import type { UserInfo } from "#/types" + +export class UserTable { + private db + constructor(db: Database) { + this.db = db + } + + async init() { + await this.db.prepare(` + CREATE TABLE IF NOT EXISTS user ( + id TEXT PRIMARY KEY, + email TEXT, + data TEXT, + type TEXT, + created INTEGER, + updated INTEGER + ); + `).run() + logger.success(`init user table`) + } + + async addUser(id: string, email: string, type: "github") { + const u = await this.getUser(id) + const now = Date.now() + if (!u) { + await this.db.prepare(`INSERT INTO user (id, email, data, type, created, updated) VALUES (?, ?, ?, ?, ?, ?)`) + .run(id, email, "", type, now, now) + logger.success(`add user ${id}`) + } 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`) + } + } + + async getUser(id: string) { + return (await this.db.prepare(`SELECT id, email, data, created, updated FROM user WHERE id = ?`).get(id)) as UserInfo + } + + async setData(key: string, value: string) { + const now = Date.now() + const state = await this.db.prepare( + `REPLACE INTO user (id, data, updated) VALUES (?, ?, ?)`, + ).run(key, JSON.stringify(value), now) + if (!state.success) throw new Error(`set user ${key} data failed`) + logger.success(`set ${key} cache`) + } + + async getData(id: string) { + const row: any = await this.db.prepare(`SELECT data, update FROM user WHERE id = ?`).get(id) + if (!row || !row.data) throw new Error(`user ${id} not found`) + logger.success(`get ${id} data`) + return row.data as { + data: string + updated: number + } + } + + async deleteUser(key: string) { + const state = await this.db.prepare(`DELETE FROM user WHERE id = ?`).run(key) + if (!state.success) throw new Error(`delete user ${key} failed`) + logger.success(`delete user ${key}`) + } +} diff --git a/server/middleware/auth.ts b/server/middleware/auth.ts new file mode 100644 index 0000000..19f7f3d --- /dev/null +++ b/server/middleware/auth.ts @@ -0,0 +1,16 @@ +import process from "node:process" +import jwt from "jsonwebtoken" + +export default defineEventHandler((event) => { + const token = getCookie(event, "jwt") + if (token && process.env.JWT_SECRET) { + try { + const { id: userID, exp } = jwt.verify(token, process.env.JWT_SECRET) as { id: string, exp: number } + if (Date.now() < exp * 1000) { + event.context.user = userID + } + } catch { + logger.error("JWT verification failed") + } + } +}) diff --git a/server/routes/me.ts b/server/routes/me.ts new file mode 100644 index 0000000..bbebce2 --- /dev/null +++ b/server/routes/me.ts @@ -0,0 +1,3 @@ +export default defineEventHandler(async (event) => { + return event +}) diff --git a/server/routes/oauth-callback/github.ts b/server/routes/oauth-callback/github.ts new file mode 100644 index 0000000..1a13ec1 --- /dev/null +++ b/server/routes/oauth-callback/github.ts @@ -0,0 +1,69 @@ +import process from "node:process" +import jwt from "jsonwebtoken" +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 + + 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 = jwt.sign({ id: userID, type: "github" }, process.env.JWT_SECRET!, { + expiresIn: "70d", + }) + + const expires = new Date(Date.now() + 60 * 24 * 60 * 60 * 1000) + setCookie(event, "jwt", jwtToken, { expires }) + setCookie(event, "avatar", userInfo.avatar_url, { expires }) + setCookie(event, "name", userInfo.name, { expires }) + return sendRedirect(event, `/?login=github`) +}) diff --git a/server/routes/[id].ts b/server/routes/s/[id].ts similarity index 85% rename from server/routes/[id].ts rename to server/routes/s/[id].ts index 7046d64..6a6a842 100644 --- a/server/routes/[id].ts +++ b/server/routes/s/[id].ts @@ -2,7 +2,7 @@ import { TTL } from "@shared/consts" import type { SourceID, SourceResponse } from "@shared/types" import { sources } from "@shared/sources" import { sourcesFn } from "#/sources" -import { Cache } from "#/cache" +import { Cache } from "#/database/cache" export default defineEventHandler(async (event): Promise => { try { @@ -18,11 +18,11 @@ export default defineEventHandler(async (event): Promise => { } const db = useDatabase() - const cacheStore = db ? new Cache(db) : undefined + const cacheTable = db ? new Cache(db) : undefined const now = Date.now() - if (cacheStore) { - // await cacheStore.init() - const cache = await cacheStore.get(id) + if (cacheTable) { + await cacheTable.init() + const cache = await cacheTable.get(id) if (cache) { // interval 刷新间隔,对于缓存失效也要执行的。本质上表示本来内容更新就很慢,这个间隔内可能内容压根不会更新。 // 默认 10 分钟,是低于 TTL 的,但部分 Source 的间隔会超过 TTL,甚至有的一天刷新一次。 @@ -36,9 +36,10 @@ export default defineEventHandler(async (event): Promise => { }, } } + // 而 TTL 缓存失效时间,在时间范围内,就算内容更新了也要用这个缓存。 // 复用缓存是不会更新时间的。 - if (!latest && now - cache.updated < TTL) { + if ((!latest || !event.context.user) && now - cache.updated < TTL) { return { status: "cache", data: { @@ -52,7 +53,7 @@ export default defineEventHandler(async (event): Promise => { const data = await sourcesFn[id]() logger.success(`fetch ${id} latest`) - if (cacheStore) event.waitUntil(cacheStore.set(id, data)) + if (cacheTable) event.waitUntil(cacheTable.set(id, data)) return { status: "success", data: { diff --git a/server/types.ts b/server/types.ts new file mode 100644 index 0000000..046490a --- /dev/null +++ b/server/types.ts @@ -0,0 +1,46 @@ +import type { NewsItem, SourceID } from "@shared/types" + +export interface RSSInfo { + title: string + description: string + link: string + image: string + updatedTime: string + items: RSSItem[] +} +export interface RSSItem { + title: string + description: string + link: string + created?: string +} + +export interface CacheInfo { + id: SourceID + data: NewsItem[] + updated: number +} + +export interface RSSHubInfo { + title: string + home_page_url: string + description: string + items: RSSHubItem[] +} + +export interface RSSHubItem { + id: string + url: string + title: string + content_html: string + date_published: string +} + +export interface UserInfo { + id: string + email: string + type: "github" + data: string + created: number + updated: number +} diff --git a/server/utils/date.ts b/server/utils/date.ts index 334c85f..a9d1c27 100644 --- a/server/utils/date.ts +++ b/server/utils/date.ts @@ -1,10 +1,10 @@ -import dayjs from "dayjs" -import utcPlugin from "dayjs/plugin/utc.js" -import timezonePlugin from "dayjs/plugin/timezone.js" -import customParseFormat from "dayjs/plugin/customParseFormat" -import duration from "dayjs/plugin/duration" -import isSameOrBefore from "dayjs/plugin/isSameOrBefore" -import weekday from "dayjs/plugin/weekday" +import dayjs from "dayjs/esm" +import utcPlugin from "dayjs/esm/plugin/utc" +import timezonePlugin from "dayjs/esm/plugin/timezone" +import customParseFormat from "dayjs/esm/plugin/customParseFormat" +import duration from "dayjs/esm/plugin/duration" +import isSameOrBefore from "dayjs/esm/plugin/isSameOrBefore" +import weekday from "dayjs/esm/plugin/weekday" dayjs.extend(utcPlugin) dayjs.extend(timezonePlugin) diff --git a/shared/types.ts b/shared/types.ts index 9229507..bf6d32c 100644 --- a/shared/types.ts +++ b/shared/types.ts @@ -15,7 +15,7 @@ export type SourceID = { }[MainSourceID] export type ColumnID = (typeof columnIds)[number] -export type Metadata = Record +export type Metadata = Record export interface OriginSource { name: string @@ -49,7 +49,7 @@ export interface Source { redirect?: SourceID } -export interface Section { +export interface Column { name: string sources: SourceID[] } @@ -75,39 +75,3 @@ export type SourceResponse = { status: "error" message?: string } - -export interface RSSInfo { - title: string - description: string - link: string - image: string - updatedTime: string - items: RSSItem[] -} -export interface RSSItem { - title: string - description: string - link: string - created?: string -} - -export interface CacheInfo { - id: MainSourceID - data: NewsItem[] - updated: number -} - -export interface RSSHubInfo { - title: string - home_page_url: string - description: string - items: RSSHubItem[] -} - -export interface RSSHubItem { - id: string - url: string - title: string - content_html: string - date_published: string -} diff --git a/src/assets/icon.svg b/src/assets/icon.svg deleted file mode 100644 index 07042a8..0000000 --- a/src/assets/icon.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/src/components/column/card.tsx b/src/components/column/card.tsx index 989e8e8..53f712a 100644 --- a/src/components/column/card.tsx +++ b/src/components/column/card.tsx @@ -67,9 +67,9 @@ export function CardOverlay({ id }: { id: SourceID }) {
@@ -101,9 +101,9 @@ function NewsCard({ id, inView, handleListeners }: NewsCardProps) { queryKey: [id, refetchSource[id]], queryFn: async ({ queryKey }) => { const [_id, _refetchTime] = queryKey as [SourceID, number] - let url = `/api/${_id}` + let url = `/api/s/${_id}` if (Date.now() - _refetchTime < 1000) { - url = `/api/${_id}?latest` + url = `/api/s/${_id}?latest` } const response: SourceResponse = await ofetch(url, { timeout: 5000 }) if (response.status === "error") { diff --git a/src/components/header.tsx b/src/components/header.tsx index 7aceb6b..35cc5ec 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 logo from "~/assets/icon.svg" +import { useCookie } from "react-use" import { useDark } from "~/hooks/useDark" import { currentColumnAtom, goToTopAtom, refetchSourcesAtom } from "~/atoms" @@ -21,12 +21,49 @@ function ThemeToggle() { ) } +function LoginIn() { + const [name] = useCookie("name") + const [avatar] = useCookie("avatar") + const [jwt, setJwt] = useCookie("jwt") + if (jwt) { + return ( + + ) + } + return ( + _< + href={`https://github.com/login/oauth/authorize?client_id=${__G_CLIENT_ID__}&scope=read:user,user:email`} + /> + ) +} + function GoTop() { const { ok, fn: goToTop } = useAtomValue(goToTopAtom) return ( ok && (