Skip to content

Commit 2fca40d

Browse files
list tokens of user with master access
1 parent 75df098 commit 2fca40d

File tree

15 files changed

+290
-55
lines changed

15 files changed

+290
-55
lines changed

apps/client/src/lib/services/sync.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ export class Sync extends Effect.Service<Sync>()("Sync", {
3131
}) =>
3232
Effect.gen(function* () {
3333
const clientId = yield* initClient;
34+
yield* Effect.log(`Pushing snapshot ${snapshotId}`);
3435

3536
const response = yield* Effect.fromNullable(workspace.token).pipe(
3637
Effect.flatMap((token) =>

apps/client/src/routeTree.gen.ts

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -11,23 +11,30 @@
1111
// Import Routes
1212

1313
import { Route as rootRoute } from './routes/__root'
14-
import { Route as WorkspaceIdImport } from './routes/$workspaceId'
1514
import { Route as IndexImport } from './routes/index'
15+
import { Route as WorkspaceIdIndexImport } from './routes/$workspaceId/index'
16+
import { Route as WorkspaceIdTokenImport } from './routes/$workspaceId/token'
1617

1718
// Create/Update Routes
1819

19-
const WorkspaceIdRoute = WorkspaceIdImport.update({
20-
id: '/$workspaceId',
21-
path: '/$workspaceId',
22-
getParentRoute: () => rootRoute,
23-
} as any)
24-
2520
const IndexRoute = IndexImport.update({
2621
id: '/',
2722
path: '/',
2823
getParentRoute: () => rootRoute,
2924
} as any)
3025

26+
const WorkspaceIdIndexRoute = WorkspaceIdIndexImport.update({
27+
id: '/$workspaceId/',
28+
path: '/$workspaceId/',
29+
getParentRoute: () => rootRoute,
30+
} as any)
31+
32+
const WorkspaceIdTokenRoute = WorkspaceIdTokenImport.update({
33+
id: '/$workspaceId/token',
34+
path: '/$workspaceId/token',
35+
getParentRoute: () => rootRoute,
36+
} as any)
37+
3138
// Populate the FileRoutesByPath interface
3239

3340
declare module '@tanstack/react-router' {
@@ -39,11 +46,18 @@ declare module '@tanstack/react-router' {
3946
preLoaderRoute: typeof IndexImport
4047
parentRoute: typeof rootRoute
4148
}
42-
'/$workspaceId': {
43-
id: '/$workspaceId'
49+
'/$workspaceId/token': {
50+
id: '/$workspaceId/token'
51+
path: '/$workspaceId/token'
52+
fullPath: '/$workspaceId/token'
53+
preLoaderRoute: typeof WorkspaceIdTokenImport
54+
parentRoute: typeof rootRoute
55+
}
56+
'/$workspaceId/': {
57+
id: '/$workspaceId/'
4458
path: '/$workspaceId'
4559
fullPath: '/$workspaceId'
46-
preLoaderRoute: typeof WorkspaceIdImport
60+
preLoaderRoute: typeof WorkspaceIdIndexImport
4761
parentRoute: typeof rootRoute
4862
}
4963
}
@@ -53,37 +67,42 @@ declare module '@tanstack/react-router' {
5367

5468
export interface FileRoutesByFullPath {
5569
'/': typeof IndexRoute
56-
'/$workspaceId': typeof WorkspaceIdRoute
70+
'/$workspaceId/token': typeof WorkspaceIdTokenRoute
71+
'/$workspaceId': typeof WorkspaceIdIndexRoute
5772
}
5873

5974
export interface FileRoutesByTo {
6075
'/': typeof IndexRoute
61-
'/$workspaceId': typeof WorkspaceIdRoute
76+
'/$workspaceId/token': typeof WorkspaceIdTokenRoute
77+
'/$workspaceId': typeof WorkspaceIdIndexRoute
6278
}
6379

6480
export interface FileRoutesById {
6581
__root__: typeof rootRoute
6682
'/': typeof IndexRoute
67-
'/$workspaceId': typeof WorkspaceIdRoute
83+
'/$workspaceId/token': typeof WorkspaceIdTokenRoute
84+
'/$workspaceId/': typeof WorkspaceIdIndexRoute
6885
}
6986

7087
export interface FileRouteTypes {
7188
fileRoutesByFullPath: FileRoutesByFullPath
72-
fullPaths: '/' | '/$workspaceId'
89+
fullPaths: '/' | '/$workspaceId/token' | '/$workspaceId'
7390
fileRoutesByTo: FileRoutesByTo
74-
to: '/' | '/$workspaceId'
75-
id: '__root__' | '/' | '/$workspaceId'
91+
to: '/' | '/$workspaceId/token' | '/$workspaceId'
92+
id: '__root__' | '/' | '/$workspaceId/token' | '/$workspaceId/'
7693
fileRoutesById: FileRoutesById
7794
}
7895

7996
export interface RootRouteChildren {
8097
IndexRoute: typeof IndexRoute
81-
WorkspaceIdRoute: typeof WorkspaceIdRoute
98+
WorkspaceIdTokenRoute: typeof WorkspaceIdTokenRoute
99+
WorkspaceIdIndexRoute: typeof WorkspaceIdIndexRoute
82100
}
83101

84102
const rootRouteChildren: RootRouteChildren = {
85103
IndexRoute: IndexRoute,
86-
WorkspaceIdRoute: WorkspaceIdRoute,
104+
WorkspaceIdTokenRoute: WorkspaceIdTokenRoute,
105+
WorkspaceIdIndexRoute: WorkspaceIdIndexRoute,
87106
}
88107

89108
export const routeTree = rootRoute
@@ -97,14 +116,18 @@ export const routeTree = rootRoute
97116
"filePath": "__root.tsx",
98117
"children": [
99118
"/",
100-
"/$workspaceId"
119+
"/$workspaceId/token",
120+
"/$workspaceId/"
101121
]
102122
},
103123
"/": {
104124
"filePath": "index.tsx"
105125
},
106-
"/$workspaceId": {
107-
"filePath": "$workspaceId.tsx"
126+
"/$workspaceId/token": {
127+
"filePath": "$workspaceId/token.tsx"
128+
},
129+
"/$workspaceId/": {
130+
"filePath": "$workspaceId/index.tsx"
108131
}
109132
}
110133
}

apps/client/src/routes/$workspaceId.tsx renamed to apps/client/src/routes/$workspaceId/index.tsx

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
import { Worker } from "@effect/platform";
22
import { BrowserWorker } from "@effect/platform-browser";
3-
import { createFileRoute } from "@tanstack/react-router";
3+
import { createFileRoute, Link } from "@tanstack/react-router";
44
import { Effect } from "effect";
55
import { startTransition, useEffect } from "react";
6-
import { useActivity } from "../lib/hooks/use-activity";
7-
import { RuntimeClient } from "../lib/runtime-client";
8-
import { LoroStorage } from "../lib/services/loro-storage";
9-
import { WorkspaceManager } from "../lib/services/workspace-manager";
10-
import { useActionEffect } from "../lib/use-action-effect";
11-
import { Bootstrap, LiveQuery } from "../workers/schema";
6+
import { useActivity } from "../../lib/hooks/use-activity";
7+
import { RuntimeClient } from "../../lib/runtime-client";
8+
import { LoroStorage } from "../../lib/services/loro-storage";
9+
import { WorkspaceManager } from "../../lib/services/workspace-manager";
10+
import { useActionEffect } from "../../lib/use-action-effect";
11+
import { Bootstrap, LiveQuery } from "../../workers/schema";
1212

13-
export const Route = createFileRoute("/$workspaceId")({
13+
export const Route = createFileRoute("/$workspaceId/")({
1414
component: RouteComponent,
1515
loader: ({ params: { workspaceId } }) =>
1616
RuntimeClient.runPromise(
@@ -89,6 +89,13 @@ function RouteComponent() {
8989

9090
return (
9191
<div>
92+
<Link
93+
to="/$workspaceId/token"
94+
params={{ workspaceId: workspace.workspaceId }}
95+
>
96+
Tokens
97+
</Link>
98+
9299
<p>{workspace.workspaceId}</p>
93100
<button
94101
onClick={() =>
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { createFileRoute } from "@tanstack/react-router";
2+
import { Effect } from "effect";
3+
import { ApiClient } from "../../lib/api-client";
4+
import { RuntimeClient } from "../../lib/runtime-client";
5+
import { WorkspaceManager } from "../../lib/services/workspace-manager";
6+
7+
export const Route = createFileRoute("/$workspaceId/token")({
8+
component: RouteComponent,
9+
loader: ({ params: { workspaceId } }) =>
10+
RuntimeClient.runPromise(
11+
Effect.gen(function* () {
12+
const api = yield* ApiClient;
13+
const token = yield* WorkspaceManager.getById({ workspaceId }).pipe(
14+
Effect.flatMap((workspace) => Effect.fromNullable(workspace?.token))
15+
);
16+
17+
return yield* api.client.syncAuth.listTokens({
18+
path: { workspaceId },
19+
headers: { "x-api-key": token },
20+
});
21+
})
22+
),
23+
});
24+
25+
function RouteComponent() {
26+
const tokens = Route.useLoaderData();
27+
return (
28+
<div>
29+
<h1>Tokens</h1>
30+
<table>
31+
<thead>
32+
<tr>
33+
<th>Index</th>
34+
<th>clientId</th>
35+
<th>isMaster</th>
36+
<th>scope</th>
37+
<th>issuedAt</th>
38+
<th>expired</th>
39+
<th>revoked</th>
40+
</tr>
41+
</thead>
42+
<tbody>
43+
{tokens.map((token, index) => (
44+
<tr key={index}>
45+
<td>{index}</td>
46+
<td>{token.clientId}</td>
47+
<td>{token.isMaster ? "✔️" : "❌"}</td>
48+
<td>{token.scope}</td>
49+
<td>
50+
{new Date(token.issuedAt).toLocaleDateString(undefined, {
51+
year: "numeric",
52+
month: "long",
53+
day: "numeric",
54+
})}
55+
</td>
56+
<td>
57+
{token.expiresAt
58+
? new Date(token.expiresAt).toLocaleDateString(undefined, {
59+
year: "numeric",
60+
month: "long",
61+
day: "numeric",
62+
})
63+
: "N/A"}
64+
</td>
65+
<td>
66+
{token.revokedAt
67+
? new Date(token.revokedAt).toLocaleDateString(undefined, {
68+
year: "numeric",
69+
month: "long",
70+
day: "numeric",
71+
})
72+
: "N/A"}
73+
</td>
74+
</tr>
75+
))}
76+
</tbody>
77+
</table>
78+
</div>
79+
);
80+
}
File renamed without changes.

apps/server/drizzle/meta/0000_snapshot.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"id": "710fed2b-4b98-4e23-b4d0-d6dc775c98fe",
2+
"id": "b089a447-bbae-4d52-b83e-a0ce1cc4b359",
33
"prevId": "00000000-0000-0000-0000-000000000000",
44
"version": "7",
55
"dialect": "postgresql",

apps/server/drizzle/meta/_journal.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55
{
66
"idx": 0,
77
"version": "7",
8-
"when": 1740722484842,
9-
"tag": "0000_parallel_jigsaw",
8+
"when": 1740889946454,
9+
"tag": "0000_supreme_bedlam",
1010
"breakpoints": true
1111
}
1212
]

apps/server/src/group/sync-auth.ts

Lines changed: 51 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
import { HttpApiBuilder } from "@effect/platform";
2-
import { SyncApi, type Scope } from "@local/sync";
3-
import { eq } from "drizzle-orm";
2+
import { AuthWorkspace, SyncApi, type Scope } from "@local/sync";
3+
import { and, eq } from "drizzle-orm";
44
import { Effect, Layer, Schema } from "effect";
55
import { tokenTable, workspaceTable } from "../db/schema";
6-
import { Drizzle } from "../drizzle";
76
import { AuthorizationLive } from "../middleware/authorization";
7+
import { MasterAuthorizationLive } from "../middleware/master-authorization";
8+
import { Drizzle } from "../services/drizzle";
89
import { Jwt } from "../services/jwt";
910

1011
export const SyncAuthGroupLive = HttpApiBuilder.group(
@@ -18,6 +19,10 @@ export const SyncAuthGroupLive = HttpApiBuilder.group(
1819
return handlers
1920
.handle("generateToken", ({ payload }) =>
2021
Effect.gen(function* () {
22+
yield* Effect.log(
23+
`Generating token for workspace ${payload.workspaceId}`
24+
);
25+
2126
const scope: typeof Scope.Type = "read_write";
2227
const isMaster = true;
2328
const issuedAt = new Date();
@@ -37,7 +42,7 @@ export const SyncAuthGroupLive = HttpApiBuilder.group(
3742
)
3843
);
3944

40-
const token = jwt.sign({
45+
const token = yield* jwt.sign({
4146
clientId: payload.clientId,
4247
workspaceId: payload.workspaceId,
4348
});
@@ -97,14 +102,52 @@ export const SyncAuthGroupLive = HttpApiBuilder.group(
97102
Effect.mapError((error) => error.message)
98103
)
99104
)
100-
.handle("issueToken", ({ path: { workspaceId } }) =>
101-
Effect.fail("Not implemented")
102-
)
103105
.handle("listTokens", ({ path: { workspaceId } }) =>
106+
Effect.gen(function* () {
107+
const workspace = yield* AuthWorkspace;
108+
const tokens = yield* query({
109+
Request: Schema.Struct({
110+
workspaceId: Schema.String,
111+
clientId: Schema.String,
112+
}),
113+
execute: (db, { workspaceId, clientId }) =>
114+
db
115+
.select()
116+
.from(tokenTable)
117+
.where(
118+
and(
119+
eq(tokenTable.workspaceId, workspaceId),
120+
eq(tokenTable.clientId, clientId)
121+
)
122+
),
123+
})({ workspaceId, clientId: workspace.clientId });
124+
125+
return tokens.map((token) => ({
126+
clientId: token.clientId,
127+
tokenValue: token.tokenValue,
128+
scope: token.scope,
129+
isMaster: token.isMaster,
130+
issuedAt: token.issuedAt,
131+
expiresAt: token.expiresAt,
132+
revokedAt: token.revokedAt,
133+
}));
134+
}).pipe(
135+
Effect.tapErrorCause(Effect.logError),
136+
Effect.mapError((error) => error.message)
137+
)
138+
)
139+
.handle("issueToken", ({ path: { workspaceId } }) =>
104140
Effect.fail("Not implemented")
105141
)
106142
.handle("revokeToken", ({ path: { workspaceId } }) =>
107143
Effect.fail("Not implemented")
108144
);
109145
})
110-
).pipe(Layer.provide([Drizzle.Default, AuthorizationLive, Jwt.Default]));
146+
).pipe(
147+
Layer.provide([
148+
Drizzle.Default,
149+
AuthorizationLive,
150+
MasterAuthorizationLive,
151+
Jwt.Default,
152+
])
153+
);

apps/server/src/group/sync-data.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { AuthWorkspace, SyncApi } from "@local/sync";
33
import { SnapshotToLoroDoc } from "@local/sync/loro";
44
import { Array, Effect, Layer, Schema } from "effect";
55
import { workspaceTable } from "../db/schema";
6-
import { Drizzle } from "../drizzle";
76
import { AuthorizationLive } from "../middleware/authorization";
7+
import { Drizzle } from "../services/drizzle";
88

99
export const SyncDataGroupLive = HttpApiBuilder.group(
1010
SyncApi,

apps/server/src/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@ import {
1212
import { SyncApi } from "@local/sync";
1313
import { Effect, Layer } from "effect";
1414
import { createServer } from "node:http";
15-
import { Drizzle } from "./drizzle";
1615
import { SyncAuthGroupLive } from "./group/sync-auth";
1716
import { SyncDataGroupLive } from "./group/sync-data";
17+
import { Drizzle } from "./services/drizzle";
1818

1919
const EnvProviderLayer = Layer.unwrapEffect(
2020
PlatformConfigProvider.fromDotEnv(".env").pipe(

0 commit comments

Comments
 (0)