create-m5kdev 0.39.0 → 0.40.1
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.
- package/package.json +1 -1
- package/templates/minimal-app/apps/e2e/tests/expo-standard.spec.ts.tpl +14 -16
- package/templates/minimal-app/apps/e2e/tests/files.spec.ts.tpl +4 -0
- package/templates/minimal-app/apps/e2e/tests/standard.spec.ts.tpl +39 -17
- package/templates/minimal-app/apps/webapp/src/modules/files/FilesRoute.tsx.tpl +24 -0
- package/templates/minimal-app/apps/webapp/src/modules/files/hooks/useFilesRoute.ts.tpl +33 -5
- package/templates/minimal-app/apps/webapp/translations/en/{{APP_SLUG}}.json.tpl +7 -3
- package/templates/minimal-app/pnpm-workspace.yaml.tpl +12 -12
package/package.json
CHANGED
|
@@ -636,28 +636,26 @@ test("session management endpoints list and revoke sessions", async ({ page, req
|
|
|
636
636
|
await expectAuthOk(revokeResponse);
|
|
637
637
|
});
|
|
638
638
|
|
|
639
|
-
test("
|
|
640
|
-
const email = `expo.
|
|
639
|
+
test("Close User refuses login until Admin Restore", async ({ page, request }) => {
|
|
640
|
+
const email = `expo.close.${Date.now()}@auth-e2e.local`;
|
|
641
641
|
const password = "password1234";
|
|
642
642
|
|
|
643
643
|
await createVerifiedExpoAccount(page, request, email, password);
|
|
644
644
|
await expoLoginNative(page, email, password);
|
|
645
645
|
|
|
646
|
-
const
|
|
647
|
-
|
|
648
|
-
body: {
|
|
649
|
-
callbackURL: `${profiles[profile].webUrl}/login`,
|
|
650
|
-
},
|
|
651
|
-
});
|
|
652
|
-
await expectAuthOk(deleteResponse);
|
|
646
|
+
const userTrpc = await createExpoTrpcClient(page, profile);
|
|
647
|
+
await userTrpc.auth.closeUser.mutate({ email });
|
|
653
648
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
expect(
|
|
649
|
+
await expectExpoLoginRejectedNative(page, email, password);
|
|
650
|
+
|
|
651
|
+
await expoLoginNative(page, profiles[profile].adminEmail, profiles[profile].adminPassword);
|
|
652
|
+
const adminTrpc = await createExpoTrpcClient(page, profile);
|
|
653
|
+
const users = await adminTrpc.auth.searchAdminUsers.query({ q: email, limit: 10 });
|
|
654
|
+
const user = users.rows.find((row) => row.email === email);
|
|
655
|
+
expect(user).toBeTruthy();
|
|
656
|
+
await adminTrpc.auth.restoreUser.mutate({ id: user?.id ?? "" });
|
|
657
|
+
|
|
658
|
+
await expoLoginNative(page, email, password);
|
|
661
659
|
});
|
|
662
660
|
|
|
663
661
|
test("admin can create and switch to a child organization from an enterprise org", async ({
|
|
@@ -32,4 +32,8 @@ test("upload a file and see it inventoried", async ({ page, request }) => {
|
|
|
32
32
|
await expect(page.getByTestId("file-row").first()).toContainText("hello.png", {
|
|
33
33
|
timeout: 15_000,
|
|
34
34
|
});
|
|
35
|
+
await expect(page.getByTestId("file-download")).toHaveAttribute(
|
|
36
|
+
"href",
|
|
37
|
+
/\/files\/[0-9a-f-]{36}$/i
|
|
38
|
+
);
|
|
35
39
|
});
|
|
@@ -781,31 +781,53 @@ test("session management endpoints list and revoke sessions", async ({ page, req
|
|
|
781
781
|
).toBe(true);
|
|
782
782
|
});
|
|
783
783
|
|
|
784
|
-
test("
|
|
785
|
-
|
|
784
|
+
test("Close account from preferences refuses login until Admin Restore", async ({
|
|
785
|
+
page,
|
|
786
|
+
request,
|
|
787
|
+
}) => {
|
|
788
|
+
const email = `close.${Date.now()}@auth-e2e.local`;
|
|
786
789
|
const password = "password1234";
|
|
787
790
|
|
|
788
791
|
await createVerifiedAccount(page, request, email, password);
|
|
789
792
|
await login(page, email, password);
|
|
790
793
|
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
794
|
+
await page.goto("/user/preferences");
|
|
795
|
+
await page.getByPlaceholder(email).fill(email);
|
|
796
|
+
const closeResponsePromise = page.waitForResponse(
|
|
797
|
+
(response) =>
|
|
798
|
+
response.request().method() === "POST" && response.url().includes("closeUser")
|
|
799
|
+
);
|
|
800
|
+
await page.getByRole("button", { name: /close account/i }).click();
|
|
801
|
+
const closeResponse = await closeResponsePromise;
|
|
797
802
|
expect(
|
|
798
|
-
|
|
799
|
-
`${
|
|
803
|
+
closeResponse.ok(),
|
|
804
|
+
`${closeResponse.status()} ${closeResponse.statusText()}: ${await closeResponse.text()}`
|
|
800
805
|
).toBe(true);
|
|
806
|
+
await expect(page).toHaveURL(/\/login/);
|
|
801
807
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
expect(
|
|
808
|
+
await expectLoginRejected(page, email, password);
|
|
809
|
+
|
|
810
|
+
await login(page, profiles.standard.adminEmail, profiles.standard.adminPassword);
|
|
811
|
+
await page.goto("/admin/users");
|
|
812
|
+
await page.locator('input[name="search"]').fill(email);
|
|
813
|
+
const row = page.getByRole("row").filter({ hasText: email });
|
|
814
|
+
await expect(row).toBeVisible();
|
|
815
|
+
await expect(row.getByText(/^closed$/i)).toBeVisible();
|
|
816
|
+
await row.getByLabel("User actions").click();
|
|
817
|
+
const restoreResponsePromise = page.waitForResponse(
|
|
818
|
+
(response) =>
|
|
819
|
+
response.request().method() === "POST" && response.url().includes("restoreUser")
|
|
820
|
+
);
|
|
821
|
+
await page.getByRole("menuitem", { name: /^restore$/i }).click();
|
|
822
|
+
const restoreResponse = await restoreResponsePromise;
|
|
823
|
+
expect(
|
|
824
|
+
restoreResponse.ok(),
|
|
825
|
+
`${restoreResponse.status()} ${restoreResponse.statusText()}: ${await restoreResponse.text()}`
|
|
826
|
+
).toBe(true);
|
|
827
|
+
await expect(row.getByText(/^active$/i)).toBeVisible();
|
|
828
|
+
|
|
829
|
+
await logout(page);
|
|
830
|
+
await login(page, email, password);
|
|
809
831
|
});
|
|
810
832
|
|
|
811
833
|
test("admin can create and switch to a child organization from an enterprise org", async ({
|
|
@@ -79,6 +79,30 @@ export function FilesRoute() {
|
|
|
79
79
|
</Chip>
|
|
80
80
|
) : null}
|
|
81
81
|
</Card.Header>
|
|
82
|
+
<Card.Content className="flex flex-wrap items-center gap-3">
|
|
83
|
+
<img
|
|
84
|
+
src={row.downloadUrl}
|
|
85
|
+
alt={row.originalName}
|
|
86
|
+
className="h-16 w-16 rounded-md object-cover"
|
|
87
|
+
data-testid="file-preview"
|
|
88
|
+
/>
|
|
89
|
+
<a
|
|
90
|
+
href={row.downloadUrl}
|
|
91
|
+
data-testid="file-download"
|
|
92
|
+
className="text-sm font-medium underline"
|
|
93
|
+
>
|
|
94
|
+
{t("files.row.open")}
|
|
95
|
+
</a>
|
|
96
|
+
<Button
|
|
97
|
+
size="sm"
|
|
98
|
+
variant="ghost"
|
|
99
|
+
data-testid="file-delete"
|
|
100
|
+
isDisabled={files.isDeleting}
|
|
101
|
+
onPress={() => files.remove(row.id)}
|
|
102
|
+
>
|
|
103
|
+
{t("files.row.delete")}
|
|
104
|
+
</Button>
|
|
105
|
+
</Card.Content>
|
|
82
106
|
</Card>
|
|
83
107
|
))}
|
|
84
108
|
</div>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { useAppConfig } from "@m5kdev/frontend/modules/app/hooks/useAppConfig";
|
|
2
|
+
import { fileCookieDownloadUrl } from "@m5kdev/frontend/modules/file/hooks/useS3DownloadUrl";
|
|
3
|
+
import { useS3Upload } from "@m5kdev/frontend/modules/file/hooks/useS3Upload";
|
|
2
4
|
import type { AppRouter } from "{{PACKAGE_SCOPE}}/server/types";
|
|
3
5
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
4
6
|
import type { inferRouterOutputs } from "@trpc/server";
|
|
@@ -9,25 +11,32 @@ import { useTRPC } from "@/utils/trpc";
|
|
|
9
11
|
type FileListOutput = inferRouterOutputs<AppRouter>["file"]["list"];
|
|
10
12
|
type FileRow = FileListOutput["rows"][number];
|
|
11
13
|
|
|
14
|
+
interface FilesRouteRow extends FileRow {
|
|
15
|
+
readonly downloadUrl: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
interface FilesRouteState {
|
|
13
|
-
readonly rows:
|
|
19
|
+
readonly rows: FilesRouteRow[];
|
|
14
20
|
readonly isLoading: boolean;
|
|
15
21
|
readonly isFetching: boolean;
|
|
16
22
|
readonly isUploading: boolean;
|
|
23
|
+
readonly isDeleting: boolean;
|
|
17
24
|
readonly upload: (file: File) => void;
|
|
25
|
+
readonly remove: (fileId: string) => void;
|
|
18
26
|
}
|
|
19
27
|
|
|
20
28
|
export function useFilesRoute(): FilesRouteState {
|
|
21
29
|
const { t } = useTranslation("{{APP_SLUG}}");
|
|
30
|
+
const { serverUrl } = useAppConfig();
|
|
22
31
|
const trpc = useTRPC();
|
|
23
32
|
const queryClient = useQueryClient();
|
|
24
|
-
const fileUpload =
|
|
33
|
+
const fileUpload = useS3Upload({ scope: "organization" });
|
|
25
34
|
|
|
26
35
|
const listQuery = useQuery(trpc.file.list.queryOptions({}));
|
|
27
36
|
|
|
28
37
|
const uploadMutation = useMutation({
|
|
29
38
|
mutationFn: async (file: File) => {
|
|
30
|
-
await fileUpload.upload(
|
|
39
|
+
await fileUpload.upload(file);
|
|
31
40
|
},
|
|
32
41
|
onSuccess: async () => {
|
|
33
42
|
toast.success(t("files.toast.uploaded"));
|
|
@@ -38,13 +47,32 @@ export function useFilesRoute(): FilesRouteState {
|
|
|
38
47
|
},
|
|
39
48
|
});
|
|
40
49
|
|
|
50
|
+
const deleteMutation = useMutation(
|
|
51
|
+
trpc.file.delete.mutationOptions({
|
|
52
|
+
onSuccess: async () => {
|
|
53
|
+
toast.success(t("files.toast.deleted"));
|
|
54
|
+
await queryClient.invalidateQueries(trpc.file.list.queryFilter());
|
|
55
|
+
},
|
|
56
|
+
onError: (error) => {
|
|
57
|
+
toast.error(error instanceof Error ? error.message : t("files.toast.deleteFailed"));
|
|
58
|
+
},
|
|
59
|
+
})
|
|
60
|
+
);
|
|
61
|
+
|
|
41
62
|
return {
|
|
42
|
-
rows: listQuery.data?.rows ?? []
|
|
63
|
+
rows: (listQuery.data?.rows ?? []).map((row) => ({
|
|
64
|
+
...row,
|
|
65
|
+
downloadUrl: fileCookieDownloadUrl(serverUrl, row.id),
|
|
66
|
+
})),
|
|
43
67
|
isLoading: listQuery.isLoading,
|
|
44
68
|
isFetching: listQuery.isFetching,
|
|
45
69
|
isUploading: uploadMutation.isPending,
|
|
70
|
+
isDeleting: deleteMutation.isPending,
|
|
46
71
|
upload: (file: File) => {
|
|
47
72
|
void uploadMutation.mutateAsync(file).catch(() => undefined);
|
|
48
73
|
},
|
|
74
|
+
remove: (fileId: string) => {
|
|
75
|
+
void deleteMutation.mutateAsync({ fileId }).catch(() => undefined);
|
|
76
|
+
},
|
|
49
77
|
};
|
|
50
78
|
}
|
|
@@ -168,7 +168,7 @@
|
|
|
168
168
|
"hero": {
|
|
169
169
|
"eyebrow": "Files",
|
|
170
170
|
"title": "Upload an image and see it inventoried.",
|
|
171
|
-
"body": "
|
|
171
|
+
"body": "The browser PUTs to S3 after initiate, then finalize inventories the File. Open uses /files/:id.",
|
|
172
172
|
"upload": "Upload image",
|
|
173
173
|
"syncing": "Refreshing",
|
|
174
174
|
"synced": "Live against the files table"
|
|
@@ -178,11 +178,15 @@
|
|
|
178
178
|
"body": "Upload an image to see it listed here."
|
|
179
179
|
},
|
|
180
180
|
"row": {
|
|
181
|
-
"organization": "Organization"
|
|
181
|
+
"organization": "Organization",
|
|
182
|
+
"open": "Open",
|
|
183
|
+
"delete": "Delete"
|
|
182
184
|
},
|
|
183
185
|
"toast": {
|
|
184
186
|
"uploaded": "File uploaded.",
|
|
185
|
-
"failed": "Upload failed."
|
|
187
|
+
"failed": "Upload failed.",
|
|
188
|
+
"deleted": "File deleted.",
|
|
189
|
+
"deleteFailed": "Delete failed."
|
|
186
190
|
}
|
|
187
191
|
},
|
|
188
192
|
"notifications": {
|
|
@@ -7,18 +7,18 @@ catalogs:
|
|
|
7
7
|
"@heroui/react": 3.0.3
|
|
8
8
|
"@heroui/styles": 3.0.3
|
|
9
9
|
"@libsql/client": 0.17.4
|
|
10
|
-
"@m5kdev/backend": 0.
|
|
11
|
-
"@m5kdev/commons": 0.
|
|
12
|
-
"@m5kdev/config": 0.
|
|
13
|
-
"@m5kdev/email": 0.
|
|
14
|
-
"@m5kdev/expo": 0.
|
|
15
|
-
"@m5kdev/frontend": 0.
|
|
16
|
-
"@m5kdev/module-clay": 0.
|
|
17
|
-
"@m5kdev/module-docx": 0.
|
|
18
|
-
"@m5kdev/module-pdf": 0.
|
|
19
|
-
"@m5kdev/module-social": 0.
|
|
20
|
-
"@m5kdev/module-video": 0.
|
|
21
|
-
"@m5kdev/web-ui": 0.
|
|
10
|
+
"@m5kdev/backend": 0.40.1
|
|
11
|
+
"@m5kdev/commons": 0.40.1
|
|
12
|
+
"@m5kdev/config": 0.40.1
|
|
13
|
+
"@m5kdev/email": 0.40.1
|
|
14
|
+
"@m5kdev/expo": 0.40.1
|
|
15
|
+
"@m5kdev/frontend": 0.40.1
|
|
16
|
+
"@m5kdev/module-clay": 0.40.1
|
|
17
|
+
"@m5kdev/module-docx": 0.40.1
|
|
18
|
+
"@m5kdev/module-pdf": 0.40.1
|
|
19
|
+
"@m5kdev/module-social": 0.40.1
|
|
20
|
+
"@m5kdev/module-video": 0.40.1
|
|
21
|
+
"@m5kdev/web-ui": 0.40.1
|
|
22
22
|
"@mastra/core": 1.51.0
|
|
23
23
|
"@openrouter/ai-sdk-provider": 3.0.0
|
|
24
24
|
"@opentelemetry/api": 1.9.0
|