@allbluecn/admin-app 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -45,6 +45,8 @@ import {
45
45
  CreditCard,
46
46
  Shield,
47
47
  ScrollText,
48
+ Ticket,
49
+ KeyRound,
48
50
  } from "lucide-react";
49
51
  import { Logo } from "@/components/layout/sidebar-02/logo";
50
52
  import type { Route } from "./nav-main";
@@ -56,6 +58,8 @@ function buildDashboardRoutes(): Route[] {
56
58
  return [
57
59
  { id: "home", title: m.common_nav_dashboard(), icon: <Home className="size-4" />, link: "/dashboard" },
58
60
  { id: "users", title: m.common_nav_users(), icon: <Users className="size-4" />, link: "/users" },
61
+ { id: "invitations", title: m["admin-invitations_page_title"](), icon: <Ticket className="size-4" />, link: "/invitations" },
62
+ { id: "login-providers", title: m["admin-loginProviders_page_title"](), icon: <KeyRound className="size-4" />, link: "/login-providers" },
59
63
  { id: "docs", title: m.common_nav_docs(), icon: <FileText className="size-4" />, link: "/docs" },
60
64
  { id: "apps", title: m.common_nav_apps(), icon: <Package2 className="size-4" />, link: "/apps" },
61
65
  {
@@ -0,0 +1,324 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { useEffect, useState } from 'react'
19
+ import type React from 'react'
20
+ import { m } from '@/paraglide/messages.js'
21
+ import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
22
+ import { toast } from 'sonner'
23
+ import { Eye, EyeOff, KeyRound, PlugZap, Save, Trash2 } from 'lucide-react'
24
+ import { PageHeader } from '@/components/common/page-header'
25
+ import { Button } from '@/components/ui/button'
26
+ import { Badge } from '@/components/ui/badge'
27
+ import { Card, CardContent } from '@/components/ui/card'
28
+ import { Input } from '@/components/ui/input'
29
+ import { Label } from '@/components/ui/label'
30
+ import { Switch } from '@/components/ui/switch'
31
+ import { Skeleton } from '@/components/ui/skeleton'
32
+ import {
33
+ AlertDialog,
34
+ AlertDialogAction,
35
+ AlertDialogCancel,
36
+ AlertDialogContent,
37
+ AlertDialogDescription,
38
+ AlertDialogFooter,
39
+ AlertDialogHeader,
40
+ AlertDialogTitle,
41
+ } from '@/components/ui/alert-dialog'
42
+ import {
43
+ deleteOAuthProviderFn,
44
+ listOAuthProvidersFn,
45
+ saveOAuthProviderFn,
46
+ testOAuthProviderFn,
47
+ } from '@/server/serverFns/oauth-providers'
48
+
49
+ function GoogleIcon({ className }: { className?: string }) {
50
+ return (
51
+ <svg viewBox="0 0 24 24" className={className} aria-hidden="true">
52
+ <path
53
+ fill="#EA4335"
54
+ d="M12 10.2v3.6h5.1c-.3 2-2 3.4-5.1 3.4-3.1 0-5.6-2.4-5.6-5.6s2.5-5.6 5.6-5.6c1.6 0 2.9.5 4 1.5l2.4-2.4C16.7 3.2 14.5 2.3 12 2.3 6.8 2.3 2.6 6.5 2.6 11.6S6.8 21 12 21c5.8 0 9.6-4 9.6-9.7 0-.6 0-1.1-.1-1.6H12z"
55
+ />
56
+ </svg>
57
+ )
58
+ }
59
+
60
+ export function OAuthProviderPage(): React.ReactElement {
61
+ const queryClient = useQueryClient()
62
+ const [clientId, setClientId] = useState('')
63
+ const [clientSecret, setClientSecret] = useState('')
64
+ const [enabled, setEnabled] = useState(false)
65
+ const [showSecret, setShowSecret] = useState(false)
66
+ const [pendingDelete, setPendingDelete] = useState(false)
67
+
68
+ const providersQuery = useQuery({
69
+ queryKey: ['oauth-providers', 'list'],
70
+ queryFn: () => listOAuthProvidersFn({ data: {} }),
71
+ })
72
+
73
+ const googleRow = providersQuery.data?.providers?.find((p) => p.provider === 'google')
74
+
75
+ useEffect(() => {
76
+ if (!googleRow) return
77
+ if (googleRow.fromDb) {
78
+ setClientId(googleRow.clientId)
79
+ setEnabled(googleRow.enabled)
80
+ setClientSecret('')
81
+ } else if (clientId === '') {
82
+ setEnabled(false)
83
+ }
84
+ }, [googleRow])
85
+
86
+ const invalidateList = () =>
87
+ queryClient.invalidateQueries({ queryKey: ['oauth-providers', 'list'] })
88
+
89
+ const saveMutation = useMutation({
90
+ mutationFn: () =>
91
+ saveOAuthProviderFn({
92
+ data: {
93
+ provider: 'google',
94
+ clientId: clientId.trim(),
95
+ clientSecret: clientSecret.trim() || undefined,
96
+ enabled,
97
+ },
98
+ }),
99
+ onSuccess: () => {
100
+ toast.success(m["admin-loginProviders_toast_saved"]())
101
+ setClientSecret('')
102
+ invalidateList()
103
+ },
104
+ onError: (err) =>
105
+ toast.error(
106
+ err instanceof Error ? err.message : m["admin-loginProviders_toast_saveFailed"](),
107
+ ),
108
+ })
109
+
110
+ const testMutation = useMutation({
111
+ mutationFn: () =>
112
+ testOAuthProviderFn({
113
+ data: {
114
+ provider: 'google',
115
+ clientId: clientId.trim(),
116
+ },
117
+ }),
118
+ onSuccess: (res) => {
119
+ if (res.ok) {
120
+ toast.success(res.detail || m["admin-loginProviders_toast_testOk"]())
121
+ } else {
122
+ toast.error(res.detail || m["admin-loginProviders_toast_testFailed"]())
123
+ }
124
+ },
125
+ onError: (err) =>
126
+ toast.error(
127
+ err instanceof Error ? err.message : m["admin-loginProviders_toast_testFailed"](),
128
+ ),
129
+ })
130
+
131
+ const deleteMutation = useMutation({
132
+ mutationFn: () => deleteOAuthProviderFn({ data: { provider: 'google' } }),
133
+ onSuccess: () => {
134
+ toast.success(m["admin-loginProviders_toast_deleted"]())
135
+ setClientId('')
136
+ setClientSecret('')
137
+ setEnabled(false)
138
+ setPendingDelete(false)
139
+ invalidateList()
140
+ },
141
+ onError: (err) => {
142
+ toast.error(
143
+ err instanceof Error ? err.message : m["admin-loginProviders_toast_deleteFailed"](),
144
+ )
145
+ setPendingDelete(false)
146
+ },
147
+ })
148
+
149
+ const loading = providersQuery.isLoading
150
+ const fromDb = googleRow?.fromDb ?? false
151
+ const hasEnvFallback = googleRow?.hasEnvFallback ?? false
152
+ const canTest = clientId.trim().length > 0
153
+ const canSave = clientId.trim().length > 0
154
+
155
+ return (
156
+ <div className="space-y-6 p-6">
157
+ <PageHeader
158
+ title={m["admin-loginProviders_page_title"]()}
159
+ description={m["admin-loginProviders_page_subtitle"]()}
160
+ icon={KeyRound}
161
+ />
162
+
163
+ <div className="grid gap-6 lg:grid-cols-2">
164
+ <Card>
165
+ <CardContent className="pt-6">
166
+ {loading ? (
167
+ <div className="space-y-3">
168
+ <Skeleton className="h-8 w-full" />
169
+ <Skeleton className="h-8 w-full" />
170
+ <Skeleton className="h-8 w-full" />
171
+ </div>
172
+ ) : (
173
+ <div className="space-y-5">
174
+ <div className="flex items-center gap-3">
175
+ <div className="flex size-10 shrink-0 items-center justify-center rounded-lg border bg-background">
176
+ <GoogleIcon className="size-5" />
177
+ </div>
178
+ <div className="flex-1">
179
+ <div className="text-sm font-medium">Google</div>
180
+ <div className="text-xs text-muted-foreground">
181
+ {fromDb
182
+ ? m["admin-loginProviders_source_db"]()
183
+ : hasEnvFallback
184
+ ? m["admin-loginProviders_source_env"]()
185
+ : m["admin-loginProviders_source_none"]()}
186
+ </div>
187
+ </div>
188
+ <Badge variant={fromDb ? 'default' : 'secondary'}>
189
+ {fromDb
190
+ ? m["admin-loginProviders_source_db"]()
191
+ : hasEnvFallback
192
+ ? m["admin-loginProviders_source_env"]()
193
+ : m["admin-loginProviders_source_none"]()}
194
+ </Badge>
195
+ </div>
196
+
197
+ <div className="space-y-2">
198
+ <Label htmlFor="oauth-client-id">
199
+ {m["admin-loginProviders_field_clientId"]()}
200
+ </Label>
201
+ <Input
202
+ id="oauth-client-id"
203
+ value={clientId}
204
+ onChange={(e) => setClientId(e.target.value)}
205
+ placeholder={m["admin-loginProviders_field_clientId_placeholder"]()}
206
+ autoComplete="off"
207
+ spellCheck={false}
208
+ />
209
+ </div>
210
+
211
+ <div className="space-y-2">
212
+ <Label htmlFor="oauth-client-secret">
213
+ {m["admin-loginProviders_field_clientSecret"]()}
214
+ </Label>
215
+ <div className="relative">
216
+ <Input
217
+ id="oauth-client-secret"
218
+ type={showSecret ? 'text' : 'password'}
219
+ value={clientSecret}
220
+ onChange={(e) => setClientSecret(e.target.value)}
221
+ placeholder={
222
+ googleRow?.hasSecret
223
+ ? m["admin-loginProviders_field_clientSecret_hint"]()
224
+ : m["admin-loginProviders_field_clientSecret_placeholder"]()
225
+ }
226
+ autoComplete="new-password"
227
+ spellCheck={false}
228
+ className="pr-10"
229
+ />
230
+ <Button
231
+ type="button"
232
+ variant="ghost"
233
+ size="icon"
234
+ className="absolute right-0 top-0 h-full w-10 px-0"
235
+ onClick={() => setShowSecret((v) => !v)}
236
+ aria-label={
237
+ showSecret
238
+ ? m["admin-loginProviders_action_hideSecret"]()
239
+ : m["admin-loginProviders_action_showSecret"]()
240
+ }
241
+ >
242
+ {showSecret ? (
243
+ <EyeOff className="size-4" />
244
+ ) : (
245
+ <Eye className="size-4" />
246
+ )}
247
+ </Button>
248
+ </div>
249
+ <p className="text-xs text-muted-foreground">
250
+ {m["admin-loginProviders_field_clientSecret_desc"]()}
251
+ </p>
252
+ </div>
253
+
254
+ <div className="flex items-center justify-between rounded-md border px-3 py-2">
255
+ <div className="text-sm font-medium">
256
+ {m["admin-loginProviders_field_enabled"]()}
257
+ </div>
258
+ <Switch
259
+ checked={enabled}
260
+ onCheckedChange={setEnabled}
261
+ aria-label={m["admin-loginProviders_field_enabled"]()}
262
+ />
263
+ </div>
264
+
265
+ <div className="flex flex-wrap items-center gap-2">
266
+ <Button
267
+ onClick={() => saveMutation.mutate()}
268
+ disabled={saveMutation.isPending || !canSave}
269
+ >
270
+ <Save className="size-4" />
271
+ {m["admin-loginProviders_action_save"]()}
272
+ </Button>
273
+ <Button
274
+ variant="outline"
275
+ onClick={() => testMutation.mutate()}
276
+ disabled={testMutation.isPending || !canTest}
277
+ >
278
+ <PlugZap className="size-4" />
279
+ {m["admin-loginProviders_action_test"]()}
280
+ </Button>
281
+ {fromDb && (
282
+ <Button
283
+ variant="outline"
284
+ className="text-destructive hover:bg-destructive/10 hover:text-destructive"
285
+ onClick={() => setPendingDelete(true)}
286
+ >
287
+ <Trash2 className="size-4" />
288
+ {m["admin-loginProviders_action_delete"]()}
289
+ </Button>
290
+ )}
291
+ </div>
292
+
293
+ <p className="text-xs text-muted-foreground">
294
+ {m["admin-loginProviders_cache_hint"]()}
295
+ </p>
296
+ </div>
297
+ )}
298
+ </CardContent>
299
+ </Card>
300
+ </div>
301
+
302
+ <AlertDialog open={pendingDelete} onOpenChange={(o: boolean) => !o && setPendingDelete(false)}>
303
+ <AlertDialogContent>
304
+ <AlertDialogHeader>
305
+ <AlertDialogTitle>{m["admin-loginProviders_delete_confirm_title"]()}</AlertDialogTitle>
306
+ <AlertDialogDescription>
307
+ {m["admin-loginProviders_delete_confirm_desc"]()}
308
+ </AlertDialogDescription>
309
+ </AlertDialogHeader>
310
+ <AlertDialogFooter>
311
+ <AlertDialogCancel>{m.common_common_cancel()}</AlertDialogCancel>
312
+ <AlertDialogAction
313
+ className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
314
+ disabled={deleteMutation.isPending}
315
+ onClick={() => deleteMutation.mutate()}
316
+ >
317
+ {m["admin-loginProviders_action_delete"]()}
318
+ </AlertDialogAction>
319
+ </AlertDialogFooter>
320
+ </AlertDialogContent>
321
+ </AlertDialog>
322
+ </div>
323
+ )
324
+ }
@@ -26,7 +26,9 @@ import { Route as NavDashboardRouteRouteImport } from './routes/_nav/dashboard/r
26
26
  import { Route as NavDependenciesRouteRouteImport } from './routes/_nav/dependencies/route'
27
27
  import { Route as NavDeveloperLogsRouteRouteImport } from './routes/_nav/developer-logs/route'
28
28
  import { Route as NavDocsRouteRouteImport } from './routes/_nav/docs/route'
29
+ import { Route as NavInvitationsRouteRouteImport } from './routes/_nav/invitations/route'
29
30
  import { Route as NavLicensesRouteRouteImport } from './routes/_nav/licenses/route'
31
+ import { Route as NavLoginProvidersRouteRouteImport } from './routes/_nav/login-providers/route'
30
32
  import { Route as NavMarketRouteRouteImport } from './routes/_nav/market/route'
31
33
  import { Route as NavMonitoringRouteRouteImport } from './routes/_nav/monitoring/route'
32
34
  import { Route as NavPermissionsRouteRouteImport } from './routes/_nav/permissions/route'
@@ -166,6 +168,13 @@ const NavDocsRouteRoute = NavDocsRouteRouteImport.update({
166
168
  } as any).lazy(() =>
167
169
  import('./routes/_nav/docs/route.lazy').then((d) => d.Route),
168
170
  )
171
+ const NavInvitationsRouteRoute = NavInvitationsRouteRouteImport.update({
172
+ id: '/invitations',
173
+ path: '/invitations',
174
+ getParentRoute: () => NavRouteRoute,
175
+ } as any).lazy(() =>
176
+ import('./routes/_nav/invitations/route.lazy').then((d) => d.Route),
177
+ )
169
178
  const NavLicensesRouteRoute = NavLicensesRouteRouteImport.update({
170
179
  id: '/licenses',
171
180
  path: '/licenses',
@@ -173,6 +182,13 @@ const NavLicensesRouteRoute = NavLicensesRouteRouteImport.update({
173
182
  } as any).lazy(() =>
174
183
  import('./routes/_nav/licenses/route.lazy').then((d) => d.Route),
175
184
  )
185
+ const NavLoginProvidersRouteRoute = NavLoginProvidersRouteRouteImport.update({
186
+ id: '/login-providers',
187
+ path: '/login-providers',
188
+ getParentRoute: () => NavRouteRoute,
189
+ } as any).lazy(() =>
190
+ import('./routes/_nav/login-providers/route.lazy').then((d) => d.Route),
191
+ )
176
192
  const NavMarketRouteRoute = NavMarketRouteRouteImport.update({
177
193
  id: '/market',
178
194
  path: '/market',
@@ -417,7 +433,9 @@ export interface FileRoutesByFullPath {
417
433
  '/dependencies': typeof NavDependenciesRouteRoute
418
434
  '/developer-logs': typeof NavDeveloperLogsRouteRoute
419
435
  '/docs': typeof NavDocsRouteRoute
436
+ '/invitations': typeof NavInvitationsRouteRoute
420
437
  '/licenses': typeof NavLicensesRouteRouteWithChildren
438
+ '/login-providers': typeof NavLoginProvidersRouteRoute
421
439
  '/market': typeof NavMarketRouteRoute
422
440
  '/monitoring': typeof NavMonitoringRouteRouteWithChildren
423
441
  '/permissions': typeof NavPermissionsRouteRoute
@@ -463,7 +481,9 @@ export interface FileRoutesByTo {
463
481
  '/dependencies': typeof NavDependenciesRouteRoute
464
482
  '/developer-logs': typeof NavDeveloperLogsRouteRoute
465
483
  '/docs': typeof NavDocsRouteRoute
484
+ '/invitations': typeof NavInvitationsRouteRoute
466
485
  '/licenses': typeof NavLicensesRouteRouteWithChildren
486
+ '/login-providers': typeof NavLoginProvidersRouteRoute
467
487
  '/market': typeof NavMarketRouteRoute
468
488
  '/monitoring': typeof NavMonitoringRouteRouteWithChildren
469
489
  '/permissions': typeof NavPermissionsRouteRoute
@@ -512,7 +532,9 @@ export interface FileRoutesById {
512
532
  '/_nav/dependencies': typeof NavDependenciesRouteRoute
513
533
  '/_nav/developer-logs': typeof NavDeveloperLogsRouteRoute
514
534
  '/_nav/docs': typeof NavDocsRouteRoute
535
+ '/_nav/invitations': typeof NavInvitationsRouteRoute
515
536
  '/_nav/licenses': typeof NavLicensesRouteRouteWithChildren
537
+ '/_nav/login-providers': typeof NavLoginProvidersRouteRoute
516
538
  '/_nav/market': typeof NavMarketRouteRoute
517
539
  '/_nav/monitoring': typeof NavMonitoringRouteRouteWithChildren
518
540
  '/_nav/permissions': typeof NavPermissionsRouteRoute
@@ -561,7 +583,9 @@ export interface FileRouteTypes {
561
583
  | '/dependencies'
562
584
  | '/developer-logs'
563
585
  | '/docs'
586
+ | '/invitations'
564
587
  | '/licenses'
588
+ | '/login-providers'
565
589
  | '/market'
566
590
  | '/monitoring'
567
591
  | '/permissions'
@@ -607,7 +631,9 @@ export interface FileRouteTypes {
607
631
  | '/dependencies'
608
632
  | '/developer-logs'
609
633
  | '/docs'
634
+ | '/invitations'
610
635
  | '/licenses'
636
+ | '/login-providers'
611
637
  | '/market'
612
638
  | '/monitoring'
613
639
  | '/permissions'
@@ -655,7 +681,9 @@ export interface FileRouteTypes {
655
681
  | '/_nav/dependencies'
656
682
  | '/_nav/developer-logs'
657
683
  | '/_nav/docs'
684
+ | '/_nav/invitations'
658
685
  | '/_nav/licenses'
686
+ | '/_nav/login-providers'
659
687
  | '/_nav/market'
660
688
  | '/_nav/monitoring'
661
689
  | '/_nav/permissions'
@@ -808,6 +836,13 @@ declare module '@tanstack/react-router' {
808
836
  preLoaderRoute: typeof NavDocsRouteRouteImport
809
837
  parentRoute: typeof NavRouteRoute
810
838
  }
839
+ '/_nav/invitations': {
840
+ id: '/_nav/invitations'
841
+ path: '/invitations'
842
+ fullPath: '/invitations'
843
+ preLoaderRoute: typeof NavInvitationsRouteRouteImport
844
+ parentRoute: typeof NavRouteRoute
845
+ }
811
846
  '/_nav/licenses': {
812
847
  id: '/_nav/licenses'
813
848
  path: '/licenses'
@@ -815,6 +850,13 @@ declare module '@tanstack/react-router' {
815
850
  preLoaderRoute: typeof NavLicensesRouteRouteImport
816
851
  parentRoute: typeof NavRouteRoute
817
852
  }
853
+ '/_nav/login-providers': {
854
+ id: '/_nav/login-providers'
855
+ path: '/login-providers'
856
+ fullPath: '/login-providers'
857
+ preLoaderRoute: typeof NavLoginProvidersRouteRouteImport
858
+ parentRoute: typeof NavRouteRoute
859
+ }
818
860
  '/_nav/market': {
819
861
  id: '/_nav/market'
820
862
  path: '/market'
@@ -1132,7 +1174,9 @@ interface NavRouteRouteChildren {
1132
1174
  NavDependenciesRouteRoute: typeof NavDependenciesRouteRoute
1133
1175
  NavDeveloperLogsRouteRoute: typeof NavDeveloperLogsRouteRoute
1134
1176
  NavDocsRouteRoute: typeof NavDocsRouteRoute
1177
+ NavInvitationsRouteRoute: typeof NavInvitationsRouteRoute
1135
1178
  NavLicensesRouteRoute: typeof NavLicensesRouteRouteWithChildren
1179
+ NavLoginProvidersRouteRoute: typeof NavLoginProvidersRouteRoute
1136
1180
  NavMarketRouteRoute: typeof NavMarketRouteRoute
1137
1181
  NavMonitoringRouteRoute: typeof NavMonitoringRouteRouteWithChildren
1138
1182
  NavPermissionsRouteRoute: typeof NavPermissionsRouteRoute
@@ -1155,7 +1199,9 @@ const NavRouteRouteChildren: NavRouteRouteChildren = {
1155
1199
  NavDependenciesRouteRoute: NavDependenciesRouteRoute,
1156
1200
  NavDeveloperLogsRouteRoute: NavDeveloperLogsRouteRoute,
1157
1201
  NavDocsRouteRoute: NavDocsRouteRoute,
1202
+ NavInvitationsRouteRoute: NavInvitationsRouteRoute,
1158
1203
  NavLicensesRouteRoute: NavLicensesRouteRouteWithChildren,
1204
+ NavLoginProvidersRouteRoute: NavLoginProvidersRouteRoute,
1159
1205
  NavMarketRouteRoute: NavMarketRouteRoute,
1160
1206
  NavMonitoringRouteRoute: NavMonitoringRouteRouteWithChildren,
1161
1207
  NavPermissionsRouteRoute: NavPermissionsRouteRoute,
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createLazyFileRoute } from "@tanstack/react-router"
19
+ import { InvitationsPage } from "@/components/invitations/invitations-page"
20
+
21
+ export const Route = createLazyFileRoute("/_nav/invitations")({
22
+ pendingComponent: () => (
23
+ <div className="flex h-64 items-center justify-center">
24
+ <div className="size-8 animate-spin rounded-full border-2 border-primary border-t-transparent" />
25
+ </div>
26
+ ),
27
+ component: InvitationsPage,
28
+ })
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createFileRoute } from "@tanstack/react-router"
19
+
20
+ export const Route = createFileRoute("/_nav/invitations")({})
@@ -0,0 +1,28 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createLazyFileRoute } from '@tanstack/react-router'
19
+ import { OAuthProviderPage } from '@/components/oauth-providers/provider-page'
20
+
21
+ export const Route = createLazyFileRoute('/_nav/login-providers')({
22
+ pendingComponent: () => (
23
+ <div className="flex h-64 items-center justify-center">
24
+ <div className="size-8 animate-spin rounded-full border-2 border-primary border-t-transparent" />
25
+ </div>
26
+ ),
27
+ component: OAuthProviderPage,
28
+ })
@@ -0,0 +1,20 @@
1
+ // SPDX-License-Identifier: AGPL-3.0-or-later
2
+ // AllBlue - 理想之海
3
+ // Copyright (C) 2026 AllBlue Contributors
4
+ //
5
+ // This program is free software: you can redistribute it and/or modify
6
+ // it under the terms of the GNU Affero General Public License as published by
7
+ // the Free Software Foundation, either version 3 of the License, or
8
+ // (at your option) any later version.
9
+ //
10
+ // This program is distributed in the hope that it will be useful,
11
+ // but WITHOUT ANY WARRANTY; without even the implied warranty of
12
+ // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
+ // GNU Affero General Public License for more details.
14
+ //
15
+ // You should have received a copy of the GNU Affero General Public License
16
+ // along with this program. If not, see <https://www.gnu.org/licenses/>.
17
+
18
+ import { createFileRoute } from '@tanstack/react-router'
19
+
20
+ export const Route = createFileRoute('/_nav/login-providers')({})