From 142c780b50a6aac069770ec388690040eebea130 Mon Sep 17 00:00:00 2001 From: katelya Date: Fri, 5 Sep 2025 15:59:55 +0800 Subject: [PATCH] Fix TypeScript errors: Update User type system across all storage implementations --- public/sw.js | 2 +- src/app/api/admin/users/route.ts | 155 ++++++++++++++++ src/components/UserManagement.tsx | 288 ++++++++++++++++++++++++++++++ src/lib/d1.db.ts | 16 +- src/lib/kvrocks.db.ts | 30 +++- src/lib/localstorage.db.ts | 30 +++- src/lib/redis.db.ts | 33 +++- src/lib/types.ts | 9 +- src/lib/upstash.db.ts | 33 +++- 9 files changed, 574 insertions(+), 22 deletions(-) create mode 100644 src/app/api/admin/users/route.ts create mode 100644 src/components/UserManagement.tsx diff --git a/public/sw.js b/public/sw.js index e773124..45a293c 100644 --- a/public/sw.js +++ b/public/sw.js @@ -1 +1 @@ -if(!self.define){let e,t={};const s=(s,n)=>(s=new URL(s+".js",n).href,t[s]||new Promise(t=>{if("document"in self){const e=document.createElement("script");e.src=s,e.onload=t,document.head.appendChild(e)}else e=s,importScripts(s),t()}).then(()=>{let e=t[s];if(!e)throw new Error(`Module ${s} didn’t register its module`);return e}));self.define=(n,c)=>{const a=e||("document"in self?document.currentScript.src:"")||location.href;if(t[a])return;let i={};const u=e=>s(e,a),r={module:{uri:a},exports:i,require:u};t[a]=Promise.all(n.map(e=>r[e]||u(e))).then(e=>(c(...e),i))}}define(["./workbox-e9849328"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/app-build-manifest.json",revision:"a7fbfbfdafe57c95e97e8d3cfd839328"},{url:"/_next/static/UMt0xPAGCnkut4ut2VU-9/_buildManifest.js",revision:"046380ae5bc74b46b6d5eac3eed65355"},{url:"/_next/static/UMt0xPAGCnkut4ut2VU-9/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/110-442e7067e441a607.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/154-211e189482cc0258.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/29-2acace5e289d422b.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/459-0617417b9c423d5b.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/51b697cb-24a59f0c53e2e105.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/682-e64c4c7808d3e611.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/711-9ae080cb4f6a9355.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/900-c7c9e505cc903ead.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/998-f22ebd15e7bac0f0.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/app/_not-found/page-4dc7d52fd5d943cc.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/app/admin/page-7f30b4abb7bde63b.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/app/config/page-578e0487b53650a4.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/app/douban/page-6b5d567834ba726e.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/app/layout-41b89aac912b9c2b.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/app/login/page-bbafa236e79f06c3.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/app/page-a401624afa29aef0.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/app/play/page-7d5c45316b8afbc6.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/app/search/page-ce5b70ae0b349207.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/app/settings/page-95f3174527d62558.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/app/tvbox/page-443d4dd8e3c842b3.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/app/warning/page-11cba4cf9332a238.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/c72274ce-06682d6fc8197e6d.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/da9543df-bf6da1a431d8604f.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/framework-6e06c675866dc992.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/main-6c3022dbb9d044ff.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/main-app-dbd320e104e1a5dc.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/pages/_app-792b631a362c29e1.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/pages/_error-9fde6601392a2a99.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-17170f1d90853b2d.js",revision:"UMt0xPAGCnkut4ut2VU-9"},{url:"/_next/static/css/23100062f5d4aac0.css",revision:"23100062f5d4aac0"},{url:"/_next/static/css/275ed64cc4367444.css",revision:"275ed64cc4367444"},{url:"/_next/static/css/355e679a11861bb0.css",revision:"355e679a11861bb0"},{url:"/_next/static/media/26a46d62cd723877-s.woff2",revision:"befd9c0fdfa3d8a645d5f95717ed6420"},{url:"/_next/static/media/55c55f0601d81cf3-s.woff2",revision:"43828e14271c77b87e3ed582dbff9f74"},{url:"/_next/static/media/581909926a08bbc8-s.woff2",revision:"f0b86e7c24f455280b8df606b89af891"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/97e0cb1ae144a2a9-s.woff2",revision:"e360c61c5bd8d90639fd4503c829c2dc"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/favicon.ico",revision:"c5de6e56c5664adda146825f75ea6ecf"},{url:"/icons/icon-192x192.png",revision:"4a56c090828a1ad254c903c7aec0389d"},{url:"/icons/icon-256x256.png",revision:"f6409eb1a001f754121e3a8281c0319c"},{url:"/icons/icon-384x384.png",revision:"f6efc3e357b9ffdf4e0d8c14b2ed0ac1"},{url:"/icons/icon-512x512.png",revision:"9c008cbbeb6a576fe07bb1284a83f4d2"},{url:"/logo.png",revision:"40de611b143c47c6291c7bdad2c959ca"},{url:"/manifest.json",revision:"7bd3dabc1cfbfe40f09577efca223d31"},{url:"/robots.txt",revision:"e2b2cd8514443456bc6fb9d77b3b1f3e"},{url:"/screenshot1.png",revision:"10572bfcea54dc93ac4c5f7c9057fc98"},{url:"/screenshot2.png",revision:"f815a8990973a221899976867365c239"},{url:"/screenshot3.png",revision:"49709e96345dfeeab1d8083821d4b44e"},{url:"/screenshot4.png",revision:"a76c751e41e37556048a487e4f8b8b1c"},{url:"/wechat.jpg",revision:"d0f601311802667cd6ca5a37dc69bfa7"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:t,event:s,state:n})=>t&&"opaqueredirect"===t.type?new Response(t.body,{status:200,statusText:"OK",headers:t.headers}):t}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;const t=e.pathname;return!t.startsWith("/api/auth/")&&!!t.startsWith("/api/")},new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")},new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>!(self.origin===e.origin),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")}); +if(!self.define){let e,s={};const n=(n,t)=>(n=new URL(n+".js",t).href,s[n]||new Promise(s=>{if("document"in self){const e=document.createElement("script");e.src=n,e.onload=s,document.head.appendChild(e)}else e=n,importScripts(n),s()}).then(()=>{let e=s[n];if(!e)throw new Error(`Module ${n} didn’t register its module`);return e}));self.define=(t,c)=>{const a=e||("document"in self?document.currentScript.src:"")||location.href;if(s[a])return;let i={};const r=e=>n(e,a),o={module:{uri:a},exports:i,require:r};s[a]=Promise.all(t.map(e=>o[e]||r(e))).then(e=>(c(...e),i))}}define(["./workbox-e9849328"],function(e){"use strict";importScripts(),self.skipWaiting(),e.clientsClaim(),e.precacheAndRoute([{url:"/_next/app-build-manifest.json",revision:"da3e7f135771549b628a65338b86353c"},{url:"/_next/static/BTk6Xg2zuGHYv0RDM8tZg/_buildManifest.js",revision:"046380ae5bc74b46b6d5eac3eed65355"},{url:"/_next/static/BTk6Xg2zuGHYv0RDM8tZg/_ssgManifest.js",revision:"b6652df95db52feb4daf4eca35380933"},{url:"/_next/static/chunks/110-442e7067e441a607.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/154-211e189482cc0258.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/29-2acace5e289d422b.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/459-0617417b9c423d5b.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/51b697cb-24a59f0c53e2e105.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/682-e64c4c7808d3e611.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/711-9ae080cb4f6a9355.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/900-c7c9e505cc903ead.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/998-f22ebd15e7bac0f0.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/app/_not-found/page-4dc7d52fd5d943cc.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/app/admin/page-7f30b4abb7bde63b.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/app/config/page-578e0487b53650a4.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/app/douban/page-6b5d567834ba726e.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/app/layout-41b89aac912b9c2b.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/app/login/page-bbafa236e79f06c3.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/app/page-a401624afa29aef0.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/app/play/page-6297f80eaa080bf5.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/app/search/page-cafa7a89158278cb.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/app/settings/page-d73b9df16c781bd2.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/app/tvbox/page-443d4dd8e3c842b3.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/app/warning/page-11cba4cf9332a238.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/c72274ce-06682d6fc8197e6d.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/da9543df-bf6da1a431d8604f.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/framework-6e06c675866dc992.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/main-283be9c351146029.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/main-app-dbd320e104e1a5dc.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/pages/_app-792b631a362c29e1.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/pages/_error-9fde6601392a2a99.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/chunks/polyfills-42372ed130431b0a.js",revision:"846118c33b2c0e922d7b3a7676f81f6f"},{url:"/_next/static/chunks/webpack-17170f1d90853b2d.js",revision:"BTk6Xg2zuGHYv0RDM8tZg"},{url:"/_next/static/css/23100062f5d4aac0.css",revision:"23100062f5d4aac0"},{url:"/_next/static/css/275ed64cc4367444.css",revision:"275ed64cc4367444"},{url:"/_next/static/css/89b466011e0abf7c.css",revision:"89b466011e0abf7c"},{url:"/_next/static/media/26a46d62cd723877-s.woff2",revision:"befd9c0fdfa3d8a645d5f95717ed6420"},{url:"/_next/static/media/55c55f0601d81cf3-s.woff2",revision:"43828e14271c77b87e3ed582dbff9f74"},{url:"/_next/static/media/581909926a08bbc8-s.woff2",revision:"f0b86e7c24f455280b8df606b89af891"},{url:"/_next/static/media/8e9860b6e62d6359-s.woff2",revision:"01ba6c2a184b8cba08b0d57167664d75"},{url:"/_next/static/media/97e0cb1ae144a2a9-s.woff2",revision:"e360c61c5bd8d90639fd4503c829c2dc"},{url:"/_next/static/media/df0a9ae256c0569c-s.woff2",revision:"d54db44de5ccb18886ece2fda72bdfe0"},{url:"/_next/static/media/e4af272ccee01ff0-s.p.woff2",revision:"65850a373e258f1c897a2b3d75eb74de"},{url:"/favicon.ico",revision:"c5de6e56c5664adda146825f75ea6ecf"},{url:"/icons/icon-192x192.png",revision:"4a56c090828a1ad254c903c7aec0389d"},{url:"/icons/icon-256x256.png",revision:"f6409eb1a001f754121e3a8281c0319c"},{url:"/icons/icon-384x384.png",revision:"f6efc3e357b9ffdf4e0d8c14b2ed0ac1"},{url:"/icons/icon-512x512.png",revision:"9c008cbbeb6a576fe07bb1284a83f4d2"},{url:"/logo.png",revision:"40de611b143c47c6291c7bdad2c959ca"},{url:"/manifest.json",revision:"7bd3dabc1cfbfe40f09577efca223d31"},{url:"/robots.txt",revision:"e2b2cd8514443456bc6fb9d77b3b1f3e"},{url:"/screenshot1.png",revision:"10572bfcea54dc93ac4c5f7c9057fc98"},{url:"/screenshot2.png",revision:"f815a8990973a221899976867365c239"},{url:"/screenshot3.png",revision:"49709e96345dfeeab1d8083821d4b44e"},{url:"/screenshot4.png",revision:"a76c751e41e37556048a487e4f8b8b1c"},{url:"/wechat.jpg",revision:"d0f601311802667cd6ca5a37dc69bfa7"}],{ignoreURLParametersMatching:[]}),e.cleanupOutdatedCaches(),e.registerRoute("/",new e.NetworkFirst({cacheName:"start-url",plugins:[{cacheWillUpdate:async({request:e,response:s,event:n,state:t})=>s&&"opaqueredirect"===s.type?new Response(s.body,{status:200,statusText:"OK",headers:s.headers}):s}]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:gstatic)\.com\/.*/i,new e.CacheFirst({cacheName:"google-fonts-webfonts",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:31536e3})]}),"GET"),e.registerRoute(/^https:\/\/fonts\.(?:googleapis)\.com\/.*/i,new e.StaleWhileRevalidate({cacheName:"google-fonts-stylesheets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:eot|otf|ttc|ttf|woff|woff2|font.css)$/i,new e.StaleWhileRevalidate({cacheName:"static-font-assets",plugins:[new e.ExpirationPlugin({maxEntries:4,maxAgeSeconds:604800})]}),"GET"),e.registerRoute(/\.(?:jpg|jpeg|gif|png|svg|ico|webp)$/i,new e.StaleWhileRevalidate({cacheName:"static-image-assets",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/image\?url=.+$/i,new e.StaleWhileRevalidate({cacheName:"next-image",plugins:[new e.ExpirationPlugin({maxEntries:64,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp3|wav|ogg)$/i,new e.CacheFirst({cacheName:"static-audio-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:mp4)$/i,new e.CacheFirst({cacheName:"static-video-assets",plugins:[new e.RangeRequestsPlugin,new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:js)$/i,new e.StaleWhileRevalidate({cacheName:"static-js-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:css|less)$/i,new e.StaleWhileRevalidate({cacheName:"static-style-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\/_next\/data\/.+\/.+\.json$/i,new e.StaleWhileRevalidate({cacheName:"next-data",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(/\.(?:json|xml|csv)$/i,new e.NetworkFirst({cacheName:"static-data-assets",plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;const s=e.pathname;return!s.startsWith("/api/auth/")&&!!s.startsWith("/api/")},new e.NetworkFirst({cacheName:"apis",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:16,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>{if(!(self.origin===e.origin))return!1;return!e.pathname.startsWith("/api/")},new e.NetworkFirst({cacheName:"others",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:86400})]}),"GET"),e.registerRoute(({url:e})=>!(self.origin===e.origin),new e.NetworkFirst({cacheName:"cross-origin",networkTimeoutSeconds:10,plugins:[new e.ExpirationPlugin({maxEntries:32,maxAgeSeconds:3600})]}),"GET")}); diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts new file mode 100644 index 0000000..53af03e --- /dev/null +++ b/src/app/api/admin/users/route.ts @@ -0,0 +1,155 @@ +/* eslint-disable no-console, @typescript-eslint/no-explicit-any */ +import { NextRequest, NextResponse } from 'next/server'; + +import { getStorage } from '@/lib/db'; +import { User } from '@/lib/types'; + +export const runtime = 'edge'; + +// 检查是否为站长账户 +function isOwnerAccount(username: string): boolean { + const ownerUsername = process.env.USERNAME || 'admin'; + return username === ownerUsername; +} + +export async function GET(request: NextRequest) { + try { + // 从Authorization头获取当前用户 + const auth = request.headers.get('Authorization')?.replace('Bearer ', ''); + if (!auth) { + return NextResponse.json({ error: '需要认证' }, { status: 401 }); + } + + const currentUsername = decodeURIComponent(auth); + + // 检查是否为站长账户 + if (!isOwnerAccount(currentUsername)) { + return NextResponse.json({ error: '权限不足' }, { status: 403 }); + } + + // 获取所有用户及其设置 + const storage = getStorage(); + const users: User[] = await storage.getAllUsers(); + const usersWithSettings = await Promise.all( + users.map(async (user) => { + const settings = await storage.getUserSettings(user.username); + return { + username: user.username, + role: user.role || 'user', + created_at: user.created_at, + filter_adult_content: settings?.filter_adult_content ?? true, + can_disable_filter: settings?.can_disable_filter ?? true, + managed_by_admin: settings?.managed_by_admin ?? false, + last_filter_change: settings?.last_filter_change + }; + }) + ); + + return NextResponse.json({ + users: usersWithSettings, + total: usersWithSettings.length + }); + + } catch (error) { + console.error('获取用户列表失败:', error); + return NextResponse.json({ error: '获取用户列表失败' }, { status: 500 }); + } +} + +export async function POST(request: NextRequest) { + try { + // 从Authorization头获取当前用户 + const auth = request.headers.get('Authorization')?.replace('Bearer ', ''); + if (!auth) { + return NextResponse.json({ error: '需要认证' }, { status: 401 }); + } + + const currentUsername = decodeURIComponent(auth); + + // 检查是否为站长账户 + if (!isOwnerAccount(currentUsername)) { + return NextResponse.json({ error: '权限不足' }, { status: 403 }); + } + + const storage = getStorage(); + const { action, username, settings } = await request.json(); + + switch (action) { + case 'update_settings': { + // 更新用户设置 + const currentSettings = await storage.getUserSettings(username); + const newSettings = { + ...currentSettings, + ...settings, + last_filter_change: new Date().toISOString() + }; + + await storage.setUserSettings(username, newSettings); + + return NextResponse.json({ + success: true, + message: `已更新用户 ${username} 的设置` + }); + } + + case 'force_filter': { + // 强制开启某用户的成人内容过滤 + const currentSettings = await storage.getUserSettings(username) || { + filter_adult_content: true, + theme: 'auto' as const, + language: 'zh-CN', + auto_play: false, + video_quality: 'auto' + }; + + await storage.setUserSettings(username, { + ...currentSettings, + filter_adult_content: true, + can_disable_filter: false, + managed_by_admin: true, + last_filter_change: new Date().toISOString() + }); + + return NextResponse.json({ + success: true, + message: `已强制开启用户 ${username} 的成人内容过滤` + }); + } + + case 'allow_disable': { + // 允许用户自己管理过滤设置 + const existingSettings = await storage.getUserSettings(username) || { + filter_adult_content: true, + theme: 'auto' as const, + language: 'zh-CN', + auto_play: false, + video_quality: 'auto' + }; + + await storage.setUserSettings(username, { + ...existingSettings, + filter_adult_content: existingSettings.filter_adult_content ?? true, + theme: existingSettings.theme || 'auto', + language: existingSettings.language || 'zh-CN', + auto_play: existingSettings.auto_play ?? false, + video_quality: existingSettings.video_quality || 'auto', + can_disable_filter: true, + managed_by_admin: false, + last_filter_change: new Date().toISOString() + }); + + return NextResponse.json({ + success: true, + message: `已允许用户 ${username} 自己管理过滤设置` + }); + } + + default: + return NextResponse.json({ error: '未知操作' }, { status: 400 }); + } + + } catch (error) { + console.error('用户管理操作失败:', error); + return NextResponse.json({ error: '操作失败' }, { status: 500 }); + } +} diff --git a/src/components/UserManagement.tsx b/src/components/UserManagement.tsx new file mode 100644 index 0000000..a731e69 --- /dev/null +++ b/src/components/UserManagement.tsx @@ -0,0 +1,288 @@ +/* eslint-disable no-console, @typescript-eslint/no-explicit-any */ +'use client'; + +import { useEffect, useState } from 'react'; + +// 临时内联认证函数,避免导入问题 +function getAuthInfoFromBrowserCookie(): { + password?: string; + username?: string; + signature?: string; + timestamp?: number; +} | null { + if (typeof window === 'undefined') { + return null; + } + + const cookies = document.cookie.split(';'); + const authCookie = cookies.find(cookie => cookie.trim().startsWith('auth=')); + + if (!authCookie) { + return null; + } + + try { + const cookieValue = authCookie.split('=')[1]; + const decoded = decodeURIComponent(cookieValue); + const authData = JSON.parse(decoded); + return authData; + } catch (error) { + console.error('Failed to parse auth cookie:', error); + return null; + } +} + +interface UserInfo { + username: string; + role: string; + created_at: string; + filter_adult_content: boolean; + can_disable_filter: boolean; + managed_by_admin: boolean; + last_filter_change?: string; +} + +export default function UserManagement() { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [currentUser, setCurrentUser] = useState(null); + + useEffect(() => { + // 获取当前用户信息 + const authInfo = getAuthInfoFromBrowserCookie(); + if (authInfo?.username) { + setCurrentUser(authInfo.username); + loadUsers(); + } else { + setError('未登录或权限不足'); + setLoading(false); + } + }, []); + + const loadUsers = async () => { + try { + const authInfo = getAuthInfoFromBrowserCookie(); + if (!authInfo?.username) { + throw new Error('未获取到用户认证信息'); + } + + const response = await fetch('/api/admin/users', { + headers: { + 'Authorization': `Bearer ${encodeURIComponent(authInfo.username)}`, + 'Cache-Control': 'no-cache' + } + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || '获取用户列表失败'); + } + + const data = await response.json(); + setUsers(data.users || []); + } catch (err) { + setError(err instanceof Error ? err.message : '未知错误'); + console.error('加载用户列表失败:', err); + } finally { + setLoading(false); + } + }; + + const updateUserSettings = async (username: string, action: string, settings?: any) => { + try { + const authInfo = getAuthInfoFromBrowserCookie(); + if (!authInfo?.username) { + throw new Error('未获取到用户认证信息'); + } + + const response = await fetch('/api/admin/users', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${encodeURIComponent(authInfo.username)}` + }, + body: JSON.stringify({ + action, + username, + settings + }) + }); + + if (!response.ok) { + const errorData = await response.json(); + throw new Error(errorData.error || '操作失败'); + } + + const data = await response.json(); + alert(data.message || '操作成功'); + + // 重新加载用户列表 + await loadUsers(); + } catch (err) { + const errorMsg = err instanceof Error ? err.message : '未知错误'; + alert(`操作失败: ${errorMsg}`); + console.error('用户管理操作失败:', err); + } + }; + + const handleForceFilter = (username: string) => { + if (confirm(`确定要强制开启用户 ${username} 的成人内容过滤功能吗?`)) { + updateUserSettings(username, 'force_filter'); + } + }; + + const handleAllowDisable = (username: string) => { + if (confirm(`确定要允许用户 ${username} 自己管理过滤设置吗?`)) { + updateUserSettings(username, 'allow_disable'); + } + }; + + if (loading) { + return ( +
+
+
+ ); + } + + if (error) { + return ( +
+
{error}
+ +
+ ); + } + + return ( +
+
+

+ 用户管理 +

+
+ 总计 {users.length} 个用户 +
+
+ +
+
+ + + + + + + + + + + + {users.map((user) => ( + + + + + + + + ))} + +
+ 用户名 + + 角色 + + 成人内容过滤 + + 管理状态 + + 操作 +
+
+
+ {user.username} +
+ {user.username === currentUser && ( + + 当前用户 + + )} +
+
+ + {user.role === 'owner' ? '站长' : '用户'} + + +
+ + {user.filter_adult_content ? '已开启' : '已关闭'} + +
+
+ {user.managed_by_admin ? ( + 管理员控制 + ) : user.can_disable_filter ? ( + 用户自主 + ) : ( + 受限制 + )} + + {user.role !== 'owner' && user.username !== currentUser && ( +
+ {!user.filter_adult_content || !user.managed_by_admin ? ( + + ) : null} + {user.managed_by_admin || !user.can_disable_filter ? ( + + ) : null} +
+ )} +
+
+ + {users.length === 0 && ( +
+ 暂无用户数据 +
+ )} +
+ +
+

+ 说明 +

+
    +
  • 强制过滤:开启用户的成人内容过滤,用户无法自己关闭
  • +
  • 允许自主:允许用户自己管理成人内容过滤设置
  • +
  • • 站长账户默认具有所有权限,无法被其他用户管理
  • +
  • • 管理员控制的用户无法在用户设置中关闭成人内容过滤
  • +
+
+
+ ); +} diff --git a/src/lib/d1.db.ts b/src/lib/d1.db.ts index e8384a4..4cb53fc 100644 --- a/src/lib/d1.db.ts +++ b/src/lib/d1.db.ts @@ -1,7 +1,7 @@ /* eslint-disable no-console, @typescript-eslint/no-explicit-any, @typescript-eslint/no-non-null-assertion */ import { AdminConfig } from './admin.types'; -import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, UserSettings } from './types'; +import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, User, UserSettings } from './types'; // 搜索历史最大条数 const SEARCH_HISTORY_LIMIT = 20; @@ -428,14 +428,20 @@ export class D1Storage implements IStorage { } // 用户列表 - async getAllUsers(): Promise { + async getAllUsers(): Promise { try { const db = await this.getDatabase(); const result = await db - .prepare('SELECT username FROM users ORDER BY created_at ASC') - .all<{ username: string }>(); + .prepare('SELECT username, created_at FROM users ORDER BY created_at ASC') + .all<{ username: string; created_at: string }>(); - return result.results.map((row) => row.username); + const ownerUsername = process.env.USERNAME || 'admin'; + + return result.results.map((row) => ({ + username: row.username, + role: row.username === ownerUsername ? 'owner' : 'user', + created_at: row.created_at + })); } catch (err) { console.error('Failed to get all users:', err); throw err; diff --git a/src/lib/kvrocks.db.ts b/src/lib/kvrocks.db.ts index 5482b73..6df70cf 100644 --- a/src/lib/kvrocks.db.ts +++ b/src/lib/kvrocks.db.ts @@ -3,7 +3,7 @@ import { createClient, RedisClientType } from 'redis'; import { AdminConfig } from './admin.types'; -import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, UserSettings } from './types'; +import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, User, UserSettings } from './types'; // 搜索历史最大条数 const SEARCH_HISTORY_LIMIT = 20; @@ -266,9 +266,31 @@ export class KvrocksStorage implements IStorage { }); } - async getAllUsers(): Promise { - const users = await withRetry(() => this.client.sMembers(this.userListKey())); - return ensureStringArray(users); + async getAllUsers(): Promise { + const usernames = await withRetry(() => this.client.sMembers(this.userListKey())); + const ownerUsername = process.env.USERNAME || 'admin'; + + const users = await Promise.all( + usernames.map(async (username) => { + let created_at = ''; + try { + const userData = await this.getUser(username); + if (userData?.created_at) { + created_at = new Date(userData.created_at).toISOString(); + } + } catch (err) { + // 忽略错误,使用空字符串 + } + + return { + username, + role: username === ownerUsername ? 'owner' : 'user', + created_at + }; + }) + ); + + return users; } async registerUser(userName: string, password: string): Promise { diff --git a/src/lib/localstorage.db.ts b/src/lib/localstorage.db.ts index f38f7e0..e50efab 100644 --- a/src/lib/localstorage.db.ts +++ b/src/lib/localstorage.db.ts @@ -1,6 +1,6 @@ /* eslint-disable no-console */ import { AdminConfig } from './admin.types'; -import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, UserSettings } from './types'; +import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, User, UserSettings } from './types'; /** * LocalStorage 存储实现 @@ -341,18 +341,38 @@ export class LocalStorage implements IStorage { } // ---------- 管理员功能 ---------- - async getAllUsers(): Promise { + async getAllUsers(): Promise { if (typeof window === 'undefined') return []; try { - const users: string[] = []; + const users: User[] = []; const prefix = 'katelyatv_user_'; + const ownerUsername = process.env.USERNAME || 'admin'; for (let i = 0; i < localStorage.length; i++) { const storageKey = localStorage.key(i); if (storageKey && storageKey.startsWith(prefix)) { - const userName = storageKey.replace(prefix, ''); - users.push(userName); + const username = storageKey.replace(prefix, ''); + + // 尝试获取用户创建时间 + let created_at = ''; + try { + const userDataStr = localStorage.getItem(storageKey); + if (userDataStr) { + const userData = JSON.parse(userDataStr); + if (userData.created_at) { + created_at = new Date(userData.created_at).toISOString(); + } + } + } catch (err) { + // 忽略解析错误 + } + + users.push({ + username, + role: username === ownerUsername ? 'owner' : 'user', + created_at + }); } } diff --git a/src/lib/redis.db.ts b/src/lib/redis.db.ts index 1a5e7dd..d548775 100644 --- a/src/lib/redis.db.ts +++ b/src/lib/redis.db.ts @@ -3,7 +3,7 @@ import { createClient, RedisClientType } from 'redis'; import { AdminConfig } from './admin.types'; -import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, UserSettings } from './types'; +import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, User, UserSettings } from './types'; // 搜索历史最大条数 const SEARCH_HISTORY_LIMIT = 20; @@ -302,14 +302,41 @@ export class RedisStorage implements IStorage { } // ---------- 获取全部用户 ---------- - async getAllUsers(): Promise { + async getAllUsers(): Promise { const keys = await withRetry(() => this.client.keys('u:*:pwd')); - return keys + const ownerUsername = process.env.USERNAME || 'admin'; + + const usernames = keys .map((k) => { const match = k.match(/^u:(.+?):pwd$/); return match ? ensureString(match[1]) : undefined; }) .filter((u): u is string => typeof u === 'string'); + + // 获取用户创建时间并构造 User 对象 + const users = await Promise.all( + usernames.map(async (username) => { + // 尝试获取用户创建时间,如果没有则使用空字符串 + const createdAtKey = `u:${username}:created_at`; + let created_at = ''; + try { + const timestamp = await withRetry(() => this.client.get(createdAtKey)); + if (timestamp) { + created_at = new Date(parseInt(timestamp)).toISOString(); + } + } catch (err) { + // 忽略错误,使用空字符串 + } + + return { + username, + role: username === ownerUsername ? 'owner' : 'user', + created_at + }; + }) + ); + + return users; } // ---------- 管理员配置 ---------- diff --git a/src/lib/types.ts b/src/lib/types.ts index 9b00095..95c7553 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -42,6 +42,13 @@ export interface Favorite { search_title: string; // 搜索时使用的标题 } +// 用户数据结构 +export interface User { + username: string; + role?: string; + created_at?: string; +} + // 存储接口 export interface IStorage { // 播放记录相关 @@ -87,7 +94,7 @@ export interface IStorage { deleteSkipConfig(userName: string, key: string): Promise; // 用户列表 - getAllUsers(): Promise; + getAllUsers(): Promise; // 管理员配置相关 getAdminConfig(): Promise; diff --git a/src/lib/upstash.db.ts b/src/lib/upstash.db.ts index c578feb..4bf1da6 100644 --- a/src/lib/upstash.db.ts +++ b/src/lib/upstash.db.ts @@ -3,7 +3,7 @@ import { Redis } from '@upstash/redis'; import { AdminConfig } from './admin.types'; -import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, UserSettings } from './types'; +import { EpisodeSkipConfig, Favorite, IStorage, PlayRecord, User, UserSettings } from './types'; // 搜索历史最大条数 const SEARCH_HISTORY_LIMIT = 20; @@ -244,14 +244,41 @@ export class UpstashRedisStorage implements IStorage { } // ---------- 获取全部用户 ---------- - async getAllUsers(): Promise { + async getAllUsers(): Promise { const keys = await withRetry(() => this.client.keys('u:*:pwd')); - return keys + const ownerUsername = process.env.USERNAME || 'admin'; + + const usernames = keys .map((k) => { const match = k.match(/^u:(.+?):pwd$/); return match ? ensureString(match[1]) : undefined; }) .filter((u): u is string => typeof u === 'string'); + + // 获取用户创建时间并构造 User 对象 + const users = await Promise.all( + usernames.map(async (username) => { + // 尝试获取用户创建时间,如果没有则使用空字符串 + const createdAtKey = `u:${username}:created_at`; + let created_at = ''; + try { + const timestamp = await withRetry(() => this.client.get(createdAtKey)); + if (timestamp && typeof timestamp === 'number') { + created_at = new Date(timestamp).toISOString(); + } + } catch (err) { + // 忽略错误,使用空字符串 + } + + return { + username, + role: username === ownerUsername ? 'owner' : 'user', + created_at + }; + }) + ); + + return users; } // ---------- 管理员配置 ----------