diff --git a/.eslintcache b/.eslintcache index 28d825a..4ae57e2 100644 --- a/.eslintcache +++ b/.eslintcache @@ -1 +1 @@ -[{"E:\\Document\\Coding\\SecScore\\eslint.config.mjs":"1","E:\\Document\\Coding\\SecScore\\src\\App.tsx":"2","E:\\Document\\Coding\\SecScore\\src\\ClientContext.ts":"3","E:\\Document\\Coding\\SecScore\\src\\components\\BoardManager.tsx":"4","E:\\Document\\Coding\\SecScore\\src\\components\\ContentArea.tsx":"5","E:\\Document\\Coding\\SecScore\\src\\components\\EventHistory.tsx":"6","E:\\Document\\Coding\\SecScore\\src\\components\\Home.tsx":"7","E:\\Document\\Coding\\SecScore\\src\\components\\Leaderboard.tsx":"8","E:\\Document\\Coding\\SecScore\\src\\components\\OAuthCallback.tsx":"9","E:\\Document\\Coding\\SecScore\\src\\components\\OAuthLogin.tsx":"10","E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBE.tsx":"11","E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBEBackground.tsx":"12","E:\\Document\\Coding\\SecScore\\src\\components\\ReasonManager.tsx":"13","E:\\Document\\Coding\\SecScore\\src\\components\\RewardExchange.tsx":"14","E:\\Document\\Coding\\SecScore\\src\\components\\RewardSettings.tsx":"15","E:\\Document\\Coding\\SecScore\\src\\components\\ScoreManager.tsx":"16","E:\\Document\\Coding\\SecScore\\src\\components\\Settings.tsx":"17","E:\\Document\\Coding\\SecScore\\src\\components\\SettlementHistory.tsx":"18","E:\\Document\\Coding\\SecScore\\src\\components\\Sidebar.tsx":"19","E:\\Document\\Coding\\SecScore\\src\\components\\StudentManager.tsx":"20","E:\\Document\\Coding\\SecScore\\src\\components\\TagEditorDialog.tsx":"21","E:\\Document\\Coding\\SecScore\\src\\components\\ThemeEditor.tsx":"22","E:\\Document\\Coding\\SecScore\\src\\components\\ThemeQuickSettings.tsx":"23","E:\\Document\\Coding\\SecScore\\src\\components\\TitleBar.tsx":"24","E:\\Document\\Coding\\SecScore\\src\\components\\Versions.tsx":"25","E:\\Document\\Coding\\SecScore\\src\\components\\WindowControls.tsx":"26","E:\\Document\\Coding\\SecScore\\src\\components\\Wizard.tsx":"27","E:\\Document\\Coding\\SecScore\\src\\contexts\\ServiceContext.tsx":"28","E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeContext.tsx":"29","E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeEditorContext.tsx":"30","E:\\Document\\Coding\\SecScore\\src\\env.d.ts":"31","E:\\Document\\Coding\\SecScore\\src\\hooks\\useResponsive.ts":"32","E:\\Document\\Coding\\SecScore\\src\\i18n\\index.ts":"33","E:\\Document\\Coding\\SecScore\\src\\main.tsx":"34","E:\\Document\\Coding\\SecScore\\src\\preload\\types.ts":"35","E:\\Document\\Coding\\SecScore\\src\\react-19-patch.ts":"36","E:\\Document\\Coding\\SecScore\\src\\services\\StudentService.ts":"37","E:\\Document\\Coding\\SecScore\\src\\shared\\kernel.ts":"38","E:\\Document\\Coding\\SecScore\\src\\shared\\mobileNavigation.ts":"39","E:\\Document\\Coding\\SecScore\\src\\utils\\color.ts":"40","E:\\Document\\Coding\\SecScore\\src\\utils\\studentAvatar.ts":"41","E:\\Document\\Coding\\SecScore\\src\\workers\\xlsxWorker.ts":"42","E:\\Document\\Coding\\SecScore\\vite.config.ts":"43"},{"size":1413,"mtime":1774084765386,"results":"44","hashOfConfig":"45"},{"size":38217,"mtime":1774773410827,"results":"46","hashOfConfig":"47"},{"size":124,"mtime":1774084765537,"results":"48","hashOfConfig":"47"},{"size":47714,"mtime":1774704827018,"results":"49","hashOfConfig":"47"},{"size":12490,"mtime":1774773413504,"results":"50","hashOfConfig":"47"},{"size":2044,"mtime":1774084765557,"results":"51","hashOfConfig":"47"},{"size":90881,"mtime":1774704827750,"results":"52","hashOfConfig":"47"},{"size":8101,"mtime":1774084765561,"results":"53","hashOfConfig":"47"},{"size":1047,"mtime":1774772479779,"results":"54","hashOfConfig":"47"},{"size":4948,"mtime":1774780673579,"results":"55","hashOfConfig":"47"},{"size":36271,"mtime":1774176288912,"results":"56","hashOfConfig":"47"},{"size":4203,"mtime":1774084765566,"results":"57","hashOfConfig":"47"},{"size":5983,"mtime":1774084765569,"results":"58","hashOfConfig":"47"},{"size":9360,"mtime":1774084765571,"results":"59","hashOfConfig":"47"},{"size":6151,"mtime":1774084765572,"results":"60","hashOfConfig":"47"},{"size":12301,"mtime":1774084765573,"results":"61","hashOfConfig":"47"},{"size":55529,"mtime":1774779133310,"results":"62","hashOfConfig":"47"},{"size":5950,"mtime":1774084765578,"results":"63","hashOfConfig":"47"},{"size":10335,"mtime":1774675242424,"results":"64","hashOfConfig":"47"},{"size":79058,"mtime":1774704830361,"results":"65","hashOfConfig":"47"},{"size":6331,"mtime":1774084765584,"results":"66","hashOfConfig":"47"},{"size":8079,"mtime":1774084765586,"results":"67","hashOfConfig":"47"},{"size":10954,"mtime":1774084765587,"results":"68","hashOfConfig":"47"},{"size":3858,"mtime":1774084765589,"results":"69","hashOfConfig":"47"},{"size":431,"mtime":1774084765590,"results":"70","hashOfConfig":"47"},{"size":2367,"mtime":1774084765592,"results":"71","hashOfConfig":"47"},{"size":1560,"mtime":1774084765594,"results":"72","hashOfConfig":"47"},{"size":362,"mtime":1774084765604,"results":"73","hashOfConfig":"47"},{"size":5985,"mtime":1774084765606,"results":"74","hashOfConfig":"47"},{"size":2824,"mtime":1774084765608,"results":"75","hashOfConfig":"47"},{"size":38,"mtime":1774084765611,"results":"76","hashOfConfig":"47"},{"size":2097,"mtime":1774084765613,"results":"77","hashOfConfig":"47"},{"size":1533,"mtime":1774084765616,"results":"78","hashOfConfig":"47"},{"size":6919,"mtime":1774704831300,"results":"79","hashOfConfig":"47"},{"size":17900,"mtime":1774779134852,"results":"80","hashOfConfig":"47"},{"size":1150,"mtime":1774084765630,"results":"81","hashOfConfig":"47"},{"size":667,"mtime":1774084765638,"results":"82","hashOfConfig":"47"},{"size":3061,"mtime":1774176077013,"results":"83","hashOfConfig":"47"},{"size":1716,"mtime":1774772796302,"results":"84","hashOfConfig":"47"},{"size":3256,"mtime":1774084765655,"results":"85","hashOfConfig":"47"},{"size":1115,"mtime":1774084765658,"results":"86","hashOfConfig":"47"},{"size":947,"mtime":1774084765660,"results":"87","hashOfConfig":"47"},{"size":709,"mtime":1774665241507,"results":"88","hashOfConfig":"47"},{"filePath":"89","messages":"90","suppressedMessages":"91","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"oyxevq",{"filePath":"92","messages":"93","suppressedMessages":"94","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"f6a32r",{"filePath":"95","messages":"96","suppressedMessages":"97","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"98","messages":"99","suppressedMessages":"100","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"101","messages":"102","suppressedMessages":"103","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"104","messages":"105","suppressedMessages":"106","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"107","messages":"108","suppressedMessages":"109","errorCount":3,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"110","messages":"111","suppressedMessages":"112","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"113","messages":"114","suppressedMessages":"115","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"116","messages":"117","suppressedMessages":"118","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"119","messages":"120","suppressedMessages":"121","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"122","messages":"123","suppressedMessages":"124","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"125","messages":"126","suppressedMessages":"127","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"128","messages":"129","suppressedMessages":"130","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"131","messages":"132","suppressedMessages":"133","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"134","messages":"135","suppressedMessages":"136","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"137","messages":"138","suppressedMessages":"139","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"140","messages":"141","suppressedMessages":"142","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"143","messages":"144","suppressedMessages":"145","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"146","messages":"147","suppressedMessages":"148","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"149","messages":"150","suppressedMessages":"151","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"152","messages":"153","suppressedMessages":"154","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"155","messages":"156","suppressedMessages":"157","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"158","messages":"159","suppressedMessages":"160","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"161","messages":"162","suppressedMessages":"163","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"164","messages":"165","suppressedMessages":"166","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"167","messages":"168","suppressedMessages":"169","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"170","messages":"171","suppressedMessages":"172","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"173","messages":"174","suppressedMessages":"175","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"176","messages":"177","suppressedMessages":"178","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"179","messages":"180","suppressedMessages":"181","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"182","messages":"183","suppressedMessages":"184","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"185","messages":"186","suppressedMessages":"187","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"188","messages":"189","suppressedMessages":"190","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"191","messages":"192","suppressedMessages":"193","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"194","messages":"195","suppressedMessages":"196","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"197","messages":"198","suppressedMessages":"199","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"200","messages":"201","suppressedMessages":"202","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"203","messages":"204","suppressedMessages":"205","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"206","messages":"207","suppressedMessages":"208","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"209","messages":"210","suppressedMessages":"211","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"212","messages":"213","suppressedMessages":"214","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"215","messages":"216","suppressedMessages":"217","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"E:\\Document\\Coding\\SecScore\\eslint.config.mjs",[],[],"E:\\Document\\Coding\\SecScore\\src\\App.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\ClientContext.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\BoardManager.tsx",["218","219"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ContentArea.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\EventHistory.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Home.tsx",["220","221","222","223","224","225"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Leaderboard.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OAuthCallback.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OAuthLogin.tsx",["226"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBE.tsx",["227","228"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBEBackground.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ReasonManager.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\RewardExchange.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\RewardSettings.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ScoreManager.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Settings.tsx",["229"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\SettlementHistory.tsx",["230","231"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Sidebar.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\StudentManager.tsx",["232"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\TagEditorDialog.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ThemeEditor.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ThemeQuickSettings.tsx",["233"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\TitleBar.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Versions.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\WindowControls.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Wizard.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\contexts\\ServiceContext.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeContext.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeEditorContext.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\env.d.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\hooks\\useResponsive.ts",["234"],[],"E:\\Document\\Coding\\SecScore\\src\\i18n\\index.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\main.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\preload\\types.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\react-19-patch.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\StudentService.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\shared\\kernel.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\shared\\mobileNavigation.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\utils\\color.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\utils\\studentAvatar.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\workers\\xlsxWorker.ts",[],[],"E:\\Document\\Coding\\SecScore\\vite.config.ts",[],[],{"ruleId":"235","severity":1,"message":"236","line":626,"column":6,"nodeType":"237","endLine":626,"endColumn":44,"suggestions":"238"},{"ruleId":"235","severity":1,"message":"236","line":634,"column":6,"nodeType":"237","endLine":634,"endColumn":59,"suggestions":"239"},{"ruleId":"235","severity":1,"message":"240","line":532,"column":6,"nodeType":"237","endLine":532,"endColumn":76,"suggestions":"241"},{"ruleId":"242","severity":2,"message":"243","line":544,"column":34,"nodeType":null,"endLine":556,"endColumn":4},{"ruleId":"242","severity":2,"message":"243","line":544,"column":34,"nodeType":null,"endLine":556,"endColumn":4},{"ruleId":"235","severity":1,"message":"244","line":556,"column":6,"nodeType":"237","endLine":556,"endColumn":15,"suggestions":"245"},{"ruleId":"242","severity":2,"message":"246","line":1627,"column":5,"nodeType":null,"endLine":1650,"endColumn":6},{"ruleId":"235","severity":1,"message":"247","line":1710,"column":6,"nodeType":"237","endLine":1710,"endColumn":8,"suggestions":"248"},{"ruleId":"235","severity":1,"message":"249","line":119,"column":6,"nodeType":"237","endLine":119,"endColumn":8,"suggestions":"250"},{"ruleId":"235","severity":1,"message":"251","line":160,"column":6,"nodeType":"237","endLine":160,"endColumn":20,"suggestions":"252"},{"ruleId":"235","severity":1,"message":"253","line":334,"column":5,"nodeType":"237","endLine":334,"endColumn":27,"suggestions":"254"},{"ruleId":"235","severity":1,"message":"255","line":239,"column":6,"nodeType":"237","endLine":239,"endColumn":8,"suggestions":"256"},{"ruleId":"235","severity":1,"message":"257","line":54,"column":6,"nodeType":"237","endLine":54,"endColumn":18,"suggestions":"258"},{"ruleId":"235","severity":1,"message":"244","line":106,"column":5,"nodeType":"237","endLine":106,"endColumn":7,"suggestions":"259"},{"ruleId":"235","severity":1,"message":"260","line":1351,"column":6,"nodeType":"237","endLine":1351,"endColumn":50,"suggestions":"261"},{"ruleId":"235","severity":1,"message":"251","line":100,"column":6,"nodeType":"237","endLine":100,"endColumn":20,"suggestions":"262"},{"ruleId":"235","severity":1,"message":"263","line":25,"column":9,"nodeType":"264","endLine":25,"endColumn":70},"react-hooks/exhaustive-deps","React Hook useEffect has a missing dependency: 'activeBoard'. Either include it or remove the dependency array.","ArrayExpression",["265"],["266"],"React Hook useMemo has an unnecessary dependency: 'layoutType'. Either exclude it or remove the dependency array.",["267"],"react-hooks/preserve-manual-memoization","Compilation Skipped: Existing memoization could not be preserved\n\nReact Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `t`, but the source dependencies were [reasons]. Inferred different dependency than source.\n\nE:\\Document\\Coding\\SecScore\\src\\components\\Home.tsx:544:34\n 542 | }, [groupedStudents])\n 543 |\n> 544 | const groupedReasons = useMemo(() => {\n | ^^^^^^^\n> 545 | const groups: Record = {}\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n> 546 | reasons.forEach((r) => {\n …\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n> 555 | })\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n> 556 | }, [reasons])\n | ^^^^ Could not preserve existing manual memoization\n 557 |\n 558 | const selectedStudents = useMemo(() => {\n 559 | if (selectedStudentIds.length === 0) return []","React Hook useMemo has a missing dependency: 't'. Either include it or remove the dependency array.",["268"],"Compilation Skipped: Existing memoization could not be preserved\n\nReact Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.\n\nE:\\Document\\Coding\\SecScore\\src\\components\\Home.tsx:1627:5\n 1625 |\n 1626 | const handleNavAction = useCallback(\n> 1627 | (clientY: number) => {\n | ^^^^^^^^^^^^^^^^^^^^^^\n> 1628 | if (!navContainerRef.current) return\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n> 1629 | const rect = navContainerRef.current.getBoundingClientRect()\n …\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n> 1649 | }\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n> 1650 | },\n | ^^^^^^ Could not preserve existing memoization\n 1651 | [groupedStudents, quickNavLayout.itemSize, quickNavLayout.paddingY, triggerNavHaptic]\n 1652 | )\n 1653 |","React Hook useEffect has missing dependencies: 'onGlobalMouseMove' and 'onGlobalMouseUp'. Either include them or remove the dependency array.",["269"],"React Hook useEffect has a missing dependency: 'handleDeepLink'. Either include it or remove the dependency array.",["270"],"React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array.",["271"],"React Hook useCallback has missing dependencies: 'showOobeMessage' and 't'. Either include them or remove the dependency array.",["272"],"React Hook useEffect has a missing dependency: 'loadAll'. Either include it or remove the dependency array.",["273"],"React Hook useCallback has a missing dependency: 't'. Either include it or remove the dependency array.",["274"],["275"],"React Hook useMemo has missing dependencies: 'handleDelete', 'handleOpenAvatarEditor', 'handleOpenGroupEditor', and 'handleOpenTagEditor'. Either include them or remove the dependency array.",["276"],["277"],"The 'breakpoints' object makes the dependencies of useEffect Hook (at line 49) change on every render. Move it inside the useEffect callback. Alternatively, wrap the initialization of 'breakpoints' in its own useMemo() Hook.","VariableDeclarator",{"desc":"278","fix":"279"},{"desc":"280","fix":"281"},{"desc":"282","fix":"283"},{"desc":"284","fix":"285"},{"desc":"286","fix":"287"},{"desc":"288","fix":"289"},{"desc":"290","fix":"291"},{"desc":"292","fix":"293"},{"desc":"294","fix":"295"},{"desc":"296","fix":"297"},{"desc":"298","fix":"299"},{"desc":"300","fix":"301"},{"desc":"290","fix":"302"},"Update the dependencies array to be: [activeBoard, activeBoard.id, activeBoard.layout]",{"range":"303","text":"304"},"Update the dependencies array to be: [activeBoard, activeBoard.id, listConfigSignature, runAllInBoard]",{"range":"305","text":"306"},"Update the dependencies array to be: [sortedStudents, sortType, searchKeyword, getGroupName, t]",{"range":"307","text":"308"},"Update the dependencies array to be: [reasons, t]",{"range":"309","text":"310"},"Update the dependencies array to be: [onGlobalMouseMove, onGlobalMouseUp]",{"range":"311","text":"312"},"Update the dependencies array to be: [handleDeepLink]",{"range":"313","text":"314"},"Update the dependencies array to be: [currentTheme, t]",{"range":"315","text":"316"},"Update the dependencies array to be: [students, showOobeMessage, t]",{"range":"317","text":"318"},"Update the dependencies array to be: [loadAll]",{"range":"319","text":"320"},"Update the dependencies array to be: [messageApi, t]",{"range":"321","text":"322"},"Update the dependencies array to be: [t]",{"range":"323","text":"324"},"Update the dependencies array to be: [t, isMobile, canEdit, handleOpenTagEditor, handleOpenGroupEditor, handleOpenAvatarEditor, handleDelete]",{"range":"325","text":"326"},{"range":"327","text":"316"},[18048,18086],"[activeBoard, activeBoard.id, activeBoard.layout]",[18296,18349],"[activeBoard, activeBoard.id, listConfigSignature, runAllInBoard]",[16069,16139],"[sortedStudents, sortType, searchKeyword, getGroupName, t]",[16914,16923],"[reasons, t]",[53173,53175],"[onGlobalMouseMove, onGlobalMouseUp]",[3088,3090],"[handleDeepLink]",[4674,4688],"[currentTheme, t]",[10389,10411],"[students, showOobeMessage, t]",[7646,7648],"[loadAll]",[1697,1709],"[messageApi, t]",[3343,3345],"[t]",[45580,45624],"[t, isMobile, canEdit, handleOpenTagEditor, handleOpenGroupEditor, handleOpenAvatarEditor, handleDelete]",[3185,3199]] \ No newline at end of file +[{"E:\\Document\\Coding\\SecScore\\eslint.config.mjs":"1","E:\\Document\\Coding\\SecScore\\src\\App.tsx":"2","E:\\Document\\Coding\\SecScore\\src\\ClientContext.ts":"3","E:\\Document\\Coding\\SecScore\\src\\components\\BoardManager.tsx":"4","E:\\Document\\Coding\\SecScore\\src\\components\\ContentArea.tsx":"5","E:\\Document\\Coding\\SecScore\\src\\components\\EventHistory.tsx":"6","E:\\Document\\Coding\\SecScore\\src\\components\\Home.tsx":"7","E:\\Document\\Coding\\SecScore\\src\\components\\Leaderboard.tsx":"8","E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBE.tsx":"9","E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBEBackground.tsx":"10","E:\\Document\\Coding\\SecScore\\src\\components\\ReasonManager.tsx":"11","E:\\Document\\Coding\\SecScore\\src\\components\\RewardExchange.tsx":"12","E:\\Document\\Coding\\SecScore\\src\\components\\RewardSettings.tsx":"13","E:\\Document\\Coding\\SecScore\\src\\components\\ScoreManager.tsx":"14","E:\\Document\\Coding\\SecScore\\src\\components\\Settings.tsx":"15","E:\\Document\\Coding\\SecScore\\src\\components\\SettlementHistory.tsx":"16","E:\\Document\\Coding\\SecScore\\src\\components\\Sidebar.tsx":"17","E:\\Document\\Coding\\SecScore\\src\\components\\StudentManager.tsx":"18","E:\\Document\\Coding\\SecScore\\src\\components\\TagEditorDialog.tsx":"19","E:\\Document\\Coding\\SecScore\\src\\components\\ThemeEditor.tsx":"20","E:\\Document\\Coding\\SecScore\\src\\components\\ThemeQuickSettings.tsx":"21","E:\\Document\\Coding\\SecScore\\src\\components\\TitleBar.tsx":"22","E:\\Document\\Coding\\SecScore\\src\\components\\Versions.tsx":"23","E:\\Document\\Coding\\SecScore\\src\\components\\WindowControls.tsx":"24","E:\\Document\\Coding\\SecScore\\src\\components\\Wizard.tsx":"25","E:\\Document\\Coding\\SecScore\\src\\contexts\\ServiceContext.tsx":"26","E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeContext.tsx":"27","E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeEditorContext.tsx":"28","E:\\Document\\Coding\\SecScore\\src\\env.d.ts":"29","E:\\Document\\Coding\\SecScore\\src\\hooks\\useResponsive.ts":"30","E:\\Document\\Coding\\SecScore\\src\\i18n\\index.ts":"31","E:\\Document\\Coding\\SecScore\\src\\main.tsx":"32","E:\\Document\\Coding\\SecScore\\src\\preload\\types.ts":"33","E:\\Document\\Coding\\SecScore\\src\\react-19-patch.ts":"34","E:\\Document\\Coding\\SecScore\\src\\services\\StudentService.ts":"35","E:\\Document\\Coding\\SecScore\\src\\shared\\kernel.ts":"36","E:\\Document\\Coding\\SecScore\\src\\shared\\mobileNavigation.ts":"37","E:\\Document\\Coding\\SecScore\\src\\utils\\color.ts":"38","E:\\Document\\Coding\\SecScore\\src\\utils\\studentAvatar.ts":"39","E:\\Document\\Coding\\SecScore\\src\\workers\\xlsxWorker.ts":"40","E:\\Document\\Coding\\SecScore\\vite.config.ts":"41","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\ActionEditor.tsx":"42","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\AutoScoreUtils.ts":"43","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\IntervalValueCodec.ts":"44","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\IntervalValueWidget.tsx":"45","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\TriggerRuleBuilder.tsx":"46","E:\\Document\\Coding\\SecScore\\src\\components\\AutoScoreManager.tsx":"47","E:\\Document\\Coding\\SecScore\\src\\components\\OAuth\\OAuthCallback.tsx":"48","E:\\Document\\Coding\\SecScore\\src\\components\\OAuth\\OAuthLogin.tsx":"49"},{"size":1413,"mtime":1774084765386,"results":"50","hashOfConfig":"51"},{"size":39361,"mtime":1775224756063,"results":"52","hashOfConfig":"53"},{"size":124,"mtime":1774084765537,"results":"54","hashOfConfig":"53"},{"size":47714,"mtime":1774704827018,"results":"55","hashOfConfig":"53"},{"size":12989,"mtime":1774784139058,"results":"56","hashOfConfig":"53"},{"size":2044,"mtime":1774084765557,"results":"57","hashOfConfig":"53"},{"size":90881,"mtime":1774704827750,"results":"58","hashOfConfig":"53"},{"size":8101,"mtime":1774084765561,"results":"59","hashOfConfig":"53"},{"size":36271,"mtime":1774176288912,"results":"60","hashOfConfig":"53"},{"size":4203,"mtime":1774084765566,"results":"61","hashOfConfig":"53"},{"size":5983,"mtime":1774084765569,"results":"62","hashOfConfig":"53"},{"size":9360,"mtime":1774084765571,"results":"63","hashOfConfig":"53"},{"size":6151,"mtime":1774084765572,"results":"64","hashOfConfig":"53"},{"size":12301,"mtime":1774084765573,"results":"65","hashOfConfig":"53"},{"size":61661,"mtime":1775279883358,"results":"66","hashOfConfig":"53"},{"size":5950,"mtime":1774084765578,"results":"67","hashOfConfig":"53"},{"size":10480,"mtime":1774784139062,"results":"68","hashOfConfig":"53"},{"size":79058,"mtime":1774704830361,"results":"69","hashOfConfig":"53"},{"size":6331,"mtime":1774084765584,"results":"70","hashOfConfig":"53"},{"size":8079,"mtime":1774084765586,"results":"71","hashOfConfig":"53"},{"size":10954,"mtime":1774084765587,"results":"72","hashOfConfig":"53"},{"size":3858,"mtime":1774084765589,"results":"73","hashOfConfig":"53"},{"size":431,"mtime":1774084765590,"results":"74","hashOfConfig":"53"},{"size":2367,"mtime":1774084765592,"results":"75","hashOfConfig":"53"},{"size":1560,"mtime":1774084765594,"results":"76","hashOfConfig":"53"},{"size":362,"mtime":1774084765604,"results":"77","hashOfConfig":"53"},{"size":5985,"mtime":1774084765606,"results":"78","hashOfConfig":"53"},{"size":2824,"mtime":1774084765608,"results":"79","hashOfConfig":"53"},{"size":38,"mtime":1774084765611,"results":"80","hashOfConfig":"53"},{"size":2097,"mtime":1774084765613,"results":"81","hashOfConfig":"53"},{"size":1533,"mtime":1774084765616,"results":"82","hashOfConfig":"53"},{"size":6919,"mtime":1774704831300,"results":"83","hashOfConfig":"53"},{"size":21171,"mtime":1775279884708,"results":"84","hashOfConfig":"53"},{"size":1150,"mtime":1774084765630,"results":"85","hashOfConfig":"53"},{"size":667,"mtime":1774084765638,"results":"86","hashOfConfig":"53"},{"size":3061,"mtime":1774176077013,"results":"87","hashOfConfig":"53"},{"size":1810,"mtime":1774784139078,"results":"88","hashOfConfig":"53"},{"size":3256,"mtime":1774084765655,"results":"89","hashOfConfig":"53"},{"size":1115,"mtime":1774084765658,"results":"90","hashOfConfig":"53"},{"size":947,"mtime":1774084765660,"results":"91","hashOfConfig":"53"},{"size":709,"mtime":1774665241507,"results":"92","hashOfConfig":"53"},{"size":4473,"mtime":1775279880382,"results":"93","hashOfConfig":"53"},{"size":11159,"mtime":1775279880492,"results":"94","hashOfConfig":"53"},{"size":2126,"mtime":1775224756075,"results":"95","hashOfConfig":"53"},{"size":3055,"mtime":1775224756077,"results":"96","hashOfConfig":"53"},{"size":1749,"mtime":1775279880567,"results":"97","hashOfConfig":"53"},{"size":13679,"mtime":1775279880826,"results":"98","hashOfConfig":"53"},{"size":1047,"mtime":1775224756086,"results":"99","hashOfConfig":"53"},{"size":6013,"mtime":1775279164215,"results":"100","hashOfConfig":"53"},{"filePath":"101","messages":"102","suppressedMessages":"103","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"oyxevq",{"filePath":"104","messages":"105","suppressedMessages":"106","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},"f6a32r",{"filePath":"107","messages":"108","suppressedMessages":"109","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"110","messages":"111","suppressedMessages":"112","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"113","messages":"114","suppressedMessages":"115","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"116","messages":"117","suppressedMessages":"118","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"119","messages":"120","suppressedMessages":"121","errorCount":3,"fatalErrorCount":0,"warningCount":3,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"122","messages":"123","suppressedMessages":"124","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"125","messages":"126","suppressedMessages":"127","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"128","messages":"129","suppressedMessages":"130","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"131","messages":"132","suppressedMessages":"133","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"134","messages":"135","suppressedMessages":"136","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"137","messages":"138","suppressedMessages":"139","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"140","messages":"141","suppressedMessages":"142","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"143","messages":"144","suppressedMessages":"145","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"146","messages":"147","suppressedMessages":"148","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"149","messages":"150","suppressedMessages":"151","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"152","messages":"153","suppressedMessages":"154","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"155","messages":"156","suppressedMessages":"157","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"158","messages":"159","suppressedMessages":"160","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"161","messages":"162","suppressedMessages":"163","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"164","messages":"165","suppressedMessages":"166","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"167","messages":"168","suppressedMessages":"169","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"170","messages":"171","suppressedMessages":"172","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"173","messages":"174","suppressedMessages":"175","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"176","messages":"177","suppressedMessages":"178","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"179","messages":"180","suppressedMessages":"181","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"182","messages":"183","suppressedMessages":"184","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"185","messages":"186","suppressedMessages":"187","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"188","messages":"189","suppressedMessages":"190","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"191","messages":"192","suppressedMessages":"193","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"194","messages":"195","suppressedMessages":"196","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"197","messages":"198","suppressedMessages":"199","errorCount":1,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"200","messages":"201","suppressedMessages":"202","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"203","messages":"204","suppressedMessages":"205","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"206","messages":"207","suppressedMessages":"208","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"209","messages":"210","suppressedMessages":"211","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"212","messages":"213","suppressedMessages":"214","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"215","messages":"216","suppressedMessages":"217","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"218","messages":"219","suppressedMessages":"220","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"221","messages":"222","suppressedMessages":"223","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"224","messages":"225","suppressedMessages":"226","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"227","messages":"228","suppressedMessages":"229","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"230","messages":"231","suppressedMessages":"232","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"233","messages":"234","suppressedMessages":"235","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"236","messages":"237","suppressedMessages":"238","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"239","messages":"240","suppressedMessages":"241","errorCount":0,"fatalErrorCount":0,"warningCount":2,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},{"filePath":"242","messages":"243","suppressedMessages":"244","errorCount":0,"fatalErrorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"245","messages":"246","suppressedMessages":"247","errorCount":0,"fatalErrorCount":0,"warningCount":1,"fixableErrorCount":0,"fixableWarningCount":0,"source":null},"E:\\Document\\Coding\\SecScore\\eslint.config.mjs",[],[],"E:\\Document\\Coding\\SecScore\\src\\App.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\ClientContext.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\BoardManager.tsx",["248","249"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ContentArea.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\EventHistory.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Home.tsx",["250","251","252","253","254","255"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Leaderboard.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBE.tsx",["256","257"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OOBE\\OOBEBackground.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ReasonManager.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\RewardExchange.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\RewardSettings.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ScoreManager.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Settings.tsx",["258"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\SettlementHistory.tsx",["259","260"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Sidebar.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\StudentManager.tsx",["261"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\TagEditorDialog.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ThemeEditor.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\ThemeQuickSettings.tsx",["262"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\TitleBar.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Versions.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\WindowControls.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\Wizard.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\contexts\\ServiceContext.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeContext.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\contexts\\ThemeEditorContext.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\env.d.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\hooks\\useResponsive.ts",["263"],[],"E:\\Document\\Coding\\SecScore\\src\\i18n\\index.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\main.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\preload\\types.ts",["264"],[],"E:\\Document\\Coding\\SecScore\\src\\react-19-patch.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\services\\StudentService.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\shared\\kernel.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\shared\\mobileNavigation.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\utils\\color.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\utils\\studentAvatar.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\workers\\xlsxWorker.ts",[],[],"E:\\Document\\Coding\\SecScore\\vite.config.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\ActionEditor.tsx",["265"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\AutoScoreUtils.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\IntervalValueCodec.ts",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\IntervalValueWidget.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScore\\TriggerRuleBuilder.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\AutoScoreManager.tsx",["266","267"],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OAuth\\OAuthCallback.tsx",[],[],"E:\\Document\\Coding\\SecScore\\src\\components\\OAuth\\OAuthLogin.tsx",["268"],[],{"ruleId":"269","severity":1,"message":"270","line":626,"column":6,"nodeType":"271","endLine":626,"endColumn":44,"suggestions":"272"},{"ruleId":"269","severity":1,"message":"270","line":634,"column":6,"nodeType":"271","endLine":634,"endColumn":59,"suggestions":"273"},{"ruleId":"269","severity":1,"message":"274","line":532,"column":6,"nodeType":"271","endLine":532,"endColumn":76,"suggestions":"275"},{"ruleId":"276","severity":2,"message":"277","line":544,"column":34,"nodeType":null,"endLine":556,"endColumn":4},{"ruleId":"276","severity":2,"message":"277","line":544,"column":34,"nodeType":null,"endLine":556,"endColumn":4},{"ruleId":"269","severity":1,"message":"278","line":556,"column":6,"nodeType":"271","endLine":556,"endColumn":15,"suggestions":"279"},{"ruleId":"276","severity":2,"message":"280","line":1627,"column":5,"nodeType":null,"endLine":1650,"endColumn":6},{"ruleId":"269","severity":1,"message":"281","line":1710,"column":6,"nodeType":"271","endLine":1710,"endColumn":8,"suggestions":"282"},{"ruleId":"269","severity":1,"message":"283","line":160,"column":6,"nodeType":"271","endLine":160,"endColumn":20,"suggestions":"284"},{"ruleId":"269","severity":1,"message":"285","line":334,"column":5,"nodeType":"271","endLine":334,"endColumn":27,"suggestions":"286"},{"ruleId":"269","severity":1,"message":"287","line":380,"column":6,"nodeType":"271","endLine":380,"endColumn":8,"suggestions":"288"},{"ruleId":"269","severity":1,"message":"289","line":54,"column":6,"nodeType":"271","endLine":54,"endColumn":18,"suggestions":"290"},{"ruleId":"269","severity":1,"message":"278","line":106,"column":5,"nodeType":"271","endLine":106,"endColumn":7,"suggestions":"291"},{"ruleId":"269","severity":1,"message":"292","line":1351,"column":6,"nodeType":"271","endLine":1351,"endColumn":50,"suggestions":"293"},{"ruleId":"269","severity":1,"message":"283","line":100,"column":6,"nodeType":"271","endLine":100,"endColumn":20,"suggestions":"294"},{"ruleId":"269","severity":1,"message":"295","line":25,"column":9,"nodeType":"296","endLine":25,"endColumn":70},{"ruleId":"297","severity":2,"message":"298","line":627,"column":38,"nodeType":"299","messageId":"300","endLine":627,"endColumn":43},{"ruleId":"269","severity":1,"message":"301","line":23,"column":9,"nodeType":"296","endLine":23,"endColumn":63},{"ruleId":"269","severity":1,"message":"278","line":74,"column":5,"nodeType":"271","endLine":74,"endColumn":55,"suggestions":"302"},{"ruleId":"269","severity":1,"message":"303","line":157,"column":6,"nodeType":"271","endLine":157,"endColumn":15,"suggestions":"304"},{"ruleId":"269","severity":1,"message":"305","line":131,"column":6,"nodeType":"271","endLine":131,"endColumn":8,"suggestions":"306"},"react-hooks/exhaustive-deps","React Hook useEffect has a missing dependency: 'activeBoard'. Either include it or remove the dependency array.","ArrayExpression",["307"],["308"],"React Hook useMemo has an unnecessary dependency: 'layoutType'. Either exclude it or remove the dependency array.",["309"],"react-hooks/preserve-manual-memoization","Compilation Skipped: Existing memoization could not be preserved\n\nReact Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected. The inferred dependency was `t`, but the source dependencies were [reasons]. Inferred different dependency than source.\n\nE:\\Document\\Coding\\SecScore\\src\\components\\Home.tsx:544:34\n 542 | }, [groupedStudents])\n 543 |\n> 544 | const groupedReasons = useMemo(() => {\n | ^^^^^^^\n> 545 | const groups: Record = {}\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n> 546 | reasons.forEach((r) => {\n …\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n> 555 | })\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n> 556 | }, [reasons])\n | ^^^^ Could not preserve existing manual memoization\n 557 |\n 558 | const selectedStudents = useMemo(() => {\n 559 | if (selectedStudentIds.length === 0) return []","React Hook useMemo has a missing dependency: 't'. Either include it or remove the dependency array.",["310"],"Compilation Skipped: Existing memoization could not be preserved\n\nReact Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. This value was memoized in source but not in compilation output.\n\nE:\\Document\\Coding\\SecScore\\src\\components\\Home.tsx:1627:5\n 1625 |\n 1626 | const handleNavAction = useCallback(\n> 1627 | (clientY: number) => {\n | ^^^^^^^^^^^^^^^^^^^^^^\n> 1628 | if (!navContainerRef.current) return\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n> 1629 | const rect = navContainerRef.current.getBoundingClientRect()\n …\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n> 1649 | }\n | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n> 1650 | },\n | ^^^^^^ Could not preserve existing memoization\n 1651 | [groupedStudents, quickNavLayout.itemSize, quickNavLayout.paddingY, triggerNavHaptic]\n 1652 | )\n 1653 |","React Hook useEffect has missing dependencies: 'onGlobalMouseMove' and 'onGlobalMouseUp'. Either include them or remove the dependency array.",["311"],"React Hook useEffect has a missing dependency: 't'. Either include it or remove the dependency array.",["312"],"React Hook useCallback has missing dependencies: 'showOobeMessage' and 't'. Either include them or remove the dependency array.",["313"],"React Hook useEffect has a missing dependency: 'loadAll'. Either include it or remove the dependency array.",["314"],"React Hook useCallback has a missing dependency: 't'. Either include it or remove the dependency array.",["315"],["316"],"React Hook useMemo has missing dependencies: 'handleDelete', 'handleOpenAvatarEditor', 'handleOpenGroupEditor', and 'handleOpenTagEditor'. Either include them or remove the dependency array.",["317"],["318"],"The 'breakpoints' object makes the dependencies of useEffect Hook (at line 49) change on every render. Move it inside the useEffect callback. Alternatively, wrap the initialization of 'breakpoints' in its own useMemo() Hook.","VariableDeclarator","@typescript-eslint/no-unused-vars","'_args' is defined but never used.","Identifier","unusedVar","The 'safeValue' conditional could make the dependencies of useMemo Hook (at line 58) change on every render. To fix this, wrap the initialization of 'safeValue' in its own useMemo() Hook.",["319"],"React Hook useEffect has missing dependencies: 'fetchRules', 'fetchStudents', and 'fetchTags'. Either include them or remove the dependency array.",["320"],"React Hook useEffect has a missing dependency: 'handleOAuthCallback'. Either include it or remove the dependency array.",["321"],{"desc":"322","fix":"323"},{"desc":"324","fix":"325"},{"desc":"326","fix":"327"},{"desc":"328","fix":"329"},{"desc":"330","fix":"331"},{"desc":"332","fix":"333"},{"desc":"334","fix":"335"},{"desc":"336","fix":"337"},{"desc":"338","fix":"339"},{"desc":"340","fix":"341"},{"desc":"342","fix":"343"},{"desc":"332","fix":"344"},{"desc":"345","fix":"346"},{"desc":"347","fix":"348"},{"desc":"349","fix":"350"},"Update the dependencies array to be: [activeBoard, activeBoard.id, activeBoard.layout]",{"range":"351","text":"352"},"Update the dependencies array to be: [activeBoard, activeBoard.id, listConfigSignature, runAllInBoard]",{"range":"353","text":"354"},"Update the dependencies array to be: [sortedStudents, sortType, searchKeyword, getGroupName, t]",{"range":"355","text":"356"},"Update the dependencies array to be: [reasons, t]",{"range":"357","text":"358"},"Update the dependencies array to be: [onGlobalMouseMove, onGlobalMouseUp]",{"range":"359","text":"360"},"Update the dependencies array to be: [currentTheme, t]",{"range":"361","text":"362"},"Update the dependencies array to be: [students, showOobeMessage, t]",{"range":"363","text":"364"},"Update the dependencies array to be: [loadAll]",{"range":"365","text":"366"},"Update the dependencies array to be: [messageApi, t]",{"range":"367","text":"368"},"Update the dependencies array to be: [t]",{"range":"369","text":"370"},"Update the dependencies array to be: [t, isMobile, canEdit, handleOpenTagEditor, handleOpenGroupEditor, handleOpenAvatarEditor, handleDelete]",{"range":"371","text":"372"},{"range":"373","text":"362"},"Update the dependencies array to be: [t, tagOptions]",{"range":"374","text":"375"},"Update the dependencies array to be: [canEdit, fetchRules, fetchStudents, fetchTags]",{"range":"376","text":"377"},"Update the dependencies array to be: [handleOAuthCallback]",{"range":"378","text":"379"},[18048,18086],"[activeBoard, activeBoard.id, activeBoard.layout]",[18296,18349],"[activeBoard, activeBoard.id, listConfigSignature, runAllInBoard]",[16069,16139],"[sortedStudents, sortType, searchKeyword, getGroupName, t]",[16914,16923],"[reasons, t]",[53173,53175],"[onGlobalMouseMove, onGlobalMouseUp]",[4674,4688],"[currentTheme, t]",[10389,10411],"[students, showOobeMessage, t]",[12150,12152],"[loadAll]",[1697,1709],"[messageApi, t]",[3343,3345],"[t]",[45580,45624],"[t, isMobile, canEdit, handleOpenTagEditor, handleOpenGroupEditor, handleOpenAvatarEditor, handleDelete]",[3185,3199],[1661,1711],"[t, tagOptions]",[4022,4031],"[canEdit, fetchRules, fetchStudents, fetchTags]",[3410,3412],"[handleOAuthCallback]"] \ No newline at end of file diff --git a/AUTH/API.MD b/AUTH/API.MD deleted file mode 100644 index fc62a7f..0000000 --- a/AUTH/API.MD +++ /dev/null @@ -1,922 +0,0 @@ -# SECTL Auth API 参考 - -本文档详细说明 SECTL Auth 提供的所有 API 接口。 - -## 基础信息 - -- **基础 URL**: `https://sectl.top` -- **认证方式**: OAuth 2.0 + Bearer Token -- **数据格式**: JSON - -## 接口概览 - -| 接口 | 方法 | 说明 | 对应页面 | -| ----------------------- | ---- | ------------------- | ------------------------ | -| `/oauth/authorize` | GET | 获取授权码 | OAuthAuthorizeView.vue | -| `/api/oauth/token` | POST | 换取/刷新令牌 | AuthCallbackView.vue | -| `/api/oauth/userinfo` | GET | 获取用户信息 | PlatformUsersView.vue | -| `/api/oauth/config` | POST | 配置 Token 过期时间 | PlatformSettingsView.vue | -| `/api/oauth/revoke` | POST | 撤销令牌 | SecurityView.vue | -| `/api/oauth/introspect` | POST | 验证令牌 | - | - ---- - -## OAuth 接口 - -### 1. 获取授权码 - -引导用户跳转到授权页面。 - -```http -GET /oauth/authorize -``` - -**请求参数:** - -| 参数名 | 类型 | 必填 | 说明 | -| ------------- | ------ | ---- | ---------------- | -| client_id | string | ✅ | 平台 ID | -| redirect_uri | string | ✅ | 回调 URL | -| response_type | string | ✅ | 固定值 `code` | -| state | string | ❌ | 防 CSRF 状态参数 | - -**示例请求:** - -``` -https://sectl.top/oauth/authorize?client_id=pf_AHmIhAiptyLFxk8x&redirect_uri=http://localhost:5000/callback&response_type=code -``` - -**成功响应:** - -- 重定向到 `redirect_uri?code={authorization_code}&state={state}` - -**错误响应:** - -- 重定向到 `redirect_uri?error={error_code}&error_description={description}` - -**错误码:** - -| 错误码 | 说明 | -| ------------------------- | ------------------ | -| invalid_request | 请求参数缺失或无效 | -| unauthorized_client | 客户端未授权 | -| access_denied | 用户拒绝授权 | -| unsupported_response_type | 不支持的响应类型 | -| invalid_scope | 无效的权限范围 | -| server_error | 服务器内部错误 | - -**相关页面:** - -- [OAuthAuthorizeView.vue](../../src/views/OAuthAuthorizeView.vue) - 授权页面 -- [LoginView.vue](../../src/views/LoginView.vue) - 登录页面 - ---- - -### 2. 获取访问令牌 - -使用授权码换取访问令牌。 - -```http -POST /api/oauth/token -``` - -**请求头:** - -``` -Content-Type: application/json -``` - -**请求体:** - -| 参数名 | 类型 | 必填 | 说明 | -| ------------- | ------ | ---- | ------------------------------------------------ | -| grant_type | string | ✅ | `authorization_code` 或 `refresh_token` | -| code | string | 条件 | 授权码(grant_type=authorization_code 时必填) | -| refresh_token | string | 条件 | 刷新令牌(grant_type=refresh_token 时必填) | -| client_id | string | ✅ | 平台 ID | -| client_secret | string | ✅ | 平台密钥 | -| redirect_uri | string | 条件 | 回调地址(grant_type=authorization_code 时必填) | - -**授权码模式示例:** - -```json -{ - "grant_type": "authorization_code", - "code": "NjliNDIyOTYwMDE4znLUNeWM6J8ye0MC", - "client_id": "pf_AHmIhAiptyLFxk8x", - "client_secret": "sk_UyMHuFf6oKXh8ZcHSb7lfOSgzumoOeg1", - "redirect_uri": "http://localhost:5000/callback" -} -``` - -**刷新令牌模式示例:** - -```json -{ - "grant_type": "refresh_token", - "refresh_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - "client_id": "pf_AHmIhAiptyLFxk8x", - "client_secret": "sk_UyMHuFf6oKXh8ZcHSb7lfOSgzumoOeg1" -} -``` - -**成功响应:** - -```json -{ - "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - "refresh_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - "token_type": "Bearer", - "expires_in": 3600 -} -``` - -**响应字段:** - -| 字段 | 类型 | 说明 | -| ------------- | ------ | ----------------------- | -| access_token | string | 访问令牌(JWT) | -| refresh_token | string | 刷新令牌 | -| token_type | string | 令牌类型,固定为 Bearer | -| expires_in | number | 过期时间(秒) | - -**错误响应:** - -```json -{ - "error": "invalid_grant", - "error_description": "Authorization code has expired" -} -``` - -**错误码:** - -| 错误码 | HTTP 状态 | 说明 | -| ---------------------- | --------- | ------------------------ | -| invalid_request | 400 | 请求格式错误 | -| invalid_client | 401 | 客户端认证失败 | -| invalid_grant | 400 | 授权码无效或过期 | -| unauthorized_client | 400 | 客户端无权使用此授权类型 | -| unsupported_grant_type | 400 | 不支持的授权类型 | -| invalid_scope | 400 | 无效的权限范围 | - -**相关页面:** - -- [AuthCallbackView.vue](../../src/views/AuthCallbackView.vue) - 回调处理 - ---- - -### 3. 获取用户信息 - -使用访问令牌获取用户信息。 - -```http -GET /api/oauth/userinfo -``` - -**请求头:** - -``` -Authorization: Bearer {access_token} -``` - -**成功响应:** - -```json -{ - "user_id": "69bd422960018cf4d0e5", - "email": "user@example.com", - "name": "张三", - "github_username": "zhangsan", - "permission": 1 -} -``` - -**响应字段:** - -| 字段 | 类型 | 说明 | -| --------------- | ------ | --------------------- | -| user_id | string | 用户唯一标识 | -| email | string | 用户邮箱 | -| name | string | 用户名称 | -| github_username | string | GitHub 用户名(如有) | -| permission | number | 用户权限等级 | - -**错误响应:** - -```json -{ - "error": "invalid_token", - "error_description": "The access token is invalid or has expired" -} -``` - -**相关页面:** - -- [PlatformUsersView.vue](../../src/views/dashboard/manage/PlatformUsersView.vue) - 平台用户列表 - ---- - -### 4. 配置 Token 过期时间 - -配置平台的 access_token 过期时间(需要管理员权限)。 - -```http -POST /api/oauth/config -``` - -**请求头:** - -``` -Content-Type: application/json -``` - -**请求体:** - -| 参数名 | 类型 | 必填 | 说明 | -| ------------ | ------ | ---- | ------------------------------------------- | -| platform_id | string | ✅ | 平台 ID | -| expires_in | number | ✅ | 过期时间(秒),-1 表示永不过期,最小 60 秒 | -| admin_secret | string | ✅ | 管理员密钥 | - -**请求示例:** - -```json -{ - "platform_id": "pf_AHmIhAiptyLFxk8x", - "expires_in": 86400, - "admin_secret": "your-admin-secret" -} -``` - -**成功响应:** - -```json -{ - "success": true, - "message": "Token expiration time updated", - "expires_in": 86400 -} -``` - -**错误响应:** - -```json -{ - "error": "unauthorized", - "error_description": "Invalid admin secret" -} -``` - -**相关页面:** - -- [PlatformSettingsView.vue](../../src/views/dashboard/other/PlatformSettingsView.vue) - 平台设置 - ---- - -### 5. 撤销令牌 - -撤销指定的访问令牌或刷新令牌。 - -```http -POST /api/oauth/revoke -``` - -**请求头:** - -``` -Content-Type: application/json -``` - -**请求体:** - -| 参数名 | 类型 | 必填 | 说明 | -| --------------- | ------ | ---- | --------------------------------- | -| token | string | ✅ | 要撤销的令牌 | -| token_type_hint | string | ❌ | `access_token` 或 `refresh_token` | -| client_id | string | ✅ | 平台 ID | -| client_secret | string | ✅ | 平台密钥 | - -**请求示例:** - -```json -{ - "token": "eyJhbGciOiJIUzI1NiIs...", - "token_type_hint": "access_token", - "client_id": "pf_AHmIhAiptyLFxk8x", - "client_secret": "sk_UyMHuFf6oKXh8ZcHSb7lfOSgzumoOeg1" -} -``` - -**成功响应:** - -```json -{ - "success": true, - "message": "Token revoked successfully" -} -``` - -**相关页面:** - -- [SecurityView.vue](../../src/views/dashboard/other/SecurityView.vue) - 安全设置 -- [LoginPlatformsView.vue](../../src/views/dashboard/list/LoginPlatformsView.vue) - 登录平台列表 - ---- - -### 6. 验证令牌 - -验证访问令牌的有效性和获取令牌信息。 - -```http -POST /api/oauth/introspect -``` - -**请求头:** - -``` -Content-Type: application/json -``` - -**请求体:** - -| 参数名 | 类型 | 必填 | 说明 | -| ------------- | ------ | ---- | ------------ | -| token | string | ✅ | 要验证的令牌 | -| client_id | string | ✅ | 平台 ID | -| client_secret | string | ✅ | 平台密钥 | - -**请求示例:** - -```json -{ - "token": "eyJhbGciOiJIUzI1NiIs...", - "client_id": "pf_AHmIhAiptyLFxk8x", - "client_secret": "sk_UyMHuFf6oKXh8ZcHSb7lfOSgzumoOeg1" -} -``` - -**活跃令牌响应:** - -```json -{ - "active": true, - "user_id": "69bd422960018cf4d0e5", - "client_id": "pf_AHmIhAiptyLFxk8x", - "exp": 1704783600, - "iat": 1704780000 -} -``` - -**非活跃令牌响应:** - -```json -{ - "active": false -} -``` - -**响应字段:** - -| 字段 | 类型 | 说明 | -| --------- | ------- | ------------------------ | -| active | boolean | 令牌是否有效 | -| user_id | string | 用户 ID(仅活跃令牌) | -| client_id | string | 平台 ID(仅活跃令牌) | -| exp | number | 过期时间戳(仅活跃令牌) | -| iat | number | 签发时间戳(仅活跃令牌) | - ---- - -## 平台管理 API - -以下 API 用于管理平台信息,需要通过 Appwrite SDK 调用。 - -### 获取平台列表 - -```javascript -// 使用 Appwrite SDK -const result = await databases.listDocuments({ - databaseId: "69bd89d8000304c37368", - collectionId: "platforms", - queries: [Query.orderDesc("$createdAt"), Query.limit(100)], -}) -``` - -**相关页面:** - -- [PlatformManagementView.vue](../../src/views/dashboard/manage/PlatformManagementView.vue) - -### 获取平台详情 - -```javascript -const platform = await databases.getDocument({ - databaseId: "69bd89d8000304c37368", - collectionId: "platforms", - documentId: "platform-document-id", -}) -``` - -**相关页面:** - -- [PlatformDetailView.vue](../../src/views/dashboard/detail/PlatformDetailView.vue) - -### 更新平台信息 - -```javascript -await databases.updateDocument({ - databaseId: "69bd89d8000304c37368", - collectionId: "platforms", - documentId: "platform-document-id", - data: { - callback_url: "http://new-callback.com", - token_expires_in: 7200, - }, -}) -``` - -**相关页面:** - -- [PlatformSettingsView.vue](../../src/views/dashboard/other/PlatformSettingsView.vue) - -### 申请创建平台 - -```javascript -await databases.createDocument({ - databaseId: "69bd89d8000304c37368", - collectionId: "platform_applications", - documentId: ID.unique(), - data: { - name: "我的应用", - description: "应用描述", - callback_url: "http://localhost:5000/callback", - owner_id: currentUser.$id, - status: "pending", - created_at: new Date().toISOString(), - }, -}) -``` - -**相关页面:** - -- [PlatformApplyView.vue](../../src/views/dashboard/form/PlatformApplyView.vue) - ---- - -## 用户管理 API - -### 获取用户授权的平台列表 - -```javascript -const result = await databases.listDocuments({ - databaseId: "69bd89d8000304c37368", - collectionId: "oauth_tokens", - queries: [Query.equal("user_id", currentUser.$id), Query.equal("revoked", false)], -}) -``` - -**相关页面:** - -- [LoginPlatformsView.vue](../../src/views/dashboard/list/LoginPlatformsView.vue) - -### 获取平台授权用户列表 - -```javascript -const result = await databases.listDocuments({ - databaseId: "69bd89d8000304c37368", - collectionId: "oauth_tokens", - queries: [Query.equal("platform_id", "pf_xxx"), Query.equal("revoked", false)], -}) -``` - -**相关页面:** - -- [PlatformUsersView.vue](../../src/views/dashboard/manage/PlatformUsersView.vue) - ---- - -## 云存储 API - -云存储 API 提供文件上传、下载、管理等功能。需要通过 OAuth access_token 进行认证。 - -### 接口概览 - -| 接口 | 方法 | 说明 | SDK 方法 | -| ------------------------------- | ------ | ------------------------ | ------------------------------- | -| `/api/cloud/upload` | POST | 上传文件 | `uploadFile()` | -| `/api/cloud/files/{file_id}` | GET | 获取文件信息 | `getFile()` | -| `/api/cloud/download/{file_id}` | GET | 获取下载链接 | `downloadFile()` | -| `/api/cloud/view/{file_id}` | GET | 查看文件内容 | `viewFile()` | -| `/api/cloud/preview/{file_id}` | GET | 预览文件(支持图片处理) | `previewFile()` | -| `/api/cloud/info` | GET | 获取存储信息 | `getStorageInfo()` | -| `/api/cloud/files` | GET | 列出/搜索文件 | `listFiles()` / `searchFiles()` | -| `/api/cloud/files/{file_id}` | PUT | 更新文件(重命名) | `updateFile()` | -| `/api/cloud/files/{file_id}` | DELETE | 删除文件 | `deleteFile()` | - ---- - -### 1. 上传文件 - -上传文件到云存储,系统会自动验证文件格式和检查存储容量。 - -```http -POST /api/cloud/upload -``` - -**请求头:** - -``` -Authorization: Bearer {access_token} -Content-Type: multipart/form-data -``` - -**请求参数:** - -| 参数名 | 类型 | 必填 | 说明 | -| ----------- | ------ | ---- | ------------ | -| file | File | ✅ | 要上传的文件 | -| platform_id | string | ✅ | 平台 ID | - -**请求示例(JavaScript):** - -```javascript -const formData = new FormData() -formData.append("file", fileInput.files[0]) -formData.append("platform_id", "pf_AHmIhAiptyLFxk8x") - -const response = await fetch("https://sectl.top/api/cloud/upload", { - method: "POST", - headers: { - Authorization: "Bearer eyJhbGciOiJIUzI1NiIs...", - }, - body: formData, -}) -``` - -**成功响应:** - -```json -{ - "success": true, - "file_id": "cf_xxxxxxxx", - "filename": "example.jpg", - "size": 1024567, - "url": "https://cloud.appwrite.io/v1/storage/buckets/69c869010007b752a9b1/files/xxx/view", - "storage": { - "used": "50MB", - "limit": "100MB", - "available": "50MB", - "percentage": 50 - } -} -``` - -**错误响应 - 格式不允许:** - -```json -{ - "error": "invalid_format", - "error_description": "File format 'exe' is not allowed", - "allowed_formats": ["jpg", "png", "pdf"] -} -``` - -**错误响应 - 超出容量:** - -```json -{ - "error": "storage_exceeded", - "error_description": "Upload would exceed storage limit", - "storage": { - "used": "95MB", - "limit": "100MB", - "available": "5MB", - "requested": "10MB" - } -} -``` - -**相关页面:** - -- [CloudServiceView.vue](../../src/views/dashboard/CloudServiceView.vue) - 云服务总览 - ---- - -### 2. 下载文件 - -获取文件的下载链接。 - -```http -GET /api/cloud/download/{file_id} -``` - -**请求头:** - -``` -Authorization: Bearer {access_token} -``` - -**路径参数:** - -| 参数名 | 类型 | 必填 | 说明 | -| ------- | ------ | ---- | ------- | -| file_id | string | ✅ | 文件 ID | - -**成功响应:** - -```json -{ - "success": true, - "file_id": "cf_xxxxxxxx", - "filename": "example.jpg", - "download_url": "https://cloud.appwrite.io/v1/storage/buckets/69c869010007b752a9b1/files/xxx/download", - "expires_at": "2025-01-01T01:00:00Z" -} -``` - ---- - -### 3. 获取存储信息 - -获取用户在该平台的存储使用情况和配置信息。 - -```http -GET /api/cloud/info?platform_id={platform_id} -``` - -**请求头:** - -``` -Authorization: Bearer {access_token} -``` - -**查询参数:** - -| 参数名 | 类型 | 必填 | 说明 | -| ----------- | ------ | ---- | ------- | -| platform_id | string | ✅ | 平台 ID | - -**成功响应:** - -```json -{ - "success": true, - "platform_id": "pf_xxx", - "tier": "free", - "storage": { - "used": 52428800, - "used_formatted": "50MB", - "limit": 104857600, - "limit_formatted": "100MB", - "available": 52428800, - "available_formatted": "50MB", - "percentage": 50 - }, - "allowed_formats": ["jpg", "png", "pdf"], - "max_file_size": 10485760 -} -``` - ---- - -### 4. 列出文件 - -获取用户在该平台的文件列表。 - -```http -GET /api/cloud/files?platform_id={platform_id}&limit=20&offset=0 -``` - -**请求头:** - -``` -Authorization: Bearer {access_token} -``` - -**查询参数:** - -| 参数名 | 类型 | 必填 | 说明 | -| ----------- | ------ | ---- | ----------------- | -| platform_id | string | ✅ | 平台 ID | -| limit | number | ❌ | 每页数量,默认 20 | -| offset | number | ❌ | 偏移量,默认 0 | - -**成功响应:** - -```json -{ - "success": true, - "files": [ - { - "file_id": "cf_xxx", - "filename": "example.jpg", - "size": 1024567, - "size_formatted": "1MB", - "created_at": "2025-01-01T00:00:00Z" - } - ], - "total": 10, - "limit": 20, - "offset": 0 -} -``` - ---- - -### 5. 删除文件 - -删除指定的文件。 - -```http -DELETE /api/cloud/files/{file_id} -``` - -**请求头:** - -``` -Authorization: Bearer {access_token} -``` - -**路径参数:** - -| 参数名 | 类型 | 必填 | 说明 | -| ------- | ------ | ---- | ------- | -| file_id | string | ✅ | 文件 ID | - -**成功响应:** - -```json -{ - "success": true, - "message": "File deleted successfully", - "storage": { - "used": "45MB", - "limit": "100MB", - "available": "55MB" - } -} -``` - ---- - -## 云存储 SDK - -我们提供了 JavaScript SDK 简化云存储 API 的调用。 - -### 安装 - -```html - -``` - -或使用 ES6 模块: - -```javascript -import CloudStorage from "./cloud-storage.js" -``` - -### 初始化 - -```javascript -const cloud = new CloudStorage({ - baseUrl: "https://sectl.top", - platformId: "your_platform_id", - accessToken: "your_access_token", -}) -``` - -### 使用示例 - -```javascript -// 上传文件 -try { - const result = await cloud.uploadFile(fileInput.files[0]) - console.log("文件上传成功:", result.file_id) - console.log("存储使用情况:", result.storage.percentage + "%") -} catch (error) { - if (error.code === "invalid_format") { - console.error("不支持的文件格式:", error.data.allowed_formats) - } else if (error.code === "storage_exceeded") { - console.error("存储空间不足") - } -} - -// 获取存储信息 -const info = await cloud.getStorageInfo() -console.log("已用空间:", info.storage.used_formatted) -console.log("允许格式:", info.allowed_formats) - -// 列出文件 -const files = await cloud.listFiles({ limit: 10 }) -files.files.forEach((file) => { - console.log(file.filename, file.size_formatted) -}) - -// 下载文件 -const download = await cloud.downloadFile("cf_xxx") -window.location.href = download.download_url - -// 删除文件 -await cloud.deleteFile("cf_xxx") -``` - -**相关文件:** - -- [cloud-storage.js](../../platform-sdk/cloud-storage.js) - SDK 源码 - ---- - -## 远程退登 API - -### 创建退登事件 - -当用户退出登录时,系统会自动创建退登事件。 - -```javascript -await databases.createDocument({ - databaseId: "69bd89d8000304c37368", - collectionId: "logout_events", - documentId: ID.unique(), - data: { - user_id: currentUser.$id, - platform_id: null, // null 表示全局退出 - global: true, - created_at: new Date().toISOString(), - }, -}) -``` - -### 订阅退登事件 - -使用 Appwrite Realtime 订阅退登事件: - -```javascript -import { Client } from "appwrite" - -const client = new Client() - .setEndpoint("https://cloud.appwrite.io/v1") - .setProject("your-project-id") - -const unsubscribe = client.subscribe( - "databases.69bd89d8000304c37368.collections.logout_events.documents", - (response) => { - const event = response.payload - if (event.user_id === currentUserId) { - // 执行本地退出 - localLogout() - } - } -) -``` - -**相关页面:** - -- [SecurityView.vue](../../src/views/dashboard/other/SecurityView.vue) - ---- - -## 错误处理 - -### HTTP 状态码 - -| 状态码 | 说明 | -| ------ | ------------------ | -| 200 | 请求成功 | -| 400 | 请求参数错误 | -| 401 | 未授权,认证失败 | -| 403 | 禁止访问,权限不足 | -| 404 | 资源不存在 | -| 500 | 服务器内部错误 | - -### 错误响应格式 - -```json -{ - "error": "error_code", - "error_description": "详细的错误描述", - "error_uri": "https://docs.example.com/error/error_code" -} -``` - ---- - -## 安全建议 - -1. **使用 HTTPS** - 所有 API 通信必须使用 HTTPS -2. **保护密钥** - 不要在客户端代码中暴露 client_secret -3. **验证回调地址** - 确保 redirect_uri 与注册时一致 -4. **使用状态参数** - 添加 state 参数防止 CSRF 攻击 -5. **安全存储令牌** - Token 应安全存储,避免泄露 -6. **及时撤销** - 用户退出时及时撤销令牌 - ---- - -## 相关页面 - -- [OAuthAuthorizeView.vue](../../src/views/OAuthAuthorizeView.vue) - OAuth 授权页面 -- [AuthCallbackView.vue](../../src/views/AuthCallbackView.vue) - 认证回调 -- [PlatformManagementView.vue](../../src/views/dashboard/manage/PlatformManagementView.vue) - 平台管理 -- [PlatformDetailView.vue](../../src/views/dashboard/detail/PlatformDetailView.vue) - 平台详情 -- [PlatformSettingsView.vue](../../src/views/dashboard/other/PlatformSettingsView.vue) - 平台设置 -- [SecurityView.vue](../../src/views/dashboard/other/SecurityView.vue) - 安全设置 -- [LoginPlatformsView.vue](../../src/views/dashboard/list/LoginPlatformsView.vue) - 登录平台列表 diff --git a/AUTH/AUTH.MD b/AUTH/AUTH.MD deleted file mode 100644 index d77c64b..0000000 --- a/AUTH/AUTH.MD +++ /dev/null @@ -1,482 +0,0 @@ -# 平台接入指南 - -本文档介绍如何将第三方平台与 SECTL Auth 集成,实现统一认证和远程退登功能。 - -## 目录 - -- [概述](#概述) -- [快速开始](#快速开始) -- [管理后台功能](#管理后台功能) -- [OAuth 集成](#oauth-集成) -- [远程退登](#远程退登) -- [代码示例](#代码示例) -- [常见问题](#常见问题) - -## 概述 - -SECTL Auth 提供以下核心功能: - -1. **统一认证** - 用户使用 SECTL Auth 账号登录第三方平台 -2. **远程退登** - 用户在 SECTL Auth 中心退出后,平台自动退出 -3. **用户管理** - 查看已授权用户、统计信息等 - -### 工作流程 - -``` -┌─────────────┐ 登录请求 ┌─────────────┐ -│ 用户 │ ─────────────> │ SECTL Auth │ -│ │ │ │ -│ 平台A │ <───────────── │ 认证中心 │ -│ │ Access Token │ │ -└──────┬──────┘ └──────┬──────┘ - │ │ - │ 远程退登通知 │ - │ <─────────────────────────── │ - │ (Realtime) │ - ▼ ▼ - 自动退出登录 用户点击退出 -``` - -## 快速开始 - -### 1. 注册平台 - -在 SECTL Auth 平台管理页面 ([PlatformManagementView.vue](../../src/views/dashboard/manage/PlatformManagementView.vue)) 申请接入,获取: - -| 参数 | 说明 | 示例 | -| -------------- | ------------ | ------------------------------------- | -| platform_id | 平台唯一标识 | `pf_AHmIhAiptyLFxk8x` | -| platform_token | 平台认证密钥 | `sk_UyMHuFf6oKXh8ZcHSb7lfOSgzumoOeg1` | -| callback_url | 回调地址 | `http://localhost:5000/callback` | - -### 2. 平台状态说明 - -平台在管理后台有以下状态: - -| 状态 | 说明 | 可操作 | -| -------- | ------ | -------- | -| active | 运行中 | 正常授权 | -| pending | 审核中 | 等待审核 | -| disabled | 已停用 | 无法授权 | - -### 3. 查看统计信息 - -在平台管理页面可以查看: - -- **总平台数** - 已创建的平台数量 -- **运行中** - 活跃平台数量 -- **审核中** - 待审核申请数量 -- **总用户数** - 授权用户总数 - -## 管理后台功能 - -### 平台列表 ([PlatformManagementView.vue](../../src/views/dashboard/manage/PlatformManagementView.vue)) - -平台管理页面提供以下功能: - -#### 1. 平台卡片展示 - -每个平台显示: - -- 平台名称和描述 -- 状态标签(运行中/审核中/已停用) -- 创建时间 -- 授权用户数 -- 管理按钮 - -#### 2. 搜索和过滤 - -- 按名称搜索平台 -- 按状态过滤 -- 分页展示 - -#### 3. 快捷操作 - -- **新建平台** - 跳转到申请页面 -- **我的申请** - 查看自己的平台申请 -- **审核申请** - 审核其他用户的申请(需 >=18 权限) - -### 平台详情 ([PlatformDetailView.vue](../../src/views/dashboard/detail/PlatformDetailView.vue)) - -查看平台详细信息: - -#### 基本信息 - -- 平台 ID (Client ID) -- 平台密钥 (Client Secret) -- 回调地址 -- 创建时间 - -#### 安全设置 - -- Token 过期时间配置 -- 密钥重置 -- 回调地址修改 - -#### 用户统计 - -- 授权用户列表 -- 登录时间 -- 最后活跃时间 - -### 平台申请 ([PlatformApplyView.vue](../../src/views/dashboard/form/PlatformApplyView.vue)) - -申请创建新平台: - -```javascript -// 申请参数 -{ - name: "我的应用", // 平台名称 - description: "应用描述", // 平台描述 - callback_url: "http://..." // 回调地址 -} -``` - -申请后状态为 `pending`,等待管理员审核。 - -### 申请审核 ([PlatformApplicationsView.vue](../../src/views/dashboard/manage/PlatformApplicationsView.vue)) - -管理员审核平台申请: - -- 查看申请详情 -- 批准申请(状态变为 `active`) -- 拒绝申请(状态变为 `disabled`) - -## OAuth 集成 - -### 授权流程 - -#### 1. 引导用户到授权页面 - -``` -GET https://sectl.top/oauth/authorize -``` - -**参数:** - -| 参数名 | 必填 | 说明 | -| ------------- | ---- | ------------- | -| client_id | ✅ | 平台 ID | -| redirect_uri | ✅ | 回调地址 | -| response_type | ✅ | 固定值 `code` | - -**示例:** - -``` -https://sectl.top/oauth/authorize?client_id=pf_xxx&redirect_uri=http://localhost:5000/callback&response_type=code -``` - -#### 2. 处理回调 - -用户授权后,会重定向到: - -``` -{callback_url}?code={authorization_code} -``` - -#### 3. 换取访问令牌 - -```http -POST https://sectl.top/api/oauth/token -Content-Type: application/json - -{ - "grant_type": "authorization_code", - "code": "授权码", - "client_id": "pf_xxx", - "client_secret": "sk_xxx", - "redirect_uri": "http://localhost:5000/callback" -} -``` - -**响应:** - -```json -{ - "access_token": "eyJhbGciOiJIUzI1NiIs...", - "refresh_token": "xxx", - "token_type": "Bearer", - "expires_in": 3600 -} -``` - -#### 4. 获取用户信息 - -```http -GET https://sectl.top/api/oauth/userinfo -Authorization: Bearer {access_token} -``` - -**响应:** - -```json -{ - "user_id": "69bd422960018cf4d0e5", - "email": "user@example.com", - "name": "张三", - "github_username": "zhangsan" -} -``` - -## 远程退登 - -### 实现原理 - -1. 用户在 SECTL Auth 点击退出登录 -2. 系统创建 `logout_events` 文档 -3. 第三方应用通过 Appwrite Realtime 订阅该集合 -4. 收到匹配的事件后执行本地退出 - -### 集成步骤 - -#### 1. 安装 Appwrite SDK - -**C#** - -```bash -dotnet add package Appwrite -``` - -**Python** - -```bash -pip install appwrite -``` - -#### 2. 配置 Realtime 订阅 - -**C# 示例:** - -```csharp -using Appwrite; -using Appwrite.Services; - -var client = new Client() - .SetEndpoint("https://cloud.appwrite.io/v1") - .SetProject("your-project-id"); - -var realtime = new Realtime(client); - -// 订阅 logout_events 集合 -realtime.Subscribe("databases.{databaseId}.collections.logout_events.documents", - callback: (message) => { - var logoutEvent = message.Payload; - if (logoutEvent.UserId == currentUserId) { - // 执行本地退出 - LocalLogout(); - } - }); -``` - -**Python 示例:** - -```python -from appwrite.client import Client -from appwrite.services.realtime import Realtime - -client = Client() -client.set_endpoint('https://cloud.appwrite.io/v1') -client.set_project('your-project-id') - -realtime = Realtime(client) - -def on_logout_event(message): - event = message['payload'] - if event['user_id'] == current_user_id: - # 执行本地退出 - local_logout() - -# 订阅 logout_events 集合 -realtime.subscribe( - 'databases.{database_id}.collections.logout_events.documents', - callback=on_logout_event -) -``` - -### 数据结构 - -**logout_events 集合字段:** - -| 字段 | 类型 | 说明 | -| ----------- | ------- | ------------------ | -| user_id | String | 退出登录的用户ID | -| platform_id | String | 特定平台ID(可选) | -| global | Boolean | 是否全局退出 | -| created_at | String | 创建时间 | - -## 代码示例 - -### C# 完整示例 - -详见 [csharp-example](./csharp-example/) 目录。 - -```csharp -// SectlAuthClient.cs -public class SectlAuthClient -{ - private readonly string _platformId; - private readonly string _platformSecret; - private readonly string _callbackUrl; - private readonly HttpClient _httpClient; - - public SectlAuthClient(string platformId, string platformSecret, string callbackUrl) - { - _platformId = platformId; - _platformSecret = platformSecret; - _callbackUrl = callbackUrl; - _httpClient = new HttpClient(); - } - - // 获取授权 URL - public string GetAuthorizationUrl() - { - return $"https://sectl.top/oauth/authorize?" + - $"client_id={_platformId}&" + - $"redirect_uri={Uri.EscapeDataString(_callbackUrl)}&" + - $"response_type=code"; - } - - // 换取访问令牌 - public async Task ExchangeCodeAsync(string code) - { - var request = new - { - grant_type = "authorization_code", - code = code, - client_id = _platformId, - client_secret = _platformSecret, - redirect_uri = _callbackUrl - }; - - var response = await _httpClient.PostAsJsonAsync( - "https://sectl.top/api/oauth/token", request); - - return await response.Content.ReadFromJsonAsync(); - } - - // 获取用户信息 - public async Task GetUserInfoAsync(string accessToken) - { - _httpClient.DefaultRequestHeaders.Authorization = - new AuthenticationHeaderValue("Bearer", accessToken); - - var response = await _httpClient.GetAsync( - "https://sectl.top/api/oauth/userinfo"); - - return await response.Content.ReadFromJsonAsync(); - } -} -``` - -### Python 完整示例 - -详见 [python-example](./python-example/) 目录。 - -```python -# sectl_auth/client.py -import requests - -class SectlAuthClient: - BASE_URL = "https://sectl.top" - AUTH_URL = "https://sectl.top" - - def __init__(self, platform_id: str, platform_secret: str, callback_url: str): - self.platform_id = platform_id - self.platform_secret = platform_secret - self.callback_url = callback_url - - def get_authorization_url(self) -> str: - """获取授权 URL""" - return ( - f"{self.AUTH_URL}/oauth/authorize?" - f"client_id={self.platform_id}&" - f"redirect_uri={self.callback_url}&" - f"response_type=code" - ) - - def exchange_code(self, code: str) -> dict: - """用授权码换取访问令牌""" - response = requests.post( - f"{self.BASE_URL}/api/oauth/token", - json={ - "grant_type": "authorization_code", - "code": code, - "client_id": self.platform_id, - "client_secret": self.platform_secret, - "redirect_uri": self.callback_url - } - ) - response.raise_for_status() - return response.json() - - def get_user_info(self, access_token: str) -> dict: - """获取用户信息""" - response = requests.get( - f"{self.BASE_URL}/api/oauth/userinfo", - headers={"Authorization": f"Bearer {access_token}"} - ) - response.raise_for_status() - return response.json() -``` - -## 常见问题 - -### Q: 如何获取 platform_id 和 platform_token? - -A: 在 SECTL Auth 平台管理页面申请创建平台,审核通过后会获得: - -- platform_id (Client ID) -- platform_token (Client Secret) - -### Q: 授权码有效期多久? - -A: 授权码有效期为 10 分钟,且只能使用一次。 - -### Q: Access Token 过期后怎么办? - -A: 使用 refresh_token 换取新的 access_token: - -```http -POST /api/oauth/token -{ - "grant_type": "refresh_token", - "refresh_token": "your-refresh-token", - "client_id": "your-platform-id", - "client_secret": "your-platform-secret" -} -``` - -### Q: 如何配置 Token 过期时间? - -A: 在平台设置页面 ([PlatformSettingsView.vue](../../src/views/dashboard/other/PlatformSettingsView.vue)) 可以配置: - -- access_token 过期时间(默认 3600 秒) -- 设置为 -1 表示永不过期 - -### Q: 远程退登不生效怎么办? - -A: 检查以下几点: - -1. 是否正确订阅了 `logout_events` 集合 -2. 检查 user_id 是否匹配 -3. 确认 Appwrite Realtime 连接正常 -4. 查看浏览器控制台是否有错误 - -### Q: 平台状态显示"审核中"怎么办? - -A: 新创建的平台需要管理员审核。审核通过后状态会变为 `active`。 - -## 相关页面 - -- [PlatformManagementView.vue](../../src/views/dashboard/manage/PlatformManagementView.vue) - 平台管理 -- [PlatformDetailView.vue](../../src/views/dashboard/detail/PlatformDetailView.vue) - 平台详情 -- [PlatformApplyView.vue](../../src/views/dashboard/form/PlatformApplyView.vue) - 申请平台 -- [PlatformApplicationsView.vue](../../src/views/dashboard/manage/PlatformApplicationsView.vue) - 审核申请 -- [PlatformSettingsView.vue](../../src/views/dashboard/other/PlatformSettingsView.vue) - 平台设置 -- [LoginPlatformsView.vue](../../src/views/dashboard/list/LoginPlatformsView.vue) - 登录平台列表 - -## 参考文档 - -- [OAuth 2.0 流程](./OAUTH_FLOW.MD) -- [API 参考](./API_REFERENCE.md) -- [故障排查](../oauth-flow/TROUBLESHOOTING.md) diff --git a/AUTH/OAUTH_FLOW.MD b/AUTH/OAUTH_FLOW.MD deleted file mode 100644 index b3279b6..0000000 --- a/AUTH/OAUTH_FLOW.MD +++ /dev/null @@ -1,311 +0,0 @@ -# OAuth 2.0 授权流程文档 - -本文档详细说明 SECTL Auth 的 OAuth 2.0 授权流程,帮助第三方应用开发者接入统一认证服务。 - -## 概述 - -SECTL Auth 实现了标准的 OAuth 2.0 授权码流程(Authorization Code Flow),允许第三方应用使用 SECTL Auth 账户进行用户认证。 - -### 流程特点 - -- **安全性高** - 使用授权码模式,避免 Token 暴露在浏览器端 -- **标准化** - 遵循 OAuth 2.0 RFC 6749 规范 -- **远程退登** - 支持通过 Realtime 实现跨平台同步退出 -- **可配置** - 支持自定义 Token 过期时间 - -## 授权流程 - -### 流程图 - -``` -┌─────────┐ ┌─────────────┐ -│ 用户 │ │ 第三方应用 │ -└────┬────┘ └──────┬──────┘ - │ │ - │ 1. 访问应用 │ - │───────────────────────────────────────────────>│ - │ │ - │ 2. 重定向到授权页面 │ - │<───────────────────────────────────────────────│ - │ │ - │ 3. 登录并授权 (LoginView.vue / OAuthAuthorizeView.vue) - │───────────────────────────────────────────────>│ - │ │ - │ 4. 返回授权码 │ - │<───────────────────────────────────────────────│ - │ │ - │ 5. 用授权码换取 Token │ - │ │──────┐ - │ │ │ - │ 6. 返回 Access Token │<─────┘ - │ │ - │ 7. 使用 Token 访问用户资源 │ - │<───────────────────────────────────────────────│ -``` - -## 详细步骤 - -### 1. 引导用户到授权页面 - -当用户需要登录时,第三方应用应将用户重定向到 SECTL Auth 的授权页面。 - -**请求地址:** - -``` -GET https://sectl.top/oauth/authorize -``` - -**请求参数:** - -| 参数名 | 必填 | 类型 | 说明 | -| ------------- | ---- | ------ | --------------------------------- | -| client_id | ✅ | string | 平台 ID,如 `pf_AHmIhAiptyLFxk8x` | -| redirect_uri | ✅ | string | 回调地址,必须与平台设置匹配 | -| response_type | ✅ | string | 固定值 `code` | - -**示例 URL:** - -``` -https://sectl.top/oauth/authorize?client_id=pf_AHmIhAiptyLFxk8x&redirect_uri=http://localhost:5000/callback&response_type=code -``` - -**相关代码:** [OAuthAuthorizeView.vue](../../src/views/OAuthAuthorizeView.vue) - -### 2. 用户登录与授权 - -用户将被引导至 SECTL Auth 登录页面(如果未登录): - -1. **登录页面** ([LoginView.vue](../../src/views/LoginView.vue)) - - 用户输入邮箱密码或使用 GitHub 登录 - - 支持记住我功能 - - 检测 OAuth 参数,登录后自动跳转授权页 - -2. **授权确认页** ([OAuthAuthorizeView.vue](../../src/views/OAuthAuthorizeView.vue)) - - 显示应用名称和图标 - - 列出请求的权限范围 - - 用户可选择"授权"或"拒绝" - -**权限范围:** - -- 访问用户基本信息(用户名、邮箱、头像) -- 验证用户身份 - -### 3. 获取授权码 - -用户授权后,页面将重定向到指定的回调地址: - -``` -{redirect_uri}?code={authorization_code} -``` - -**示例:** - -``` -http://localhost:5000/callback?code=NjliNDIyOTYwMDE4znLUNeWM6J8ye0MC -``` - -**授权码特性:** - -- 有效期:10 分钟 -- 一次性使用 -- 与 `redirect_uri` 绑定 - -### 4. 换取访问令牌 - -第三方应用后端使用授权码向 SECTL Auth 换取访问令牌。 - -**请求地址:** - -``` -POST https://sectl.top/api/oauth/token -``` - -**请求头:** - -``` -Content-Type: application/json -``` - -**请求体:** - -```json -{ - "grant_type": "authorization_code", - "code": "NjliNDIyOTYwMDE4znLUNeWM6J8ye0MC", - "client_id": "pf_AHmIhAiptyLFxk8x", - "client_secret": "sk_UyMHuFf6oKXh8ZcHSb7lfOSgzumoOeg1", - "redirect_uri": "http://localhost:5000/callback" -} -``` - -**参数说明:** - -| 参数名 | 必填 | 说明 | -| ------------- | ---- | --------------------------- | -| grant_type | ✅ | 固定值 `authorization_code` | -| code | ✅ | 上一步获取的授权码 | -| client_id | ✅ | 平台 ID | -| client_secret | ✅ | 平台密钥 | -| redirect_uri | ✅ | 必须与授权时一致 | - -**成功响应:** - -```json -{ - "access_token": "eyJhbGciOiJIUzI1NiIs...", - "refresh_token": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - "token_type": "Bearer", - "expires_in": 3600 -} -``` - -**Token 信息:** - -| 字段 | 说明 | -| ------------- | ----------------------------------- | -| access_token | 访问令牌,用于调用 API | -| refresh_token | 刷新令牌,用于获取新的 access_token | -| token_type | 令牌类型,固定为 Bearer | -| expires_in | 过期时间(秒) | - -### 5. 使用访问令牌 - -获取 access_token 后,可用于调用 SECTL Auth API 获取用户信息。 - -**获取用户信息:** - -```http -GET https://sectl.top/api/oauth/userinfo -Authorization: Bearer {access_token} -``` - -**响应示例:** - -```json -{ - "user_id": "69bd422960018cf4d0e5", - "email": "user@example.com", - "name": "张三", - "github_username": "zhangsan" -} -``` - -## Token 管理 - -### 过期时间配置 - -平台可配置 Token 过期时间: - -| 令牌类型 | 默认有效期 | 说明 | -| ------------- | --------------- | ---------------------- | -| access_token | 3600 秒 (1小时) | 短期有效,用于访问资源 | -| refresh_token | 长期有效 | 用于刷新 access_token | -| 授权码 | 600 秒 (10分钟) | 一次性使用 | - -**修改过期时间:** - -```http -POST https://sectl.top/api/oauth/config -Content-Type: application/json - -{ - "expires_in": 86400, - "admin_secret": "your-admin-secret" -} -``` - -### 刷新 Token - -当 access_token 过期时,使用 refresh_token 获取新的令牌: - -```http -POST https://sectl.top/api/oauth/token -Content-Type: application/json - -{ - "grant_type": "refresh_token", - "refresh_token": "your-refresh-token", - "client_id": "your-platform-id", - "client_secret": "your-platform-secret" -} -``` - -## 远程退登 - -SECTL Auth 支持远程退登功能,当用户在 SECTL Auth 中心退出登录时,已授权的第三方应用会收到通知。 - -### 实现原理 - -1. 用户在 SECTL Auth 点击退出登录 -2. 系统创建 `logout_events` 文档 -3. 第三方应用通过 Appwrite Realtime 订阅该集合 -4. 收到退登事件后,应用执行本地退出操作 - -### 集成方式 - -详见 [平台接入指南](../platform-integration/README.md)。 - -## 错误处理 - -### 授权错误 - -当授权失败时,将重定向到回调地址并附带错误信息: - -``` -{redirect_uri}?error=access_denied&error_description=user+denied+authorization -``` - -**常见错误码:** - -| 错误码 | 说明 | -| ------------------------- | ------------------ | -| invalid_request | 请求参数缺失或无效 | -| unauthorized_client | 客户端未授权 | -| access_denied | 用户拒绝授权 | -| unsupported_response_type | 不支持的响应类型 | -| invalid_scope | 无效的权限范围 | -| server_error | 服务器内部错误 | - -### Token 错误 - -换取 Token 时的错误响应: - -```json -{ - "error": "invalid_grant", - "error_description": "Authorization code has expired" -} -``` - -**常见错误码:** - -| 错误码 | 说明 | -| ---------------------- | ------------------------ | -| invalid_request | 请求格式错误 | -| invalid_client | 客户端认证失败 | -| invalid_grant | 授权码无效或过期 | -| unauthorized_client | 客户端无权使用此授权类型 | -| unsupported_grant_type | 不支持的授权类型 | -| invalid_scope | 无效的权限范围 | - -## 安全建议 - -1. **使用 HTTPS** - 所有 OAuth 通信必须使用 HTTPS -2. **保护 client_secret** - 不要在客户端代码中暴露 client_secret -3. **验证 redirect_uri** - 确保回调地址与注册时一致 -4. **使用状态参数** - 建议添加 `state` 参数防止 CSRF 攻击 -5. **及时清理** - 使用后的授权码应立即作废 -6. **安全存储** - Token 应安全存储,避免泄露 - -## 相关页面 - -- [LoginView.vue](../../src/views/LoginView.vue) - 登录页面 -- [OAuthAuthorizeView.vue](../../src/views/OAuthAuthorizeView.vue) - OAuth 授权页面 -- [AuthCallbackView.vue](../../src/views/AuthCallbackView.vue) - 认证回调处理 -- [LoginPlatformsView.vue](../../src/views/dashboard/list/LoginPlatformsView.vue) - 登录平台列表 - -## 参考文档 - -- [平台接入指南](../platform-integration/README.md) -- [API 参考](../platform-integration/API_REFERENCE.md) -- [故障排查](./TROUBLESHOOTING.md) diff --git a/AUTH/TROUBLESHOOTING.md b/AUTH/TROUBLESHOOTING.md deleted file mode 100644 index c24753a..0000000 --- a/AUTH/TROUBLESHOOTING.md +++ /dev/null @@ -1,476 +0,0 @@ -# OAuth 流程故障排查指南 - -本文档汇总了 SECTL Auth OAuth 流程中的常见问题及解决方案。 - -## 目录 - -- [授权阶段问题](#授权阶段问题) -- [Token 换取问题](#token-换取问题) -- [用户信息获取问题](#用户信息获取问题) -- [远程退登问题](#远程退登问题) -- [平台管理问题](#平台管理问题) -- [网络与 CORS 问题](#网络与-cors-问题) - ---- - -## 授权阶段问题 - -### 1. 授权页面无法打开 - -**现象:** 访问授权 URL 返回 404 或空白页面 - -**排查步骤:** - -1. 检查 URL 格式是否正确 - ``` - https://auth.sectl.top/oauth/authorize?client_id=xxx&redirect_uri=xxx&response_type=code - ``` -2. 确认必要参数是否齐全 - - `client_id` - 平台 ID - - `redirect_uri` - 回调地址 - - `response_type` - 固定值 `code` -3. 检查平台是否存在且状态为 `active` - - 登录 SECTL Auth 管理后台 - - 查看 [PlatformManagementView.vue](../../src/views/dashboard/manage/PlatformManagementView.vue) - - 确认平台状态不是 `pending` 或 `disabled` - -**相关页面:** - -- [OAuthAuthorizeView.vue](../../src/views/OAuthAuthorizeView.vue) -- [PlatformManagementView.vue](../../src/views/dashboard/manage/PlatformManagementView.vue) - ---- - -### 2. 授权后没有记录到登录平台 - -**现象:** 用户授权后,在"登录平台"页面 ([LoginPlatformsView.vue](../../src/views/dashboard/list/LoginPlatformsView.vue)) 看不到记录 - -**排查步骤:** - -1. 检查浏览器控制台是否有错误 -2. 检查 `user_data` 集合是否有该用户的记录 -3. 检查 `user_data.login_platforms` 字段是否正确更新 -4. 检查 `oauth_tokens` 集合是否有对应的令牌记录 - -**可能原因:** - -- 用户未登录 SECTL Auth -- `user_data` 集合权限问题 -- 网络请求失败 -- 平台状态为 `disabled` - -**相关页面:** - -- [LoginPlatformsView.vue](../../src/views/dashboard/list/LoginPlatformsView.vue) -- [SecurityView.vue](../../src/views/dashboard/other/SecurityView.vue) - ---- - -### 3. 用户已登录但授权页面仍要求登录 - -**现象:** 用户已登录 SECTL Auth,但授权页面仍跳转到登录页 - -**排查步骤:** - -1. 检查登录状态是否正确保持 -2. 检查浏览器 Cookie 是否被阻止 -3. 检查是否跨域问题 - -**解决方案:** - -- 确保浏览器允许第三方 Cookie -- 检查 `rememberMe` 选项是否勾选 -- 查看 [LoginView.vue](../../src/views/LoginView.vue) 中的登录逻辑 - ---- - -## Token 换取问题 - -### 1. 换取 token 时提示 "Invalid authorization code" - -**现象:** 用 code 换取 token 返回 400 错误 - -```json -{ - "error": "invalid_grant", - "error_description": "Invalid authorization code" -} -``` - -**排查步骤:** - -1. 检查 code 是否已过期(10分钟有效期) -2. 检查 code 是否已被使用过 -3. 检查 `redirect_uri` 是否与授权时一致 -4. 检查 `client_id` 和 `client_secret` 是否正确 - -**可能原因:** - -- Code 已过期 -- Code 已被使用(一次性) -- 回调地址不匹配 -- 平台密钥错误 - -**相关代码:** [AuthCallbackView.vue](../../src/views/AuthCallbackView.vue) - ---- - -### 2. 换取 token 时提示 "Invalid client" - -**现象:** 返回 401 错误 - -```json -{ - "error": "invalid_client", - "error_description": "Client authentication failed" -} -``` - -**排查步骤:** - -1. 确认 `client_id` 正确 -2. 确认 `client_secret` 正确且未泄露 -3. 检查平台状态是否为 `active` - -**解决方案:** - -- 在 [PlatformManagementView.vue](../../src/views/dashboard/manage/PlatformManagementView.vue) 查看平台信息 -- 如密钥泄露,在 [PlatformSettingsView.vue](../../src/views/dashboard/other/PlatformSettingsView.vue) 重置密钥 - ---- - -### 3. Access Token 过期后无法刷新 - -**现象:** 使用 refresh_token 换取新 token 失败 - -**排查步骤:** - -1. 检查 refresh_token 是否有效 -2. 检查 refresh_token 是否被撤销 -3. 检查平台是否被禁用 - -**请求示例:** - -```http -POST /api/oauth/token -{ - "grant_type": "refresh_token", - "refresh_token": "your-refresh-token", - "client_id": "your-platform-id", - "client_secret": "your-platform-secret" -} -``` - ---- - -## 用户信息获取问题 - -### 1. 获取用户信息返回 401 - -**现象:** 调用 `/api/oauth/userinfo` 返回未授权错误 - -**排查步骤:** - -1. 检查 access_token 是否过期 -2. 检查 Authorization 头格式是否正确 - ``` - Authorization: Bearer {access_token} - ``` -3. 检查 token 是否被撤销 - -**解决方案:** - -- 使用 refresh_token 获取新的 access_token -- 重新走授权流程 - -**相关页面:** [PlatformUsersView.vue](../../src/views/dashboard/manage/PlatformUsersView.vue) - ---- - -### 2. 用户信息字段缺失 - -**现象:** 返回的用户信息缺少某些字段 - -**排查步骤:** - -1. 检查用户是否设置了该字段 -2. 检查数据库中 `user_data` 集合是否有该字段 - -**响应示例:** - -```json -{ - "user_id": "69bd422960018cf4d0e5", - "email": "user@example.com", - "name": "张三", - "github_username": "zhangsan", - "permission": 1 -} -``` - ---- - -## 远程退登问题 - -### 1. 远程退登不生效 - -**现象:** 用户在 SECTL Auth 退出后,第三方平台没有自动退出 - -**排查步骤:** - -1. 检查是否正确订阅了 `logout_events` 集合 -2. 检查 user_id 是否匹配 -3. 确认 Appwrite Realtime 连接正常 -4. 查看浏览器控制台是否有错误 - -**订阅代码示例:** - -```javascript -client.subscribe( - "databases.69bd89d8000304c37368.collections.logout_events.documents", - (response) => { - const event = response.payload - if (event.user_id === currentUserId) { - localLogout() - } - } -) -``` - -**相关页面:** - -- [SecurityView.vue](../../src/views/dashboard/other/SecurityView.vue) -- [PlatformManagementView.vue](../../src/views/dashboard/manage/PlatformManagementView.vue) - ---- - -### 2. Realtime 连接断开 - -**现象:** 退登事件偶尔收不到 - -**排查步骤:** - -1. 检查网络连接稳定性 -2. 检查是否有心跳机制保持连接 -3. 检查浏览器是否进入休眠状态 - -**解决方案:** - -- 实现重连机制 -- 定期检查连接状态 -- 使用 Service Worker 保持后台连接 - ---- - -## 平台管理问题 - -### 1. 平台管理的"登录用户"为空 - -**现象:** 在平台管理页面查看登录用户,显示为空 - -**排查步骤:** - -1. 检查 `oauth_tokens` 集合是否有记录 -2. 检查记录是否有 `user_id` 字段 -3. 检查 `oauth_tokens` 集合权限 - -**解决方案:** - -如果 `oauth_tokens` 没有 `user_id` 字段,需要在 Appwrite Console 中添加: - -1. 进入 Database → 选择数据库 -2. 找到 `oauth_tokens` 集合 -3. 添加字段:`user_id`(String 类型,非必填) - -**相关页面:** - -- [PlatformManagementView.vue](../../src/views/dashboard/manage/PlatformManagementView.vue) -- [PlatformUsersView.vue](../../src/views/dashboard/manage/PlatformUsersView.vue) - ---- - -### 2. 平台状态显示"审核中" - -**现象:** 新创建的平台无法使用,状态为 `pending` - -**排查步骤:** - -1. 检查是否已提交平台申请 -2. 联系管理员审核 -3. 在 [MyPlatformApplicationsView.vue](../../src/views/dashboard/manage/MyPlatformApplicationsView.vue) 查看申请状态 - -**解决方案:** - -- 等待管理员审核 -- 管理员在 [PlatformApplicationsView.vue](../../src/views/dashboard/manage/PlatformApplicationsView.vue) 中批准申请 - ---- - -### 3. 无法修改平台回调地址 - -**现象:** 在平台设置中修改回调地址失败 - -**排查步骤:** - -1. 检查是否有权限修改(所有者或管理员) -2. 检查回调地址格式是否正确 -3. 检查数据库权限配置 - -**相关页面:** [PlatformSettingsView.vue](../../src/views/dashboard/other/PlatformSettingsView.vue) - ---- - -## 网络与 CORS 问题 - -### 1. CORS 错误 - -**现象:** 浏览器控制台显示 CORS 错误 - -**错误信息:** - -``` -Access to fetch at 'https://auth.sectl.top/...' from origin 'https://auth.sectl.top' has been blocked by CORS policy -``` - -**解决方案:** - -已在 OAuth API 中添加 CORS 头: - -```javascript -res.setHeader("Access-Control-Allow-Origin", "*") -res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS") -res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization") -``` - -**排查步骤:** - -1. 确认请求头是否正确 -2. 确认是否为预检请求(OPTIONS)问题 -3. 检查服务器 CORS 配置 - ---- - -### 2. 授权码中的用户ID解码失败 - -**现象:** 换取 token 后 `user_id` 为 "anonymous" - -**排查步骤:** - -1. 检查授权码格式(应为 32 位:前16位 base64 用户ID + 后16位随机) -2. 检查 `oauth_codes` 集合是否有 `user_id` 字段 - -**代码逻辑:** - -```javascript -// 生成授权码时编码用户ID -const userIdHash = Buffer.from(userId) - .toString("base64") - .replace(/[^a-zA-Z0-9]/g, "") - .substring(0, 16) -const code = userIdHash + randomPart - -// 换取 token 时解码 -try { - const userIdHash = code.substring(0, 16) - const base64Str = userIdHash.replace(/_/g, "/").replace(/-/g, "+") + "==" - const decoded = Buffer.from(base64Str, "base64").toString("utf8") - tokenUserId = decoded -} catch (e) { - tokenUserId = "anonymous" -} -``` - ---- - -### 3. 网络超时 - -**现象:** 请求 OAuth API 超时 - -**排查步骤:** - -1. 检查网络连接 -2. 检查服务器状态 -3. 增加请求超时时间 - -**解决方案:** - -```javascript -// 增加超时时间 -const response = await fetch(url, { - ...options, - signal: AbortSignal.timeout(30000), // 30秒超时 -}) -``` - ---- - -## 调试技巧 - -### 1. 浏览器开发者工具 - -使用浏览器开发者工具排查问题: - -1. **Network 面板** - 查看请求和响应 -2. **Console 面板** - 查看错误日志 -3. **Application 面板** - 查看 Cookie 和 LocalStorage - -### 2. Appwrite Console - -在 Appwrite Console 中检查: - -1. **Database** - 检查集合数据和权限 -2. **Auth** - 检查用户和会话 -3. **Functions** - 查看函数日志 -4. **Realtime** - 检查实时连接 - -### 3. 日志记录 - -在关键位置添加日志: - -```javascript -// 授权流程日志 -console.log("OAuth params:", { client_id, redirect_uri, response_type }) -console.log("Authorization code:", code) -console.log("Token response:", tokenData) - -// 退登事件日志 -console.log("Logout event received:", event) -console.log("Current user ID:", currentUserId) -``` - ---- - -## 常见问题速查表 - -| 问题 | 可能原因 | 解决方案 | -| :------------- | :------------------- | :---------------------- | -| 授权页面 404 | 平台不存在或状态异常 | 检查平台状态和 ID | -| Invalid code | Code 过期或已使用 | 重新授权获取新 code | -| Invalid client | 密钥错误 | 检查 client_secret | -| Token 过期 | 超过 expires_in | 使用 refresh_token 刷新 | -| 用户信息 401 | Token 无效 | 检查 Token 格式和有效期 | -| 退登不生效 | Realtime 未连接 | 检查订阅代码 | -| CORS 错误 | 跨域配置问题 | 检查服务器 CORS 头 | - ---- - -## 获取帮助 - -如果以上方案无法解决问题: - -1. 查看 [API 参考](../platform-integration/API_REFERENCE.md) -2. 查看 [平台接入指南](../platform-integration/README.md) -3. 提交 Issue 到项目仓库 -4. 联系管理员 - ---- - -## 相关页面 - -- [OAuthAuthorizeView.vue](../../src/views/OAuthAuthorizeView.vue) - OAuth 授权页面 -- [AuthCallbackView.vue](../../src/views/AuthCallbackView.vue) - 认证回调 -- [LoginView.vue](../../src/views/LoginView.vue) - 登录页面 -- [PlatformManagementView.vue](../../src/views/dashboard/manage/PlatformManagementView.vue) - 平台管理 -- [PlatformSettingsView.vue](../../src/views/dashboard/other/PlatformSettingsView.vue) - 平台设置 -- [SecurityView.vue](../../src/views/dashboard/other/SecurityView.vue) - 安全设置 -- [LoginPlatformsView.vue](../../src/views/dashboard/list/LoginPlatformsView.vue) - 登录平台列表 diff --git a/public/about-content.json b/public/about-content.json index c183616..ea40f2b 100644 --- a/public/about-content.json +++ b/public/about-content.json @@ -4,4 +4,4 @@ "version": "v1.0.0", "content": "

SecScore

\r
\r
\r

\r
\"GitHub\r
\"Downloads\"\r
\"License\"\r
\"GitHub\r
\"GitHub\r
\"Electron\"\r
\"React\"\r
\"TypeScript\"\r
\"Vite\"\r

\r
\r
SecScore 是一款教育积分管理软件,基于 Electron + React + TypeScript 开发,用于管理学生名单、记录加/扣分、查看排行榜与结算历史,并提供权限保护与数据备份。\r
\r

主要功能

\r
\r
  • 学生管理
\r
- 添加/删除学生\r
- 通过 xlsx 批量导入名单(支持导入前预览与选择“姓名列”)\r
  • 积分管理
\r
- 选择学生并提交加分/扣分\r
- 支持“预设理由”一键填充理由与分值\r
- 支持撤销最近的积分记录(撤销后学生积分会回滚)\r
  • 理由管理
\r
- 维护“预设理由”(分类、理由内容、预设分值)\r
  • 排行榜
\r
- 支持按“今天 / 本周 / 本月”查看积分变化\r
- 支持导出排行榜为 XLSX\r
- 支持查看单个学生的操作记录(文本列表)\r
  • 结算与历史
\r
- “结算并重新开始”:把当前未结算记录归档为一个阶段,并将所有学生当前积分清零\r
- 在“结算历史”查看每个阶段的排行榜\r
  • 系统设置
\r
- 主题切换\r
- 日志查看/导出/清空\r
- 数据导入/导出(JSON)\r
- 密码保护(管理密码 / 积分密码)与找回字符串\r
\r

使用方法

\r
\r

1. 权限与解锁

\r
\r
  • 右上角会显示当前权限:管理权限 / 积分权限 / 只读
\r
  • 若设置了密码,可通过右上角“输入密码”进行解锁
\r
- 管理密码:全功能(学生管理、理由管理、结算、数据管理等)\r
- 积分密码:仅允许积分相关操作\r
  • 点击“锁定”可切回只读状态
\r
  • 无密码时默认视为管理权限
\r
\r

2. 学生管理(导入名单)

\r
\r
入口:左侧菜单 → 学生管理 → 导入名单\r
\r
  • 通过 xlsx 导入
\r
1. 选择一个 .xlsx 文件(默认读取第一个工作表)\r
2. 在预览表格里点击表头,选择“姓名列”\r
3. 点击“导入”\r
4. 导入完成后会提示“新增 / 跳过”数量\r
\r
建议:姓名列尽量只包含纯姓名文本;同名学生会被视为重复并跳过。\r
\r

3. 积分管理(加分/扣分)

\r
\r
入口:左侧菜单 → 积分管理\r
\r
1. 在“姓名”选择一个学生(支持搜索)\r
2. 选择“加分/扣分”,并输入分值\r
3. 在“理由内容”填写原因(可手动输入)\r
4. 点击“确认提交”\r
\r
快捷理由:\r
\r
  • 可在“快捷理由”下拉框中选择预设理由,一键填充理由内容/分值(优先尊重你当前是否已手动输入分值)
\r
\r
撤销最近记录:\r
\r
  • “最近记录”默认折叠,展开后可对记录点击“撤销”
\r
  • 撤销会回滚该条记录对学生积分的影响
\r
\r

4. 排行榜与导出

\r
\r
入口:左侧菜单 → 排行榜\r
\r
  • 可切换统计范围:今天 / 本周 / 本月
\r
  • 点击“导出 XLSX”可导出当前排行榜
\r
  • 点击某个学生的“查看”可打开该学生的操作记录
\r
\r

5. 结算与数据备份

\r
\r
入口:左侧菜单 → 系统设置 → 数据管理\r
\r
  • 结算并重新开始
\r
- 会把当前未结算的积分记录归档为一个阶段\r
- 会将所有学生当前积分清零\r
- 学生名单不变;结算后的历史在“结算历史”查看\r
  • 导出 JSON(强烈建议定期备份)
\r
- 导入会覆盖现有学生/理由/积分记录/设置\r
- 安全相关设置(密码等)不会随导入写入\r
\r

开发与运行(面向贡献者)

\r
\r

MCP 文档

\r
\r
  • [MCP-使用说明](./MCP-使用说明.md)
\r
\r

环境要求

\r
\r
  • Node.js(建议使用 LTS 版本)
\r
  • pnpm
\r
\r

安装依赖

\r
\r
``bash\r
pnpm install\r
`\r
\r

开发模式

\r
\r
`bash\r
pnpm dev\r
`\r
\r

构建

\r
\r
`bash\r
pnpm build\r
`\r
\r
(可选)打包:\r
\r
`bash\r
pnpm build:win\r
pnpm build:unpack\r
`\r
\r

常用检查

\r
\r
`bash\r
pnpm lint\r
pnpm typecheck\r
``\r
", "rawMarkdown": "# SecScore\r\n\r\n\r\n

\r\n \"GitHub\r\n \"Downloads\"\r\n \"License\"\r\n \"GitHub\r\n \"GitHub\r\n \"Electron\"\r\n \"React\"\r\n \"TypeScript\"\r\n \"Vite\"\r\n

\r\n\r\nSecScore 是一款教育积分管理软件,基于 Electron + React + TypeScript 开发,用于管理学生名单、记录加/扣分、查看排行榜与结算历史,并提供权限保护与数据备份。\r\n\r\n## 主要功能\r\n\r\n- 学生管理\r\n - 添加/删除学生\r\n - 通过 xlsx 批量导入名单(支持导入前预览与选择“姓名列”)\r\n- 积分管理\r\n - 选择学生并提交加分/扣分\r\n - 支持“预设理由”一键填充理由与分值\r\n - 支持撤销最近的积分记录(撤销后学生积分会回滚)\r\n- 理由管理\r\n - 维护“预设理由”(分类、理由内容、预设分值)\r\n- 排行榜\r\n - 支持按“今天 / 本周 / 本月”查看积分变化\r\n - 支持导出排行榜为 XLSX\r\n - 支持查看单个学生的操作记录(文本列表)\r\n- 结算与历史\r\n - “结算并重新开始”:把当前未结算记录归档为一个阶段,并将所有学生当前积分清零\r\n - 在“结算历史”查看每个阶段的排行榜\r\n- 系统设置\r\n - 主题切换\r\n - 日志查看/导出/清空\r\n - 数据导入/导出(JSON)\r\n - 密码保护(管理密码 / 积分密码)与找回字符串\r\n\r\n## 使用方法\r\n\r\n### 1. 权限与解锁\r\n\r\n- 右上角会显示当前权限:管理权限 / 积分权限 / 只读\r\n- 若设置了密码,可通过右上角“输入密码”进行解锁\r\n - 管理密码:全功能(学生管理、理由管理、结算、数据管理等)\r\n - 积分密码:仅允许积分相关操作\r\n- 点击“锁定”可切回只读状态\r\n- 无密码时默认视为管理权限\r\n\r\n### 2. 学生管理(导入名单)\r\n\r\n入口:左侧菜单 → 学生管理 → 导入名单\r\n\r\n- 通过 xlsx 导入\r\n 1. 选择一个 `.xlsx` 文件(默认读取第一个工作表)\r\n 2. 在预览表格里点击表头,选择“姓名列”\r\n 3. 点击“导入”\r\n 4. 导入完成后会提示“新增 / 跳过”数量\r\n\r\n建议:姓名列尽量只包含纯姓名文本;同名学生会被视为重复并跳过。\r\n\r\n### 3. 积分管理(加分/扣分)\r\n\r\n入口:左侧菜单 → 积分管理\r\n\r\n1. 在“姓名”选择一个学生(支持搜索)\r\n2. 选择“加分/扣分”,并输入分值\r\n3. 在“理由内容”填写原因(可手动输入)\r\n4. 点击“确认提交”\r\n\r\n快捷理由:\r\n\r\n- 可在“快捷理由”下拉框中选择预设理由,一键填充理由内容/分值(优先尊重你当前是否已手动输入分值)\r\n\r\n撤销最近记录:\r\n\r\n- “最近记录”默认折叠,展开后可对记录点击“撤销”\r\n- 撤销会回滚该条记录对学生积分的影响\r\n\r\n### 4. 排行榜与导出\r\n\r\n入口:左侧菜单 → 排行榜\r\n\r\n- 可切换统计范围:今天 / 本周 / 本月\r\n- 点击“导出 XLSX”可导出当前排行榜\r\n- 点击某个学生的“查看”可打开该学生的操作记录\r\n\r\n### 5. 结算与数据备份\r\n\r\n入口:左侧菜单 → 系统设置 → 数据管理\r\n\r\n- 结算并重新开始\r\n - 会把当前未结算的积分记录归档为一个阶段\r\n - 会将所有学生当前积分清零\r\n - 学生名单不变;结算后的历史在“结算历史”查看\r\n- 导出 JSON(强烈建议定期备份)\r\n - 导入会覆盖现有学生/理由/积分记录/设置\r\n - 安全相关设置(密码等)不会随导入写入\r\n\r\n## 开发与运行(面向贡献者)\r\n\r\n## MCP 文档\r\n\r\n- [MCP-使用说明](./MCP-使用说明.md)\r\n\r\n### 环境要求\r\n\r\n- Node.js(建议使用 LTS 版本)\r\n- pnpm\r\n\r\n### 安装依赖\r\n\r\n```bash\r\npnpm install\r\n```\r\n\r\n### 开发模式\r\n\r\n```bash\r\npnpm dev\r\n```\r\n\r\n### 构建\r\n\r\n```bash\r\npnpm build\r\n```\r\n\r\n(可选)打包:\r\n\r\n```bash\r\npnpm build:win\r\npnpm build:unpack\r\n```\r\n\r\n### 常用检查\r\n\r\n```bash\r\npnpm lint\r\npnpm typecheck\r\n```\r\n" -} \ No newline at end of file +} diff --git a/src-tauri/data.sql b/src-tauri/data.sql index f82cd1f..e03ea45 100644 Binary files a/src-tauri/data.sql and b/src-tauri/data.sql differ diff --git a/src-tauri/src/commands/auth.rs b/src-tauri/src/commands/auth.rs index ed6afc1..195870c 100644 --- a/src-tauri/src/commands/auth.rs +++ b/src-tauri/src/commands/auth.rs @@ -54,6 +54,35 @@ pub struct OAuthUserInfo { pub permission: u32, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OAuthIntrospectResponse { + pub active: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub scope: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub client_id: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub username: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub token_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub exp: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub iat: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub sub: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub aud: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub iss: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OAuthAuthorizationUrlResponse { + pub url: String, + pub state: String, +} + fn get_iv_hex() -> String { SecurityService::generate_iv_hex() } @@ -300,13 +329,23 @@ pub async fn auth_clear_all( pub async fn oauth_get_authorization_url( platform_id: String, callback_url: String, -) -> Result, String> { + state: Option, +) -> Result, String> { + let state = state.unwrap_or_else(|| { + use rand::Rng; + let mut rng = rand::thread_rng(); + let random_bytes: Vec = (0..32).map(|_| rng.gen()).collect(); + base64::Engine::encode(&base64::engine::general_purpose::URL_SAFE_NO_PAD, &random_bytes) + }); + let url = format!( - "https://sectl.top/oauth/authorize?client_id={}&redirect_uri={}&response_type=code", + "https://sectl.top/oauth/authorize?client_id={}&redirect_uri={}&response_type=code&state={}", platform_id, - urlencoding::encode(&callback_url) + urlencoding::encode(&callback_url), + urlencoding::encode(&state) ); - Ok(IpcResponse::success(url)) + + Ok(IpcResponse::success(OAuthAuthorizationUrlResponse { url, state })) } #[tauri::command] @@ -316,15 +355,92 @@ pub async fn oauth_exchange_code( platform_secret: String, callback_url: String, ) -> Result, String> { + println!("[OAuth] 换取令牌 - code: {}, platform_id: {}, callback_url: {}", code, platform_id, callback_url); + let client = reqwest::Client::new(); + let payload = serde_json::json!({ + "grant_type": "authorization_code", + "code": code, + "client_id": platform_id, + "client_secret": platform_secret, + "redirect_uri": callback_url + }); + + println!("[OAuth] 请求体:{:?}", payload); + let response = client .post("https://sectl.top/api/oauth/token") + .json(&payload) + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + println!("[OAuth] 响应状态:{}", response.status()); + + let response_text = response.text().await.map_err(|e| format!("Failed to read response: {}", e))?; + println!("[OAuth] 响应内容:{}", response_text); + + let status_success = response_text.is_empty() || !response_text.contains("error"); + + if !status_success { + return Ok(IpcResponse::error(&response_text)); + } + + let token_response: OAuthTokenResponse = serde_json::from_str(&response_text) + .map_err(|e| format!("Failed to parse response: {}", e))?; + + Ok(IpcResponse::success(token_response)) +} + +#[tauri::command] +pub async fn oauth_revoke_token( + token: String, + token_type_hint: Option, + platform_id: String, + platform_secret: String, +) -> Result, String> { + let client = reqwest::Client::new(); + let mut payload = serde_json::json!({ + "token": token, + "client_id": platform_id, + "client_secret": platform_secret + }); + + if let Some(hint) = token_type_hint { + payload["token_type_hint"] = serde_json::json!(hint); + } + + let response = client + .post("https://sectl.top/api/oauth/revoke") + .json(&payload) + .send() + .await + .map_err(|e| format!("Request failed: {}", e))?; + + if !response.status().is_success() { + let error_text = response + .text() + .await + .unwrap_or_else(|_| "Unknown error".to_string()); + return Ok(IpcResponse::error(&error_text)); + } + + Ok(IpcResponse::success(())) +} + +#[tauri::command] +pub async fn oauth_introspect_token( + token: String, + platform_id: String, + platform_secret: String, +) -> Result, String> { + let client = reqwest::Client::new(); + let response = client + .post("https://sectl.top/api/oauth/introspect") .json(&serde_json::json!({ - "grant_type": "authorization_code", - "code": code, + "token": token, "client_id": platform_id, - "client_secret": platform_secret, - "redirect_uri": callback_url + "client_secret": platform_secret })) .send() .await @@ -338,12 +454,12 @@ pub async fn oauth_exchange_code( return Ok(IpcResponse::error(&error_text)); } - let token_response: OAuthTokenResponse = response + let introspect_response: OAuthIntrospectResponse = response .json() .await .map_err(|e| format!("Failed to parse response: {}", e))?; - Ok(IpcResponse::success(token_response)) + Ok(IpcResponse::success(introspect_response)) } #[tauri::command] diff --git a/src-tauri/src/commands/mod.rs b/src-tauri/src/commands/mod.rs index a2c92c3..4a5e24c 100644 --- a/src-tauri/src/commands/mod.rs +++ b/src-tauri/src/commands/mod.rs @@ -9,6 +9,7 @@ pub mod filesystem; pub mod http_server; pub mod log; pub mod mcp; +pub mod oauth_server; pub mod reason; pub mod response; pub mod reward; @@ -30,6 +31,7 @@ pub use filesystem::*; pub use http_server::*; pub use log::*; pub use mcp::*; +pub use oauth_server::*; pub use reason::*; pub use response::*; pub use reward::*; diff --git a/src-tauri/src/commands/oauth_server.rs b/src-tauri/src/commands/oauth_server.rs new file mode 100644 index 0000000..2596d31 --- /dev/null +++ b/src-tauri/src/commands/oauth_server.rs @@ -0,0 +1,228 @@ +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; +use std::sync::Arc; +use tauri::Emitter; +use tauri::State; +use tokio::sync::Mutex; +use tokio::sync::oneshot; + +use crate::state::AppState; + +use super::response::IpcResponse; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OAuthServerStartResult { + pub url: String, + pub port: u16, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OAuthCallbackResult { + pub code: Option, + pub state: Option, + pub error: Option, + pub error_description: Option, +} + +static OAUTH_SERVER_SHUTDOWN: once_cell::sync::Lazy>>>> = + once_cell::sync::Lazy::new(|| Arc::new(Mutex::new(None))); + +#[tauri::command] +pub async fn oauth_start_callback_server( + app_handle: tauri::AppHandle, + _state: State<'_, Arc>>, +) -> Result, String> { + let mut shutdown_tx = OAUTH_SERVER_SHUTDOWN.lock().await; + + // 如果服务器已经在运行,直接返回 URL + if shutdown_tx.is_some() { + let port = 16888u16; + let url = format!("http://127.0.0.1:{}/oauth/callback", port); + return Ok(IpcResponse::success(OAuthServerStartResult { url, port })); + } + + let port = 16888u16; + let addr: SocketAddr = ([127, 0, 0, 1], port).into(); + let url = format!("http://127.0.0.1:{}/oauth/callback", port); + let url_for_spawn = url.clone(); + + let (tx, rx) = oneshot::channel::<()>(); + *shutdown_tx = Some(tx); + drop(shutdown_tx); + + let app_handle_clone = app_handle.clone(); + + tokio::spawn(async move { + let app = axum::Router::new() + .route("/oauth/callback", axum::routing::get(handle_oauth_callback)) + .layer(axum::extract::Extension(app_handle_clone)); + + let listener = match tokio::net::TcpListener::bind(addr).await { + Ok(l) => l, + Err(e) => { + eprintln!("Failed to bind OAuth callback server: {}", e); + return; + } + }; + + println!("OAuth callback server started at {}", url_for_spawn); + + let server = axum::serve(listener, app); + + tokio::select! { + _ = server => {}, + _ = rx => { + println!("OAuth callback server shutting down"); + } + } + }); + + Ok(IpcResponse::success(OAuthServerStartResult { url, port })) +} + +#[tauri::command] +pub async fn oauth_stop_callback_server( + _state: State<'_, Arc>>, +) -> Result, String> { + let mut shutdown_tx = OAUTH_SERVER_SHUTDOWN.lock().await; + + if let Some(tx) = shutdown_tx.take() { + let _ = tx.send(()); + } + + Ok(IpcResponse::success(())) +} + +async fn handle_oauth_callback( + axum::extract::Query(params): axum::extract::Query>, + axum::extract::Extension(app_handle): axum::extract::Extension, +) -> impl axum::response::IntoResponse { + let code = params.get("code").cloned(); + let state = params.get("state").cloned(); + let error = params.get("error").cloned(); + let error_description = params.get("error_description").cloned(); + + println!("[OAuth Callback] 收到回调 - code: {:?}, state: {:?}, error: {:?}", code, state, error); + + let result = OAuthCallbackResult { + code: code.clone(), + state: state.clone(), + error: error.clone(), + error_description: error_description.clone(), + }; + + match app_handle.emit("oauth-callback", result) { + Ok(_) => println!("[OAuth Callback] Event 发送成功"), + Err(e) => println!("[OAuth Callback] Event 发送失败:{:?}", e), + } + + let html = r#" + + + + + OAuth 授权完成 + + + +
+

✓ 授权成功

+

请返回 SecScore 应用查看登录结果

+

此窗口可以关闭

+
+ + + + "#; + + let error_html = r#" + + + + + OAuth 授权失败 + + + +
+

✗ 授权失败

+

请返回 SecScore 应用查看错误信息

+

此窗口可以关闭

+
+ + + "#; + + let response_html = if error.is_some() { error_html } else { html }; + + ( + axum::http::StatusCode::OK, + [(axum::http::header::CONTENT_TYPE, "text/html; charset=utf-8")], + response_html, + ) +} diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 2ab7ea0..72273fd 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -80,6 +80,10 @@ pub fn run() { oauth_exchange_code, oauth_get_user_info, oauth_refresh_token, + oauth_revoke_token, + oauth_introspect_token, + oauth_start_callback_server, + oauth_stop_callback_server, theme_list, theme_current, theme_set, diff --git a/src/components/AutoScore/ActionEditor.tsx b/src/components/AutoScore/ActionEditor.tsx index 8508edb..b17b44c 100644 --- a/src/components/AutoScore/ActionEditor.tsx +++ b/src/components/AutoScore/ActionEditor.tsx @@ -107,7 +107,9 @@ export const ActionEditor: React.FC = ({ allowClear style={{ minWidth: 260 }} placeholder={t("autoScore.tagNamePlaceholder")} - value={Array.isArray(action.value) ? action.value : action.value ? [action.value] : []} + value={ + Array.isArray(action.value) ? action.value : action.value ? [action.value] : [] + } disabled={!canEdit} options={mergedTagOptions} onChange={(nextValue) => updateAction(action.id, { value: nextValue })} diff --git a/src/components/AutoScore/AutoScoreUtils.ts b/src/components/AutoScore/AutoScoreUtils.ts index 8bc2de7..ccce73a 100644 --- a/src/components/AutoScore/AutoScoreUtils.ts +++ b/src/components/AutoScore/AutoScoreUtils.ts @@ -8,7 +8,11 @@ import { type JsonRule, } from "@react-awesome-query-builder/antd" import type { TFunction } from "i18next" -import { IntervalValueWidget, parseIntervalTriggerValue, stringifyIntervalTriggerValue } from "./IntervalValueWidget" +import { + IntervalValueWidget, + parseIntervalTriggerValue, + stringifyIntervalTriggerValue, +} from "./IntervalValueWidget" export interface AutoScoreTrigger { event: string @@ -76,9 +80,7 @@ const toStringValue = (value: unknown): string => { } const normalizeTagValues = (values: unknown[]): string[] => { - const normalized = values - .map((value) => toStringValue(value).trim()) - .filter(Boolean) + const normalized = values.map((value) => toStringValue(value).trim()).filter(Boolean) return Array.from(new Set(normalized)) } @@ -265,7 +267,10 @@ export const createTriggerQueryConfig = (t: TFunction, tagOptions: AutoScoreTagO export const createEmptyTriggerTree = (config: Config): ImmutableTree => QbUtils.checkTree(QbUtils.loadTree(buildEmptyGroup()), config) -export const triggersToQueryTree = (config: Config, triggers: AutoScoreTrigger[]): ImmutableTree => { +export const triggersToQueryTree = ( + config: Config, + triggers: AutoScoreTrigger[] +): ImmutableTree => { const children = triggers.map(ruleFromTrigger).filter((item): item is JsonRule => Boolean(item)) const group: JsonGroup = { ...buildEmptyGroup(), @@ -282,7 +287,8 @@ export const queryTreeToTriggers = (tree: ImmutableTree, config: Config): AutoSc } const hasUnsupportedLogicInGroup = (group: JsonGroup): boolean => { - const conjunction = typeof group.properties?.conjunction === "string" ? group.properties.conjunction : "AND" + const conjunction = + typeof group.properties?.conjunction === "string" ? group.properties.conjunction : "AND" const not = Boolean(group.properties?.not) if (conjunction !== "AND" || not) { diff --git a/src/components/AutoScore/TriggerRuleBuilder.tsx b/src/components/AutoScore/TriggerRuleBuilder.tsx index 789405e..1396f6a 100644 --- a/src/components/AutoScore/TriggerRuleBuilder.tsx +++ b/src/components/AutoScore/TriggerRuleBuilder.tsx @@ -48,7 +48,11 @@ export const TriggerRuleBuilder: React.FC = ({ >
-