Fix TypeScript errors: Update User type system across all storage implementations
This commit is contained in:
+1
-1
@@ -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")});
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
@@ -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<UserInfo[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [currentUser, setCurrentUser] = useState<string | null>(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 (
|
||||
<div className="flex items-center justify-center h-40">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-500"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-md p-4">
|
||||
<div className="text-red-600 dark:text-red-400">{error}</div>
|
||||
<button
|
||||
onClick={loadUsers}
|
||||
className="mt-2 text-sm text-red-600 dark:text-red-400 underline hover:no-underline"
|
||||
>
|
||||
重试
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-bold text-gray-800 dark:text-gray-200">
|
||||
用户管理
|
||||
</h2>
|
||||
<div className="text-sm text-gray-600 dark:text-gray-400">
|
||||
总计 {users.length} 个用户
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white dark:bg-gray-800 rounded-lg shadow overflow-hidden">
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
|
||||
<thead className="bg-gray-50 dark:bg-gray-900">
|
||||
<tr>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
用户名
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
角色
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
成人内容过滤
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
管理状态
|
||||
</th>
|
||||
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
操作
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{users.map((user) => (
|
||||
<tr key={user.username} className="hover:bg-gray-50 dark:hover:bg-gray-700">
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<div className="text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{user.username}
|
||||
</div>
|
||||
{user.username === currentUser && (
|
||||
<span className="ml-2 px-2 py-1 text-xs bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200 rounded-full">
|
||||
当前用户
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
user.role === 'owner'
|
||||
? 'bg-purple-100 dark:bg-purple-900 text-purple-800 dark:text-purple-200'
|
||||
: 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300'
|
||||
}`}>
|
||||
{user.role === 'owner' ? '站长' : '用户'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap">
|
||||
<div className="flex items-center">
|
||||
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${
|
||||
user.filter_adult_content
|
||||
? 'bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200'
|
||||
: 'bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200'
|
||||
}`}>
|
||||
{user.filter_adult_content ? '已开启' : '已关闭'}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100">
|
||||
{user.managed_by_admin ? (
|
||||
<span className="text-orange-600 dark:text-orange-400">管理员控制</span>
|
||||
) : user.can_disable_filter ? (
|
||||
<span className="text-green-600 dark:text-green-400">用户自主</span>
|
||||
) : (
|
||||
<span className="text-gray-600 dark:text-gray-400">受限制</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
{user.role !== 'owner' && user.username !== currentUser && (
|
||||
<div className="flex space-x-2">
|
||||
{!user.filter_adult_content || !user.managed_by_admin ? (
|
||||
<button
|
||||
onClick={() => handleForceFilter(user.username)}
|
||||
className="text-red-600 hover:text-red-900 dark:text-red-400 dark:hover:text-red-300"
|
||||
>
|
||||
强制过滤
|
||||
</button>
|
||||
) : null}
|
||||
{user.managed_by_admin || !user.can_disable_filter ? (
|
||||
<button
|
||||
onClick={() => handleAllowDisable(user.username)}
|
||||
className="text-green-600 hover:text-green-900 dark:text-green-400 dark:hover:text-green-300"
|
||||
>
|
||||
允许自主
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{users.length === 0 && (
|
||||
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
|
||||
暂无用户数据
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-md p-4">
|
||||
<h3 className="text-sm font-medium text-blue-800 dark:text-blue-200 mb-2">
|
||||
说明
|
||||
</h3>
|
||||
<ul className="text-sm text-blue-600 dark:text-blue-300 space-y-1">
|
||||
<li>• <strong>强制过滤</strong>:开启用户的成人内容过滤,用户无法自己关闭</li>
|
||||
<li>• <strong>允许自主</strong>:允许用户自己管理成人内容过滤设置</li>
|
||||
<li>• 站长账户默认具有所有权限,无法被其他用户管理</li>
|
||||
<li>• 管理员控制的用户无法在用户设置中关闭成人内容过滤</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
+11
-5
@@ -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<string[]> {
|
||||
async getAllUsers(): Promise<User[]> {
|
||||
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;
|
||||
|
||||
+26
-4
@@ -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<string[]> {
|
||||
const users = await withRetry(() => this.client.sMembers(this.userListKey()));
|
||||
return ensureStringArray(users);
|
||||
async getAllUsers(): Promise<User[]> {
|
||||
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<void> {
|
||||
|
||||
@@ -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<string[]> {
|
||||
async getAllUsers(): Promise<User[]> {
|
||||
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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+30
-3
@@ -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<string[]> {
|
||||
async getAllUsers(): Promise<User[]> {
|
||||
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;
|
||||
}
|
||||
|
||||
// ---------- 管理员配置 ----------
|
||||
|
||||
+8
-1
@@ -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<void>;
|
||||
|
||||
// 用户列表
|
||||
getAllUsers(): Promise<string[]>;
|
||||
getAllUsers(): Promise<User[]>;
|
||||
|
||||
// 管理员配置相关
|
||||
getAdminConfig(): Promise<AdminConfig | null>;
|
||||
|
||||
+30
-3
@@ -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<string[]> {
|
||||
async getAllUsers(): Promise<User[]> {
|
||||
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;
|
||||
}
|
||||
|
||||
// ---------- 管理员配置 ----------
|
||||
|
||||
Reference in New Issue
Block a user