From 65d18315184a838b0091b981d915b7c9f6d2f8c3 Mon Sep 17 00:00:00 2001
From: Povo43 <140213985+Povo43@users.noreply.github.com>
Date: Wed, 25 Feb 2026 06:21:19 +0000
Subject: [PATCH] feat: add ClientSpace consent flow and admin logs/storage UI
---
README.md | 9 +-
client/src/App.tsx | 2 +
client/src/components/layout/app-layout.tsx | 4 +-
client/src/hooks/use-admin-logs.ts | 22 ++++
client/src/hooks/use-namespaces.ts | 29 ++++++
client/src/pages/clients.tsx | 94 ++++++++++-------
client/src/pages/logs.tsx | 58 +++++++++++
client/src/pages/namespaces.tsx | 35 +++++--
docs/README.md | 6 +-
docs/SERVER_GUIDE.md | 18 +++-
server/index.ts | 9 ++
server/request-logs.ts | 22 ++++
server/routes.ts | 108 ++++++++++++++++----
server/storage.ts | 33 +++---
shared/routes.ts | 11 +-
shared/schema.ts | 2 +-
16 files changed, 371 insertions(+), 91 deletions(-)
create mode 100644 client/src/hooks/use-admin-logs.ts
create mode 100644 client/src/pages/logs.tsx
create mode 100644 server/request-logs.ts
diff --git a/README.md b/README.md
index a88e2f0..2822df2 100644
--- a/README.md
+++ b/README.md
@@ -136,7 +136,7 @@ SDK の役割:
```js
USSP.config.url("https://storage.example.com")
-await USSP.init({ clientId: "appId" })
+await USSP.init({ clientId: "com.example.billing" }) // = ClientSpace
await USSP.upload("memo.txt", blob)
```
@@ -149,8 +149,11 @@ SDK は最小限の依存で安全に扱えるようにする。
最低限の UI 機能:
* サーバー Storage Adapter 設定画面(Local/S3/R2/Drive など)
-* OAuth クライアント登録・管理
-* Namespace・Storage Policy 設定
+* OAuth クライアント登録・管理(ClientID相当は `ClientSpace` として運用)
+* OAuth 許可画面でサービスURLとClientSpaceをユーザーへ明示
+* ユーザー管理
+* 各種ログ確認
+* Namespace・保存先ストレージ選択・Storage Policy 設定
* 使用状況・クォータ表示
---
diff --git a/client/src/App.tsx b/client/src/App.tsx
index 330885e..982c9ef 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -14,6 +14,7 @@ import AdaptersPage from "@/pages/adapters";
import NamespacesPage from "@/pages/namespaces";
import ClientsPage from "@/pages/clients";
import UsersPage from "@/pages/users";
+import LogsPage from "@/pages/logs";
import NotFound from "@/pages/not-found";
function Router() {
@@ -25,6 +26,7 @@ function Router() {
+
diff --git a/client/src/components/layout/app-layout.tsx b/client/src/components/layout/app-layout.tsx
index 3273980..7ba8243 100644
--- a/client/src/components/layout/app-layout.tsx
+++ b/client/src/components/layout/app-layout.tsx
@@ -6,7 +6,8 @@ import {
KeyRound,
LayoutDashboard,
Settings2,
- UserCog
+ UserCog,
+ ScrollText
} from "lucide-react";
import {
Sidebar,
@@ -28,6 +29,7 @@ const navItems = [
{ title: "ネームスペース", url: "/namespaces", icon: FolderTree },
{ title: "OAuthクライアント", url: "/clients", icon: KeyRound },
{ title: "ユーザー", url: "/users", icon: UserCog },
+ { title: "各種ログ", url: "/logs", icon: ScrollText },
];
function AppSidebar() {
diff --git a/client/src/hooks/use-admin-logs.ts b/client/src/hooks/use-admin-logs.ts
new file mode 100644
index 0000000..c9f5e88
--- /dev/null
+++ b/client/src/hooks/use-admin-logs.ts
@@ -0,0 +1,22 @@
+import { useQuery } from "@tanstack/react-query";
+
+export interface AdminLogEntry {
+ timestamp: string;
+ method: string;
+ path: string;
+ statusCode: number;
+ durationMs: number;
+ response?: unknown;
+}
+
+export function useAdminLogs(limit = 100) {
+ return useQuery({
+ queryKey: ["/api/admin/logs", limit],
+ queryFn: async () => {
+ const res = await fetch(`/api/admin/logs?limit=${limit}`, { credentials: "include" });
+ if (!res.ok) throw new Error("Failed to fetch admin logs");
+ return (await res.json()) as AdminLogEntry[];
+ },
+ refetchInterval: 5000,
+ });
+}
diff --git a/client/src/hooks/use-namespaces.ts b/client/src/hooks/use-namespaces.ts
index 46dd8de..dd831ea 100644
--- a/client/src/hooks/use-namespaces.ts
+++ b/client/src/hooks/use-namespaces.ts
@@ -69,3 +69,32 @@ export function useDeleteNamespace() {
}
});
}
+
+export function useUpdateNamespace() {
+ const queryClient = useQueryClient();
+ const { toast } = useToast();
+
+ return useMutation({
+ mutationFn: async ({ id, data }: { id: number; data: { storageAdapterId?: number | null; quotaBytes?: number | null } }) => {
+ const url = buildUrl(api.namespaces.update.path, { id });
+ const res = await fetch(url, {
+ method: api.namespaces.update.method,
+ headers: { "Content-Type": "application/json" },
+ credentials: "include",
+ body: JSON.stringify(data),
+ });
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({ message: "Failed to update namespace" }));
+ throw new Error(err.message || "Failed to update namespace");
+ }
+ return api.namespaces.update.responses[200].parse(await res.json());
+ },
+ onSuccess: () => {
+ queryClient.invalidateQueries({ queryKey: [api.namespaces.list.path] });
+ toast({ title: "Namespace updated" });
+ },
+ onError: (err) => {
+ toast({ title: "Failed to update namespace", description: err.message, variant: "destructive" });
+ }
+ });
+}
diff --git a/client/src/pages/clients.tsx b/client/src/pages/clients.tsx
index f50db83..24b40cf 100644
--- a/client/src/pages/clients.tsx
+++ b/client/src/pages/clients.tsx
@@ -31,6 +31,7 @@ import type { OauthClient } from "@shared/schema";
const clientFormSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
+ clientId: z.string().min(3, "ClientSpace は3文字以上で入力してください"),
redirectUris: z.string().min(5, "Provide at least one redirect URI"),
});
@@ -50,12 +51,12 @@ export default function ClientsPage() {
OAuthクライアント
-
Manage applications authorized to use the USSP SDK.
+ サービス提供者が設定する ClientSpace と OAuth 設定を管理します。
setNewClientDetails(client)}
+ onCreated={(client) => setNewClientDetails(client)}
/>
@@ -65,7 +66,7 @@ export default function ClientsPage() {
App Name
- クライアントID
+ ClientSpace
Redirect URIs
作成日時
操作
@@ -122,7 +123,6 @@ export default function ClientsPage() {
- {/* Secret Display Modal - Shown only immediately after creation */}
{newClientDetails && (
void,
- on作成日時: (client: OauthClient) => void
+ onCreated: (client: OauthClient) => void
}) {
const createMutation = useCreateClient();
@@ -148,36 +148,33 @@ function CreateClientDialog({
resolver: zodResolver(clientFormSchema),
defaultValues: {
name: "",
+ clientId: "",
redirectUris: "http://localhost:3000/callback",
},
});
function onSubmit(data: ClientFormValues) {
- createMutation.mutate(
- data,
- {
- onSuccess: (newClient) => {
- onOpenChange(false);
- form.reset();
- // Pass the returned client (which includes the unhashed secret) to parent
- on作成日時(newClient as unknown as OauthClient);
- }
+ createMutation.mutate(data, {
+ onSuccess: (newClient) => {
+ onOpenChange(false);
+ form.reset();
+ onCreated(newClient);
}
- );
+ });
}
return (