mirror of
https://github.com/nimbusdotstorage/Nimbus
synced 2026-04-22 17:45:03 +02:00
TANNNERRRRRRRROUTTERRRRRR
This commit is contained in:
1
.rules/GENERAL.md
Normal file
1
.rules/GENERAL.md
Normal file
@@ -0,0 +1 @@
|
||||
- Only do the minimum required changes to accomplish the stated task.
|
||||
@@ -272,14 +272,6 @@ Make sure you have configured the Google OAuth credentials in your `.env` file a
|
||||
|
||||
If you want to contribute, please refer to the [contributing guide](https://github.com/nimbusdotstorage/Nimbus/blob/main/CONTRIBUTING.md)
|
||||
|
||||
### 9. Tests
|
||||
|
||||
Vitest and MinIO via Docker are used for testing. To run tests, use must use `bun run test` becasuse `bun test` is reserved for `bun` (just like `bun build` etc).
|
||||
|
||||
Tests **require** the `docker-compose.test.yml` to be running.
|
||||
|
||||
Look at the scripts in `package.json` for all the `test:*` options.
|
||||
|
||||
## Deploying Docker images (ex. Coolify)
|
||||
|
||||
Follow the [DEPLOYMENT.md](DEPLOYMENT.md) file for instructions on how to deploy with Coolify.
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@microsoft/microsoft-graph-types": "^2.40.0",
|
||||
"@nimbus/vitest": "workspace:*",
|
||||
"@types/pg": "^8.15.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,566 +0,0 @@
|
||||
import {
|
||||
createFileMetadata,
|
||||
createFolderMetadata,
|
||||
createProviderWithFreshMockClient,
|
||||
createProviderWithMockClient,
|
||||
mockDropboxClient,
|
||||
mockResponses,
|
||||
resetAllMocks,
|
||||
restoreMockClient,
|
||||
} from "./test-utils";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { DropboxProvider } from "../dropbox-provider";
|
||||
import { Readable } from "node:stream";
|
||||
|
||||
describe("DropboxProvider", () => {
|
||||
let provider: DropboxProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
resetAllMocks();
|
||||
provider = createProviderWithMockClient();
|
||||
// Ensure mock client is properly set
|
||||
restoreMockClient(provider);
|
||||
});
|
||||
|
||||
describe("Constructor", () => {
|
||||
it("should create DropboxProvider with access token", () => {
|
||||
const dropboxProvider = new DropboxProvider("test-token");
|
||||
expect(dropboxProvider).toBeInstanceOf(DropboxProvider);
|
||||
expect(dropboxProvider.getAccessToken()).toBe("test-token");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authentication Interface", () => {
|
||||
it("should get and set access token", () => {
|
||||
const authProvider = createProviderWithMockClient();
|
||||
expect(authProvider.getAccessToken()).toBe("mock-access-token");
|
||||
|
||||
authProvider.setAccessToken("new-token");
|
||||
restoreMockClient(authProvider);
|
||||
expect(authProvider.getAccessToken()).toBe("new-token");
|
||||
});
|
||||
});
|
||||
|
||||
describe("create", () => {
|
||||
it("should create a folder", async () => {
|
||||
const folderMetadata = createFolderMetadata();
|
||||
mockDropboxClient.filesCreateFolderV2.mockResolvedValue(mockResponses.createFolder);
|
||||
|
||||
const result = await provider.create(folderMetadata);
|
||||
|
||||
expect(mockDropboxClient.filesCreateFolderV2).toHaveBeenCalledWith({
|
||||
path: "/test-folder",
|
||||
autorename: false,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
id: "/test-folder",
|
||||
name: "test-folder",
|
||||
mimeType: "application/x-directory",
|
||||
size: 0,
|
||||
createdTime: expect.any(String),
|
||||
modifiedTime: expect.any(String),
|
||||
type: "folder",
|
||||
parentId: "",
|
||||
trashed: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a file with Buffer content", async () => {
|
||||
const fileMetadata = createFileMetadata();
|
||||
const content = Buffer.from("test content");
|
||||
mockDropboxClient.filesUpload.mockResolvedValue(mockResponses.uploadFile);
|
||||
|
||||
const result = await provider.create(fileMetadata, content);
|
||||
|
||||
expect(mockDropboxClient.filesUpload).toHaveBeenCalledWith({
|
||||
path: "/test-file.txt",
|
||||
contents: content,
|
||||
mode: { ".tag": "add" },
|
||||
autorename: false,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
id: "/test-file.txt",
|
||||
name: "test-file.txt",
|
||||
mimeType: "text/plain",
|
||||
size: 11,
|
||||
createdTime: "2023-01-01T12:00:00Z",
|
||||
modifiedTime: "2023-01-01T12:00:00Z",
|
||||
type: "file",
|
||||
parentId: "",
|
||||
trashed: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should create a file with Readable stream content", async () => {
|
||||
const fileMetadata = createFileMetadata();
|
||||
const content = Readable.from(["test content"]);
|
||||
mockDropboxClient.filesUpload.mockResolvedValue(mockResponses.uploadFile);
|
||||
|
||||
const result = await provider.create(fileMetadata, content);
|
||||
|
||||
expect(mockDropboxClient.filesUpload).toHaveBeenCalled();
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should throw error when creating file without content", async () => {
|
||||
const fileMetadata = createFileMetadata();
|
||||
|
||||
await expect(provider.create(fileMetadata)).rejects.toThrow("Content is required for file upload");
|
||||
});
|
||||
|
||||
it("should handle nested folder creation", async () => {
|
||||
const folderMetadata = createFolderMetadata({
|
||||
name: "nested-folder",
|
||||
parentId: "/parent-folder",
|
||||
});
|
||||
mockDropboxClient.filesCreateFolderV2.mockResolvedValue({
|
||||
result: {
|
||||
metadata: {
|
||||
...mockResponses.createFolder.result.metadata,
|
||||
path_display: "/parent-folder/nested-folder",
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await provider.create(folderMetadata);
|
||||
|
||||
expect(mockDropboxClient.filesCreateFolderV2).toHaveBeenCalledWith({
|
||||
path: "/parent-folder/nested-folder",
|
||||
autorename: false,
|
||||
});
|
||||
expect(result?.parentId).toBe("/parent-folder");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getById", () => {
|
||||
it("should get file by ID", async () => {
|
||||
mockDropboxClient.filesGetMetadata.mockResolvedValue(mockResponses.getMetadata);
|
||||
|
||||
const result = await provider.getById("/test-file.txt");
|
||||
|
||||
expect(mockDropboxClient.filesGetMetadata).toHaveBeenCalledWith({
|
||||
path: "/test-file.txt",
|
||||
include_media_info: false,
|
||||
include_deleted: false,
|
||||
});
|
||||
expect(result).toEqual({
|
||||
id: "/test-file.txt",
|
||||
name: "test-file.txt",
|
||||
mimeType: "text/plain",
|
||||
size: 11,
|
||||
createdTime: "2023-01-01T12:00:00Z",
|
||||
modifiedTime: "2023-01-01T12:00:00Z",
|
||||
type: "file",
|
||||
parentId: "",
|
||||
trashed: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return null when file not found", async () => {
|
||||
const notFoundError = {
|
||||
error: {
|
||||
error: {
|
||||
".tag": "path",
|
||||
path: {
|
||||
".tag": "not_found",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
mockDropboxClient.filesGetMetadata.mockRejectedValue(notFoundError);
|
||||
|
||||
const result = await provider.getById("/nonexistent.txt");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("update", () => {
|
||||
it("should rename/move file", async () => {
|
||||
const isolatedProvider = createProviderWithFreshMockClient();
|
||||
const isolatedMockClient = (isolatedProvider as any).client;
|
||||
const updateMetadata = { name: "renamed-file.txt", parentId: "", mimeType: "text/plain" };
|
||||
|
||||
isolatedMockClient.filesMoveV2.mockResolvedValue(mockResponses.moveFile);
|
||||
|
||||
const result = await isolatedProvider.update("/test-file.txt", updateMetadata);
|
||||
|
||||
expect(isolatedMockClient.filesMoveV2).toHaveBeenCalledWith({
|
||||
from_path: "/test-file.txt",
|
||||
to_path: "/renamed-file.txt",
|
||||
autorename: false,
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should move file to different folder", async () => {
|
||||
const updateMetadata = { name: "test-file.txt", parentId: "/new-folder", mimeType: "text/plain" };
|
||||
mockDropboxClient.filesMoveV2.mockResolvedValue(mockResponses.moveFile);
|
||||
|
||||
const _result = await provider.update("/test-file.txt", updateMetadata);
|
||||
|
||||
expect(mockDropboxClient.filesMoveV2).toHaveBeenCalledWith({
|
||||
from_path: "/test-file.txt",
|
||||
to_path: "/new-folder/test-file.txt",
|
||||
autorename: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("delete", () => {
|
||||
it("should delete file", async () => {
|
||||
mockDropboxClient.filesDeleteV2.mockResolvedValue({});
|
||||
|
||||
const result = await provider.delete("/test-file.txt");
|
||||
|
||||
expect(mockDropboxClient.filesDeleteV2).toHaveBeenCalledWith({
|
||||
path: "/test-file.txt",
|
||||
});
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it("should handle delete errors", async () => {
|
||||
mockDropboxClient.filesDeleteV2.mockRejectedValue(new Error("Delete failed"));
|
||||
|
||||
await expect(provider.delete("/test-file.txt")).rejects.toThrow("Delete failed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("listChildren", () => {
|
||||
it("should list files in root folder", async () => {
|
||||
mockDropboxClient.filesListFolder.mockResolvedValue(mockResponses.listFolder);
|
||||
|
||||
const result = await provider.listChildren();
|
||||
|
||||
expect(mockDropboxClient.filesListFolder).toHaveBeenCalledWith({
|
||||
path: "",
|
||||
recursive: false,
|
||||
include_media_info: false,
|
||||
include_deleted: false,
|
||||
include_has_explicit_shared_members: false,
|
||||
limit: 100,
|
||||
});
|
||||
expect(result.items).toHaveLength(2);
|
||||
expect(result.nextPageToken).toBe("cursor-123");
|
||||
});
|
||||
|
||||
it("should list files in specific folder", async () => {
|
||||
mockDropboxClient.filesListFolder.mockResolvedValue(mockResponses.listFolder);
|
||||
|
||||
const _result = await provider.listChildren("/test-folder");
|
||||
|
||||
expect(mockDropboxClient.filesListFolder).toHaveBeenCalledWith({
|
||||
path: "/test-folder",
|
||||
recursive: false,
|
||||
include_media_info: false,
|
||||
include_deleted: false,
|
||||
include_has_explicit_shared_members: false,
|
||||
limit: 100,
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle pagination with page token", async () => {
|
||||
mockDropboxClient.filesListFolderContinue.mockResolvedValue(mockResponses.listFolder);
|
||||
|
||||
const _result = await provider.listChildren("", { pageToken: "next-cursor" });
|
||||
|
||||
expect(mockDropboxClient.filesListFolderContinue).toHaveBeenCalledWith({
|
||||
cursor: "next-cursor",
|
||||
});
|
||||
});
|
||||
|
||||
it("should respect page size limit", async () => {
|
||||
mockDropboxClient.filesListFolder.mockResolvedValue(mockResponses.listFolder);
|
||||
|
||||
await provider.listChildren("", { pageSize: 50 });
|
||||
|
||||
expect(mockDropboxClient.filesListFolder).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
limit: 50,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should limit page size to Dropbox maximum", async () => {
|
||||
mockDropboxClient.filesListFolder.mockResolvedValue(mockResponses.listFolder);
|
||||
|
||||
await provider.listChildren("", { pageSize: 5000 });
|
||||
|
||||
expect(mockDropboxClient.filesListFolder).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
limit: 2000, // Dropbox max
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("download", () => {
|
||||
it("should download file", async () => {
|
||||
mockDropboxClient.filesDownload.mockResolvedValue(mockResponses.download);
|
||||
|
||||
const result = await provider.download("/test-file.txt");
|
||||
|
||||
expect(mockDropboxClient.filesDownload).toHaveBeenCalledWith({
|
||||
path: "/test-file.txt",
|
||||
});
|
||||
expect(result).toEqual({
|
||||
data: Buffer.from("test content"),
|
||||
filename: "test-file.txt",
|
||||
mimeType: "text/plain",
|
||||
size: 11,
|
||||
});
|
||||
});
|
||||
|
||||
it("should return null on download error", async () => {
|
||||
const notFoundError = {
|
||||
error: {
|
||||
error: {
|
||||
".tag": "path",
|
||||
path: {
|
||||
".tag": "not_found",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
mockDropboxClient.filesDownload.mockRejectedValue(notFoundError);
|
||||
|
||||
const result = await provider.download("/test-file.txt");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("copy", () => {
|
||||
it("should copy file", async () => {
|
||||
mockDropboxClient.filesCopyV2.mockResolvedValue(mockResponses.copy);
|
||||
|
||||
const result = await provider.copy("/source.txt", "/target-folder");
|
||||
|
||||
expect(mockDropboxClient.filesCopyV2).toHaveBeenCalledWith({
|
||||
from_path: "/source.txt",
|
||||
to_path: "/target-folder/source.txt",
|
||||
autorename: false,
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should copy file with new name", async () => {
|
||||
mockDropboxClient.filesCopyV2.mockResolvedValue(mockResponses.copy);
|
||||
|
||||
const _result = await provider.copy("/source.txt", "/target-folder", "new-name.txt");
|
||||
|
||||
expect(mockDropboxClient.filesCopyV2).toHaveBeenCalledWith({
|
||||
from_path: "/source.txt",
|
||||
to_path: "/target-folder/new-name.txt",
|
||||
autorename: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("move", () => {
|
||||
it("should move file", async () => {
|
||||
mockDropboxClient.filesMoveV2.mockResolvedValue(mockResponses.moveFile);
|
||||
|
||||
const result = await provider.move("/source.txt", "/target-folder");
|
||||
|
||||
expect(mockDropboxClient.filesMoveV2).toHaveBeenCalledWith({
|
||||
from_path: "/source.txt",
|
||||
to_path: "/target-folder/source.txt",
|
||||
autorename: false,
|
||||
});
|
||||
expect(result).not.toBeNull();
|
||||
});
|
||||
|
||||
it("should move file with new name", async () => {
|
||||
mockDropboxClient.filesMoveV2.mockResolvedValue(mockResponses.moveFile);
|
||||
|
||||
const _result = await provider.move("/source.txt", "/target-folder", "new-name.txt");
|
||||
|
||||
expect(mockDropboxClient.filesMoveV2).toHaveBeenCalledWith({
|
||||
from_path: "/source.txt",
|
||||
to_path: "/target-folder/new-name.txt",
|
||||
autorename: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDriveInfo", () => {
|
||||
it("should get drive information", async () => {
|
||||
mockDropboxClient.usersGetSpaceUsage.mockResolvedValue(mockResponses.spaceUsage);
|
||||
|
||||
const result = await provider.getDriveInfo();
|
||||
|
||||
expect(result).toEqual({
|
||||
totalSpace: 2000000000,
|
||||
usedSpace: 1000000,
|
||||
trashSize: 0,
|
||||
trashItems: 0,
|
||||
fileCount: 0,
|
||||
state: "normal",
|
||||
});
|
||||
});
|
||||
|
||||
it("should return null on error", async () => {
|
||||
mockDropboxClient.usersGetSpaceUsage.mockRejectedValue(new Error("API error"));
|
||||
|
||||
const result = await provider.getDriveInfo();
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle team allocation", async () => {
|
||||
mockDropboxClient.usersGetSpaceUsage.mockResolvedValue({
|
||||
result: {
|
||||
used: 1000000,
|
||||
allocation: {
|
||||
".tag": "team",
|
||||
allocated: 5000000000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const result = await provider.getDriveInfo();
|
||||
|
||||
expect(result?.totalSpace).toBe(5000000000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getShareableLink", () => {
|
||||
it("should create shareable link", async () => {
|
||||
mockDropboxClient.sharingCreateSharedLinkWithSettings.mockResolvedValue(mockResponses.shareableLink);
|
||||
|
||||
const result = await provider.getShareableLink("/test-file.txt");
|
||||
|
||||
expect(mockDropboxClient.sharingCreateSharedLinkWithSettings).toHaveBeenCalledWith({
|
||||
path: "/test-file.txt",
|
||||
settings: {
|
||||
requested_visibility: { ".tag": "public" },
|
||||
audience: { ".tag": "public" },
|
||||
access: { ".tag": "viewer" },
|
||||
},
|
||||
});
|
||||
expect(result).toBe("https://dropbox.com/s/test-link");
|
||||
});
|
||||
|
||||
it("should return null on error", async () => {
|
||||
mockDropboxClient.sharingCreateSharedLinkWithSettings.mockRejectedValue(new Error("Link creation failed"));
|
||||
|
||||
const result = await provider.getShareableLink("/test-file.txt");
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("search", () => {
|
||||
it("should search files", async () => {
|
||||
mockDropboxClient.filesSearchV2.mockResolvedValue(mockResponses.search);
|
||||
|
||||
const result = await provider.search("test query");
|
||||
|
||||
expect(mockDropboxClient.filesSearchV2).toHaveBeenCalledWith({
|
||||
query: "test query",
|
||||
options: {
|
||||
path: "",
|
||||
max_results: 100,
|
||||
order_by: { ".tag": "relevance" },
|
||||
file_status: { ".tag": "active" },
|
||||
filename_only: false,
|
||||
},
|
||||
});
|
||||
expect(result.items).toHaveLength(1);
|
||||
expect(result.nextPageToken).toBe("search-cursor");
|
||||
});
|
||||
|
||||
it("should respect search options", async () => {
|
||||
mockDropboxClient.filesSearchV2.mockResolvedValue(mockResponses.search);
|
||||
|
||||
await provider.search("test", { pageSize: 50 });
|
||||
|
||||
expect(mockDropboxClient.filesSearchV2).toHaveBeenCalledWith({
|
||||
query: "test",
|
||||
options: {
|
||||
path: "",
|
||||
max_results: 50,
|
||||
order_by: { ".tag": "relevance" },
|
||||
file_status: { ".tag": "active" },
|
||||
filename_only: false,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("should limit search results to Dropbox maximum", async () => {
|
||||
mockDropboxClient.filesSearchV2.mockResolvedValue(mockResponses.search);
|
||||
|
||||
await provider.search("test", { pageSize: 5000 });
|
||||
|
||||
expect(mockDropboxClient.filesSearchV2).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
options: expect.objectContaining({
|
||||
max_results: 1000, // Dropbox search max
|
||||
}),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Path Utilities", () => {
|
||||
it("should handle root folder correctly", async () => {
|
||||
mockDropboxClient.filesListFolder.mockResolvedValue(mockResponses.listFolder);
|
||||
|
||||
await provider.listChildren("");
|
||||
await provider.listChildren("/");
|
||||
await provider.listChildren("root");
|
||||
|
||||
// All should normalize to empty string for root
|
||||
expect(mockDropboxClient.filesListFolder).toHaveBeenCalledTimes(3);
|
||||
mockDropboxClient.filesListFolder.mock.calls.forEach(call => {
|
||||
expect(call[0].path).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
it("should normalize folder paths correctly", async () => {
|
||||
const folderMetadata = createFolderMetadata({
|
||||
name: "test-folder",
|
||||
parentId: "/parent/",
|
||||
});
|
||||
mockDropboxClient.filesCreateFolderV2.mockResolvedValue(mockResponses.createFolder);
|
||||
|
||||
await provider.create(folderMetadata);
|
||||
|
||||
expect(mockDropboxClient.filesCreateFolderV2).toHaveBeenCalledWith({
|
||||
path: "/parent/test-folder",
|
||||
autorename: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("MIME Type Handling", () => {
|
||||
it("should detect MIME types correctly", async () => {
|
||||
const fileMetadata = createFileMetadata({ name: "document.pdf" });
|
||||
mockDropboxClient.filesUpload.mockResolvedValue({
|
||||
result: {
|
||||
...mockResponses.uploadFile.result,
|
||||
name: "document.pdf",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await provider.create(fileMetadata, Buffer.from("content"));
|
||||
|
||||
expect(result?.mimeType).toBe("application/pdf");
|
||||
});
|
||||
|
||||
it("should use default MIME type for unknown extensions", async () => {
|
||||
const fileMetadata = createFileMetadata({ name: "file.unknown" });
|
||||
mockDropboxClient.filesUpload.mockResolvedValue({
|
||||
result: {
|
||||
...mockResponses.uploadFile.result,
|
||||
name: "file.unknown",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await provider.create(fileMetadata, Buffer.from("content"));
|
||||
|
||||
expect(result?.mimeType).toBe("application/octet-stream");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,155 +0,0 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import { DropboxProvider } from "../dropbox-provider";
|
||||
|
||||
// Integration tests require real Dropbox credentials
|
||||
// Tests will be automatically skipped if DROPBOX_TEST_ACCESS_TOKEN is not set
|
||||
const testAccessToken = process.env.DROPBOX_TEST_ACCESS_TOKEN;
|
||||
|
||||
if (testAccessToken) {
|
||||
console.log("Running integration tests if DROPBOX_TEST_ACCESS_TOKEN is set");
|
||||
}
|
||||
|
||||
(testAccessToken ? describe : describe.skip)("DropboxProvider Integration Tests", () => {
|
||||
let provider: DropboxProvider;
|
||||
|
||||
beforeEach(() => {
|
||||
provider = new DropboxProvider(testAccessToken!);
|
||||
});
|
||||
|
||||
describe("Real API Integration", () => {
|
||||
it("should authenticate and get user space", async () => {
|
||||
const driveInfo = await provider.getDriveInfo();
|
||||
|
||||
expect(driveInfo).not.toBeNull();
|
||||
if (driveInfo) {
|
||||
expect(driveInfo.totalSpace).toBeGreaterThan(0);
|
||||
expect(driveInfo.usedSpace).toBeGreaterThanOrEqual(0);
|
||||
expect(driveInfo.state).toBe("normal");
|
||||
}
|
||||
});
|
||||
|
||||
it("should list root folder contents", async () => {
|
||||
const result = await provider.listChildren();
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.items).toBeInstanceOf(Array);
|
||||
// Root folder may be empty, so we just check structure
|
||||
});
|
||||
|
||||
it("should create and delete a test folder", async () => {
|
||||
const folderName = `test-folder-${Date.now()}`;
|
||||
let folderId: string | undefined;
|
||||
|
||||
try {
|
||||
// Create folder
|
||||
const folder = await provider.create({
|
||||
name: folderName,
|
||||
mimeType: "application/x-directory",
|
||||
parentId: "",
|
||||
});
|
||||
|
||||
expect(folder).not.toBeNull();
|
||||
expect(folder?.name).toBe(folderName);
|
||||
expect(folder?.type).toBe("folder");
|
||||
|
||||
folderId = folder?.id;
|
||||
|
||||
// Verify folder exists
|
||||
if (folder) {
|
||||
const retrieved = await provider.getById(folder.id);
|
||||
expect(retrieved).not.toBeNull();
|
||||
expect(retrieved?.name).toBe(folderName);
|
||||
}
|
||||
} finally {
|
||||
// Cleanup: Delete folder if it was created
|
||||
if (folderId) {
|
||||
try {
|
||||
await provider.delete(folderId);
|
||||
} catch (cleanupError) {
|
||||
console.warn(`Failed to cleanup test folder ${folderId}:`, cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify folder is deleted
|
||||
if (folderId) {
|
||||
const shouldBeNull = await provider.getById(folderId);
|
||||
expect(shouldBeNull).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("should create, download, and delete a test file", async () => {
|
||||
const fileName = `test-file-${Date.now()}.txt`;
|
||||
const fileContent = "This is a test file content";
|
||||
let fileId: string | undefined;
|
||||
|
||||
try {
|
||||
// Create file
|
||||
const file = await provider.create(
|
||||
{
|
||||
name: fileName,
|
||||
mimeType: "text/plain",
|
||||
parentId: "",
|
||||
},
|
||||
Buffer.from(fileContent)
|
||||
);
|
||||
|
||||
expect(file).not.toBeNull();
|
||||
expect(file?.name).toBe(fileName);
|
||||
expect(file?.type).toBe("file");
|
||||
|
||||
fileId = file?.id;
|
||||
|
||||
if (file) {
|
||||
// Download file
|
||||
const downloadResult = await provider.download(file.id);
|
||||
expect(downloadResult).not.toBeNull();
|
||||
if (downloadResult) {
|
||||
expect(downloadResult.filename).toBe(fileName);
|
||||
expect(downloadResult.data.toString()).toBe(fileContent);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
// Cleanup: Delete file if it was created
|
||||
if (fileId) {
|
||||
try {
|
||||
await provider.delete(fileId);
|
||||
} catch (cleanupError) {
|
||||
console.warn(`Failed to cleanup test file ${fileId}:`, cleanupError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Verify file is deleted
|
||||
if (fileId) {
|
||||
const shouldBeNull = await provider.getById(fileId);
|
||||
expect(shouldBeNull).toBeNull();
|
||||
}
|
||||
});
|
||||
|
||||
it("should search for files", async () => {
|
||||
// This test assumes there are some files in the Dropbox account
|
||||
const result = await provider.search("test", { pageSize: 10 });
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.items).toBeInstanceOf(Array);
|
||||
// Results may be empty if no files match
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error Handling", () => {
|
||||
it("should handle invalid file ID gracefully", async () => {
|
||||
const result = await provider.getById("/invalid/path/that/does/not/exist");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle invalid delete gracefully", async () => {
|
||||
await expect(provider.delete("/invalid/path")).rejects.toThrow();
|
||||
});
|
||||
|
||||
it("should handle download of non-existent file", async () => {
|
||||
const result = await provider.download("/invalid/path");
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,148 +0,0 @@
|
||||
import { DropboxProvider } from "../dropbox-provider";
|
||||
import type { FileMetadata } from "@nimbus/shared";
|
||||
import { vi } from "vitest";
|
||||
|
||||
const MOCK_FILE_RESPONSE = {
|
||||
".tag": "file" as const,
|
||||
id: "id:test-file-id",
|
||||
name: "test-file.txt",
|
||||
path_lower: "/test-file.txt",
|
||||
path_display: "/test-file.txt",
|
||||
size: 11,
|
||||
client_modified: "2023-01-01T12:00:00Z",
|
||||
server_modified: "2023-01-01T12:00:00Z",
|
||||
content_hash: "abc123",
|
||||
};
|
||||
|
||||
const MOCK_FOLDER_RESPONSE = {
|
||||
".tag": "folder" as const,
|
||||
id: "id:test-folder-id",
|
||||
name: "test-folder",
|
||||
path_lower: "/test-folder",
|
||||
path_display: "/test-folder",
|
||||
};
|
||||
|
||||
function createMockDropboxClient() {
|
||||
return {
|
||||
filesCreateFolderV2: vi.fn(),
|
||||
filesUpload: vi.fn(),
|
||||
filesGetMetadata: vi.fn(),
|
||||
filesMoveV2: vi.fn(),
|
||||
filesDeleteV2: vi.fn(),
|
||||
filesListFolder: vi.fn(),
|
||||
filesListFolderContinue: vi.fn(),
|
||||
filesDownload: vi.fn(),
|
||||
filesCopyV2: vi.fn(),
|
||||
usersGetSpaceUsage: vi.fn(),
|
||||
sharingCreateSharedLinkWithSettings: vi.fn(),
|
||||
filesSearchV2: vi.fn(),
|
||||
};
|
||||
}
|
||||
|
||||
export const mockDropboxClient = createMockDropboxClient();
|
||||
|
||||
export function createProviderWithMockClient(): DropboxProvider {
|
||||
const provider = new DropboxProvider("mock-access-token");
|
||||
(provider as any).client = mockDropboxClient;
|
||||
return provider;
|
||||
}
|
||||
|
||||
export function createProviderWithFreshMockClient(): DropboxProvider {
|
||||
const provider = new DropboxProvider("mock-access-token");
|
||||
const freshMockClient = createMockDropboxClient();
|
||||
(provider as any).client = freshMockClient;
|
||||
return provider;
|
||||
}
|
||||
|
||||
export function restoreMockClient(provider: DropboxProvider): void {
|
||||
(provider as any).client = mockDropboxClient;
|
||||
}
|
||||
|
||||
export function createFileMetadata(overrides: Partial<FileMetadata> = {}): FileMetadata {
|
||||
return {
|
||||
name: "test-file.txt",
|
||||
mimeType: "text/plain",
|
||||
parentId: "",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function createFolderMetadata(overrides: Partial<FileMetadata> = {}): FileMetadata {
|
||||
return {
|
||||
name: "test-folder",
|
||||
mimeType: "application/x-directory",
|
||||
parentId: "",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
export function resetAllMocks() {
|
||||
Object.values(mockDropboxClient).forEach(mock => mock.mockReset());
|
||||
}
|
||||
|
||||
// Mock successful responses
|
||||
export const mockResponses = {
|
||||
createFolder: {
|
||||
result: {
|
||||
metadata: MOCK_FOLDER_RESPONSE,
|
||||
},
|
||||
},
|
||||
uploadFile: {
|
||||
result: MOCK_FILE_RESPONSE,
|
||||
},
|
||||
getMetadata: {
|
||||
result: MOCK_FILE_RESPONSE,
|
||||
},
|
||||
moveFile: {
|
||||
result: {
|
||||
metadata: MOCK_FILE_RESPONSE,
|
||||
},
|
||||
},
|
||||
listFolder: {
|
||||
result: {
|
||||
entries: [MOCK_FILE_RESPONSE, MOCK_FOLDER_RESPONSE],
|
||||
has_more: true,
|
||||
cursor: "cursor-123",
|
||||
},
|
||||
},
|
||||
download: {
|
||||
result: {
|
||||
name: "test-file.txt",
|
||||
size: 11,
|
||||
fileBinary: Buffer.from("test content"),
|
||||
},
|
||||
},
|
||||
copy: {
|
||||
result: {
|
||||
metadata: MOCK_FILE_RESPONSE,
|
||||
},
|
||||
},
|
||||
spaceUsage: {
|
||||
result: {
|
||||
used: 1000000,
|
||||
allocation: {
|
||||
".tag": "individual",
|
||||
allocated: 2000000000,
|
||||
},
|
||||
},
|
||||
},
|
||||
shareableLink: {
|
||||
result: {
|
||||
url: "https://dropbox.com/s/test-link",
|
||||
},
|
||||
},
|
||||
search: {
|
||||
result: {
|
||||
matches: [
|
||||
{
|
||||
metadata: {
|
||||
".tag": "metadata",
|
||||
metadata: MOCK_FILE_RESPONSE,
|
||||
},
|
||||
},
|
||||
],
|
||||
has_more: true,
|
||||
cursor: "search-cursor",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,83 +0,0 @@
|
||||
import { cleanupTestBucket, createTestS3Provider, setupTestBucket } from "./test-utils";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { S3Provider } from "../s3-provider";
|
||||
|
||||
describe("S3Provider Basic Tests", () => {
|
||||
const bucketName = "basic-test-bucket";
|
||||
let s3Provider: S3Provider;
|
||||
let testFileId: string | null = null;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupTestBucket(bucketName);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
// Cleanup: Delete test file if it exists
|
||||
if (testFileId) {
|
||||
try {
|
||||
await s3Provider.delete(testFileId);
|
||||
} catch (error) {
|
||||
console.warn("Failed to cleanup test file:", error);
|
||||
}
|
||||
}
|
||||
await cleanupTestBucket(bucketName);
|
||||
});
|
||||
|
||||
describe("Basic Operations", () => {
|
||||
it("should create S3Provider instance", () => {
|
||||
s3Provider = createTestS3Provider(bucketName);
|
||||
expect(s3Provider).toBeInstanceOf(S3Provider);
|
||||
});
|
||||
|
||||
it("should validate bucket access", async () => {
|
||||
const driveInfo = await s3Provider.getDriveInfo();
|
||||
expect(driveInfo).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should list root files", async () => {
|
||||
const files = await s3Provider.listChildren();
|
||||
expect(files).toHaveProperty("items");
|
||||
expect(Array.isArray(files.items)).toBe(true);
|
||||
});
|
||||
|
||||
it("should create a test file", async () => {
|
||||
const testContent = Buffer.from("Hello from Nimbus S3 Provider! 🚀");
|
||||
const createdFile = await s3Provider.create(
|
||||
{
|
||||
name: "nimbus-test.txt",
|
||||
mimeType: "text/plain",
|
||||
},
|
||||
testContent
|
||||
);
|
||||
|
||||
expect(createdFile).toBeDefined();
|
||||
if (!createdFile) return;
|
||||
expect(createdFile.name).toBe("nimbus-test.txt");
|
||||
expect(createdFile.mimeType).toBe("text/plain");
|
||||
testFileId = createdFile.id;
|
||||
});
|
||||
|
||||
it("should download the created file", async () => {
|
||||
expect(testFileId).toBeDefined();
|
||||
if (!testFileId) return;
|
||||
const downloaded = await s3Provider.download(testFileId);
|
||||
expect(downloaded).toBeDefined();
|
||||
if (!downloaded) return;
|
||||
expect(downloaded.data.toString()).toBe("Hello from Nimbus S3 Provider! 🚀");
|
||||
expect(downloaded.size).toBeGreaterThan(0);
|
||||
expect(downloaded.mimeType).toBe("text/plain");
|
||||
});
|
||||
|
||||
it("should delete the test file", async () => {
|
||||
expect(testFileId).toBeDefined();
|
||||
if (!testFileId) return;
|
||||
const deleted = await s3Provider.delete(testFileId);
|
||||
expect(deleted).toBe(true);
|
||||
|
||||
// Verify file is actually deleted
|
||||
const files = await s3Provider.listChildren();
|
||||
const fileExists = files.items.some(item => item.id === testFileId);
|
||||
expect(fileExists).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,235 +0,0 @@
|
||||
import {
|
||||
cleanupTestBucket,
|
||||
cleanupTestFiles,
|
||||
createTestS3Provider,
|
||||
generateTestFileName,
|
||||
setupTestBucket,
|
||||
} from "./test-utils";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
import { S3Provider } from "../s3-provider";
|
||||
|
||||
// Test data interfaces
|
||||
interface TestFile {
|
||||
id: string;
|
||||
name: string;
|
||||
size?: number;
|
||||
mimeType?: string;
|
||||
parentId?: string;
|
||||
}
|
||||
|
||||
interface TestFolder {
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
}
|
||||
|
||||
interface TestEnvironment {
|
||||
folder: TestFolder;
|
||||
rootFile: TestFile;
|
||||
nestedFile: TestFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Test resource manager for handling setup and cleanup
|
||||
*/
|
||||
class TestResourceManager {
|
||||
private resources: Array<{ id: string }> = [];
|
||||
|
||||
constructor(private s3Provider: S3Provider) {}
|
||||
|
||||
/**
|
||||
* Creates a test folder with a unique name and tracks it for cleanup
|
||||
*/
|
||||
async createFolder(baseName = "test-folder"): Promise<TestFolder> {
|
||||
const name = generateTestFileName(baseName);
|
||||
const folder = await this.s3Provider.create({
|
||||
name,
|
||||
mimeType: "application/vnd.google-apps.folder",
|
||||
});
|
||||
this.resources.push({ id: folder!.id });
|
||||
return folder!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a test file with unique name and content and tracks it for cleanup
|
||||
*/
|
||||
async createFile(baseName = "test-file", content = "Test file content", parentId?: string): Promise<TestFile> {
|
||||
const name = generateTestFileName(baseName) + ".txt";
|
||||
const file = await this.s3Provider.create(
|
||||
{
|
||||
name,
|
||||
mimeType: "text/plain",
|
||||
...(parentId && { parentId }),
|
||||
},
|
||||
Buffer.from(content)
|
||||
);
|
||||
this.resources.push({ id: file!.id });
|
||||
return file!;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up a complete test environment with folder and files
|
||||
*/
|
||||
async setupEnvironment(): Promise<TestEnvironment> {
|
||||
const folder = await this.createFolder();
|
||||
const rootFile = await this.createFile("root-test", "Root file content");
|
||||
const nestedFile = await this.createFile("nested-test", "Nested file content", folder.id);
|
||||
|
||||
return { folder, rootFile, nestedFile };
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up all tracked resources
|
||||
*/
|
||||
async cleanup(): Promise<void> {
|
||||
const fileIds = this.resources.filter(r => r?.id).map(r => r.id);
|
||||
await cleanupTestFiles(this.s3Provider, fileIds);
|
||||
this.resources = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an existing resource to be tracked for cleanup
|
||||
*/
|
||||
trackResource(resource: { id: string }): void {
|
||||
this.resources.push(resource);
|
||||
}
|
||||
}
|
||||
|
||||
describe("S3Provider Integration Tests", () => {
|
||||
const bucketName = generateTestFileName("integration-test-bucket");
|
||||
let s3Provider: S3Provider;
|
||||
let resourceManager: TestResourceManager;
|
||||
|
||||
beforeAll(async () => {
|
||||
await setupTestBucket(bucketName);
|
||||
s3Provider = createTestS3Provider(bucketName);
|
||||
resourceManager = new TestResourceManager(s3Provider);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await cleanupTestBucket(bucketName);
|
||||
});
|
||||
|
||||
describe("Authentication Interface", () => {
|
||||
it("should get access token", () => {
|
||||
const token = s3Provider.getAccessToken();
|
||||
expect(typeof token).toBe("string");
|
||||
expect(token.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should reject token updates", () => {
|
||||
expect(() => {
|
||||
s3Provider.setAccessToken("test-token");
|
||||
}).toThrow("S3Provider does not support dynamic credential updates");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Drive Information", () => {
|
||||
it("should get drive info successfully", async () => {
|
||||
const driveInfo = await s3Provider.getDriveInfo();
|
||||
expect(driveInfo).toBeDefined();
|
||||
expect(typeof driveInfo).toBe("object");
|
||||
});
|
||||
});
|
||||
|
||||
describe("File Operations", () => {
|
||||
it("should create folders and files successfully", async () => {
|
||||
// Create a folder
|
||||
const testFolder = await resourceManager.createFolder("create-test-folder");
|
||||
expect(testFolder).toBeDefined();
|
||||
expect(testFolder.name).toContain("create-test-folder");
|
||||
|
||||
// Create a file in root
|
||||
const rootFile = await resourceManager.createFile("create-root-test", "Root file content");
|
||||
expect(rootFile).toBeDefined();
|
||||
expect(rootFile.name).toContain("create-root-test");
|
||||
|
||||
// Create a file in folder
|
||||
const nestedFile = await resourceManager.createFile("create-nested-test", "Nested file content", testFolder.id);
|
||||
expect(nestedFile).toBeDefined();
|
||||
expect(nestedFile.name).toContain("create-nested-test");
|
||||
});
|
||||
|
||||
it("should retrieve files by ID", async () => {
|
||||
const rootFile = await resourceManager.createFile("retrieve-test", "Content for retrieval");
|
||||
|
||||
const retrieved = await s3Provider.getById(rootFile.id);
|
||||
expect(retrieved).toBeDefined();
|
||||
expect(retrieved!.id).toBe(rootFile.id);
|
||||
expect(retrieved!.name).toBe(rootFile.name);
|
||||
});
|
||||
|
||||
it("should list directory contents", async () => {
|
||||
const { folder } = await resourceManager.setupEnvironment();
|
||||
|
||||
const rootListing = await s3Provider.listChildren();
|
||||
expect(rootListing).toBeDefined();
|
||||
expect(Array.isArray(rootListing.items)).toBe(true);
|
||||
|
||||
const folderListing = await s3Provider.listChildren(folder.id);
|
||||
expect(folderListing).toBeDefined();
|
||||
expect(Array.isArray(folderListing.items)).toBe(true);
|
||||
expect(folderListing.items.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should download file content", async () => {
|
||||
const testContent = "Content for download test";
|
||||
const rootFile = await resourceManager.createFile("download-test", testContent);
|
||||
|
||||
const downloaded = await s3Provider.download(rootFile.id);
|
||||
expect(downloaded).toBeDefined();
|
||||
if (!downloaded) return;
|
||||
expect(downloaded.data.toString()).toBe(testContent);
|
||||
expect(downloaded.size).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should update file metadata", async () => {
|
||||
const rootFile = await resourceManager.createFile("update-test", "Content for update");
|
||||
|
||||
const newName = generateTestFileName("renamed-test") + ".txt";
|
||||
const updatedFile = await s3Provider.update(rootFile.id, {
|
||||
name: newName,
|
||||
});
|
||||
expect(updatedFile).toBeDefined();
|
||||
expect(updatedFile!.name).toBe(newName);
|
||||
});
|
||||
|
||||
it("should copy files between directories", async () => {
|
||||
const { folder, rootFile } = await resourceManager.setupEnvironment();
|
||||
|
||||
const copyName = generateTestFileName("copied-file") + ".txt";
|
||||
const copiedFile = await s3Provider.copy(rootFile.id, folder.id, copyName);
|
||||
resourceManager.trackResource({ id: copiedFile!.id });
|
||||
expect(copiedFile).toBeDefined();
|
||||
expect(copiedFile!.name).toBe(copyName);
|
||||
});
|
||||
|
||||
it("should move files between directories", async () => {
|
||||
const { nestedFile } = await resourceManager.setupEnvironment();
|
||||
const moveName = generateTestFileName("moved-to-root") + ".txt";
|
||||
const movedFile = await s3Provider.move(nestedFile.id, "", moveName);
|
||||
resourceManager.trackResource({ id: movedFile!.id });
|
||||
expect(movedFile).toBeDefined();
|
||||
expect(movedFile!.name).toBe(moveName);
|
||||
});
|
||||
|
||||
it("should search for files", async () => {
|
||||
const searchTerm = generateTestFileName("searchable");
|
||||
await resourceManager.createFile(searchTerm, "Searchable content");
|
||||
const searchResults = await s3Provider.search(searchTerm);
|
||||
expect(searchResults).toBeDefined();
|
||||
expect(Array.isArray(searchResults.items)).toBe(true);
|
||||
// Note: Search might not always return results immediately in S3
|
||||
});
|
||||
});
|
||||
|
||||
describe("Shareable Links", () => {
|
||||
it("should handle shareable links appropriately", async () => {
|
||||
const testManager = new TestResourceManager(s3Provider);
|
||||
const testFile = await testManager.createFile("shareable-test", "Content for sharing");
|
||||
const shareLink = await s3Provider.getShareableLink(testFile.id);
|
||||
// S3 typically returns null for shareable links
|
||||
expect(shareLink === null || typeof shareLink === "string").toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,68 +0,0 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { S3Provider } from "../s3-provider";
|
||||
|
||||
describe("S3Provider", () => {
|
||||
const mockConfig = {
|
||||
accessKeyId: "test-access-key",
|
||||
secretAccessKey: "test-secret-key",
|
||||
region: "us-east-1",
|
||||
bucketName: "test-bucket",
|
||||
};
|
||||
|
||||
describe("Constructor", () => {
|
||||
it("should create S3Provider with correct config", () => {
|
||||
const s3Provider = new S3Provider(mockConfig);
|
||||
expect(s3Provider).toBeInstanceOf(S3Provider);
|
||||
// The token is base64 encoded with a timestamp, so we decode it to check.
|
||||
const decodedToken = Buffer.from(s3Provider.getAccessToken(), "base64").toString("utf-8");
|
||||
expect(decodedToken).toContain(mockConfig.accessKeyId);
|
||||
});
|
||||
|
||||
it("should handle custom endpoint", () => {
|
||||
const providerWithEndpoint = new S3Provider({
|
||||
...mockConfig,
|
||||
endpoint: "https://minio.example.com",
|
||||
});
|
||||
expect(providerWithEndpoint).toBeInstanceOf(S3Provider);
|
||||
});
|
||||
|
||||
it("should throw an error if region is missing", () => {
|
||||
expect(() => {
|
||||
new S3Provider({
|
||||
accessKeyId: "",
|
||||
secretAccessKey: "",
|
||||
region: "", // AWS SDK requires a region
|
||||
bucketName: "",
|
||||
});
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("Authentication Interface", () => {
|
||||
it("should get access token and reject token updates", () => {
|
||||
const s3Provider = new S3Provider(mockConfig);
|
||||
const originalToken = s3Provider.getAccessToken();
|
||||
|
||||
expect(typeof originalToken).toBe("string");
|
||||
expect(originalToken.length).toBeGreaterThan(0);
|
||||
|
||||
// S3Provider should reject dynamic token updates for security
|
||||
expect(() => {
|
||||
s3Provider.setAccessToken("new-token");
|
||||
}).toThrow("S3Provider does not support dynamic credential updates");
|
||||
});
|
||||
});
|
||||
|
||||
describe("S3-Compatible Services", () => {
|
||||
it("should support MinIO configuration", () => {
|
||||
const minioProvider = new S3Provider({
|
||||
accessKeyId: "minio-key",
|
||||
secretAccessKey: "minio-secret",
|
||||
region: "us-east-1",
|
||||
bucketName: "test-bucket",
|
||||
endpoint: "http://localhost:9000",
|
||||
});
|
||||
expect(minioProvider).toBeInstanceOf(S3Provider);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,116 +0,0 @@
|
||||
import { CreateBucketCommand, S3Client } from "@aws-sdk/client-s3";
|
||||
import { S3Provider } from "../s3-provider";
|
||||
|
||||
const config = {
|
||||
endpoint: "http://localhost:9000",
|
||||
region: "us-east-1",
|
||||
accessKeyId: "minioadmin",
|
||||
secretAccessKey: "minioadmin",
|
||||
forcePathStyle: true, // Required for local S3 docker container
|
||||
};
|
||||
|
||||
// Create S3 client
|
||||
const createLocalS3Client = () => {
|
||||
return new S3Client({
|
||||
endpoint: config.endpoint,
|
||||
region: config.region,
|
||||
credentials: {
|
||||
accessKeyId: config.accessKeyId,
|
||||
secretAccessKey: config.secretAccessKey,
|
||||
},
|
||||
forcePathStyle: config.forcePathStyle,
|
||||
});
|
||||
};
|
||||
|
||||
// Setup test bucket
|
||||
export const setupTestBucket = async (bucketName: string) => {
|
||||
const s3Client = createLocalS3Client();
|
||||
|
||||
try {
|
||||
await s3Client.send(
|
||||
new CreateBucketCommand({
|
||||
Bucket: bucketName,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
// Bucket might already exist, which is fine
|
||||
console.log(`Test bucket '${bucketName}' already exists or error:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
// Cleanup test bucket
|
||||
export const cleanupTestBucket = async (bucketName: string) => {
|
||||
const s3Client = createLocalS3Client();
|
||||
|
||||
try {
|
||||
// List all objects in the bucket
|
||||
const { ListObjectsV2Command } = await import("@aws-sdk/client-s3");
|
||||
const listResponse = await s3Client.send(
|
||||
new ListObjectsV2Command({
|
||||
Bucket: bucketName,
|
||||
})
|
||||
);
|
||||
|
||||
// Delete all objects
|
||||
if (listResponse.Contents && listResponse.Contents.length > 0) {
|
||||
const { DeleteObjectCommand } = await import("@aws-sdk/client-s3");
|
||||
|
||||
for (const object of listResponse.Contents) {
|
||||
if (object.Key) {
|
||||
await s3Client.send(
|
||||
new DeleteObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Key: object.Key,
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Delete the bucket
|
||||
const { DeleteBucketCommand } = await import("@aws-sdk/client-s3");
|
||||
await s3Client.send(
|
||||
new DeleteBucketCommand({
|
||||
Bucket: bucketName,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.log(`Error cleaning up test bucket:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates an S3Provider instance
|
||||
*/
|
||||
export function createTestS3Provider(bucketName: string): S3Provider {
|
||||
return new S3Provider({
|
||||
accessKeyId: config.accessKeyId,
|
||||
secretAccessKey: config.secretAccessKey,
|
||||
region: config.region,
|
||||
bucketName,
|
||||
endpoint: config.endpoint,
|
||||
forcePathStyle: config.forcePathStyle,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a unique test file name
|
||||
*/
|
||||
export function generateTestFileName(prefix = "test") {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).substring(2, 15)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up test files created during testing
|
||||
*/
|
||||
export async function cleanupTestFiles(s3Provider: S3Provider, fileIds: string[]) {
|
||||
const cleanupPromises = fileIds.map(async fileId => {
|
||||
try {
|
||||
await s3Provider.delete(fileId);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to cleanup test file ${fileId}:`, error);
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(cleanupPromises);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
import defineConfig from "@nimbus/vitest";
|
||||
|
||||
export default defineConfig;
|
||||
@@ -1,3 +0,0 @@
|
||||
{
|
||||
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||
}
|
||||
19
apps/web/index.html
Normal file
19
apps/web/index.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="bg-sidebar">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/x-icon" href="/favicon.ico" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Manrope:wght@200;300;400;500;600;700;800&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<title>Nimbus</title>
|
||||
</head>
|
||||
<body class="antialiased" suppressHydrationWarning>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
reactStrictMode: true,
|
||||
output: "standalone",
|
||||
transpilePackages: ["@t3-oss/env-core"],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -4,19 +4,14 @@
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"lint": "next lint",
|
||||
"dev": "next dev --turbopack",
|
||||
"build": "next build && bash scripts/handle-nextjs-standalone-build.sh",
|
||||
"start": "bun run .next/build-start-folder/apps/web/server.js",
|
||||
"docker:build": "bun run build && docker build -t nimbus-web-manual .",
|
||||
"docker:run": "source .env && docker run --name nimbus-web-manual --env-file .env -p $WEB_PORT:$WEB_PORT nimbus-web-manual:latest",
|
||||
"lint": "oxlint --fix",
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"start": "vite preview --port ${WEB_PORT:-3000}",
|
||||
"docker:up": "bun run build && docker compose up -d",
|
||||
"docker:down": "docker compose down",
|
||||
"docker:remove": "docker compose down --rmi local -v",
|
||||
"docker:reset": "bun run docker:remove && bun run docker:up"
|
||||
"docker:down": "docker compose down"
|
||||
},
|
||||
"dependencies": {
|
||||
"@databuddy/sdk": "^2.0.0",
|
||||
"@dnd-kit/react": "^0.1.20",
|
||||
"@hookform/resolvers": "^5.2.1",
|
||||
"@nimbus/auth": "workspace:*",
|
||||
@@ -38,10 +33,11 @@
|
||||
"@radix-ui/react-tabs": "^1.1.12",
|
||||
"@radix-ui/react-tooltip": "^1.2.7",
|
||||
"@t3-oss/env-core": "^0.13.8",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-query-devtools": "^5.84.1",
|
||||
"@tanstack/react-router-devtools": "^1.133.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@tanstack/router": "^0.0.1-beta.53",
|
||||
"axios": "^1.11.0",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
@@ -49,28 +45,27 @@
|
||||
"hono": "^4.8.10",
|
||||
"lucide-react": "^0.534.0",
|
||||
"motion": "^12.23.12",
|
||||
"next": "15.3",
|
||||
"next-themes": "^0.4.6",
|
||||
"posthog-js": "^1.260.1",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-dropzone": "^14.3.8",
|
||||
"react-hook-form": "^7.61.1",
|
||||
"recharts": "^3.1.0",
|
||||
"sonner": "^2.0.6",
|
||||
"stripe": "^17.4.0",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-scrollbar": "^4.0.2",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"vaul": "^1.1.2",
|
||||
"zod": "^4.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nimbus/vitest": "workspace:*",
|
||||
"@tanstack/router-plugin": "^1.94.4",
|
||||
"@types/node": "^22.10.2",
|
||||
"@types/react": "^19.1.9",
|
||||
"@types/react-dom": "^19.1.7",
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"eslint": "^9.32.0",
|
||||
"eslint-config-next": "15.4"
|
||||
"vite": "^6.0.7",
|
||||
"vite-tsconfig-paths": "^5.1.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import tailwindcss from "@tailwindcss/postcss";
|
||||
|
||||
const config = {
|
||||
plugins: ["@tailwindcss/postcss"],
|
||||
plugins: [tailwindcss],
|
||||
};
|
||||
|
||||
export default config;
|
||||
|
||||
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
7
apps/web/public/robots.txt
Normal file
7
apps/web/public/robots.txt
Normal file
@@ -0,0 +1,7 @@
|
||||
User-agent: *
|
||||
Allow: /
|
||||
Disallow: /dashboard/
|
||||
Disallow: /api/auth/
|
||||
Disallow: /reset-password
|
||||
|
||||
Sitemap: https://nimbus.example.com/sitemap.xml
|
||||
@@ -1,22 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { DownloadProvider } from "@/components/providers/download-provider";
|
||||
import { AccountProvider } from "@/components/providers/account-provider";
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { AppSidebar } from "@/components/dashboard/sidebar";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export default function AppLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<AccountProvider>
|
||||
<DownloadProvider>
|
||||
<SidebarProvider className="has-data-[variant=inset]:dark:bg-neutral-800">
|
||||
<AppSidebar variant="inset" className="px-0 py-0" />
|
||||
<SidebarInset className="md:peer-data-[variant=inset]:peer-data-[state=collapsed]:ml-0">
|
||||
<main className="flex h-full w-full flex-col p-1">{children}</main>
|
||||
</SidebarInset>
|
||||
</SidebarProvider>
|
||||
</DownloadProvider>
|
||||
</AccountProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { DefaultAccountProvider } from "@/components/providers/default-account-provider";
|
||||
import { UserInfoProvider } from "@/components/providers/user-info-provider";
|
||||
|
||||
export default function AppLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<UserInfoProvider>
|
||||
<DefaultAccountProvider>{children}</DefaultAccountProvider>
|
||||
</UserInfoProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { ProtectedRoute } from "@/components/providers/protected-route";
|
||||
|
||||
export default function ProtectedLayout({ children }: { children: React.ReactNode }) {
|
||||
return <ProtectedRoute>{children}</ProtectedRoute>;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Contributors from "@/components/contributors/contributors";
|
||||
import Header from "@/components/home/header";
|
||||
|
||||
export default function ContributorsPage() {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<Contributors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
import Header from "@/components/home/header";
|
||||
import Footer from "@/components/home/footer";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
// this is a copy of Analogs legal layout component with some changes
|
||||
// https://github.com/analogdotnow/Analog/blob/main/apps/web/src/app/(legal)/layout.tsx
|
||||
|
||||
export default function LegalLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
// <div className="bg-background min-h-screen">
|
||||
<div className="font-manrope flex w-full flex-1 flex-col items-center justify-center gap-12 overflow-hidden px-4 py-20 md:gap-12">
|
||||
<Header />
|
||||
{children}
|
||||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
import { PublicRoute } from "@/components/providers/public-route";
|
||||
|
||||
export default function PublicLayout({ children }: { children: React.ReactNode }) {
|
||||
return <PublicRoute redirectTo="/dashboard">{children}</PublicRoute>;
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
import { ReactQueryProvider } from "@/components/providers/query-provider";
|
||||
import { ThemeProvider } from "@/components/providers/theme-provider";
|
||||
import { PHProvider } from "@/components/providers/posthog-provider";
|
||||
import { AppProviders } from "@/components/providers/app-providers";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import { Databuddy } from "@databuddy/sdk/react";
|
||||
import { siteConfig } from "@/utils/site-config";
|
||||
import OGImage from "@/public/images/og.png";
|
||||
import type env from "@nimbus/env/client";
|
||||
import { manrope } from "@/utils/fonts";
|
||||
import type { ReactNode } from "react";
|
||||
import type { Metadata } from "next";
|
||||
import { Toaster } from "sonner";
|
||||
import "@/app/globals.css";
|
||||
|
||||
export const metadata: Metadata = {
|
||||
keywords: ["nimbus", "cloud", "storage", "file", "sharing", "upload", "download", "sync", "backup"],
|
||||
title: {
|
||||
default: siteConfig.name,
|
||||
template: `%s | ${siteConfig.name}`,
|
||||
},
|
||||
description: siteConfig.description,
|
||||
metadataBase: new URL(siteConfig.url as string),
|
||||
openGraph: {
|
||||
title: siteConfig.name,
|
||||
description: siteConfig.description,
|
||||
url: siteConfig.url,
|
||||
siteName: siteConfig.name,
|
||||
images: [
|
||||
{
|
||||
url: OGImage.src,
|
||||
width: OGImage.width,
|
||||
height: OGImage.height,
|
||||
alt: siteConfig.name,
|
||||
},
|
||||
],
|
||||
locale: "en_US",
|
||||
type: "website",
|
||||
},
|
||||
twitter: {
|
||||
title: siteConfig.name,
|
||||
description: siteConfig.description,
|
||||
site: siteConfig.twitterHandle,
|
||||
card: "summary_large_image",
|
||||
images: [
|
||||
{
|
||||
url: OGImage.src,
|
||||
width: OGImage.width,
|
||||
height: OGImage.height,
|
||||
alt: siteConfig.name,
|
||||
},
|
||||
],
|
||||
},
|
||||
robots: {
|
||||
index: true,
|
||||
follow: true,
|
||||
nocache: false,
|
||||
googleBot: {
|
||||
index: true,
|
||||
follow: true,
|
||||
noimageindex: false,
|
||||
"max-video-preview": -1,
|
||||
"max-image-preview": "large",
|
||||
"max-snippet": -1,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
const geistMono = Geist_Mono({
|
||||
variable: "--font-geist-mono",
|
||||
subsets: ["latin"],
|
||||
});
|
||||
|
||||
export default function RootLayout({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<html lang="en" suppressHydrationWarning className="bg-sidebar">
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} ${manrope.variable} antialiased`}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<ReactQueryProvider>
|
||||
<AppProviders>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||
<div className="relative min-h-screen">
|
||||
<main className="flex h-full w-full justify-center">{children}</main>
|
||||
<Databuddy
|
||||
clientId="f0gfwz-oGflp3lSEx_gaA"
|
||||
trackHashChanges={true}
|
||||
trackAttributes={true}
|
||||
trackOutgoingLinks={true}
|
||||
trackInteractions={true}
|
||||
trackEngagement={true}
|
||||
trackScrollDepth={true}
|
||||
trackExitIntent={true}
|
||||
trackBounceRate={true}
|
||||
trackWebVitals={true}
|
||||
trackErrors={true}
|
||||
enableBatching={true}
|
||||
/>
|
||||
<Toaster position="top-center" richColors theme="system" />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</AppProviders>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</ReactQueryProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import Hero from "@/components/home/hero";
|
||||
|
||||
export default function Home() {
|
||||
return <Hero />;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import type { MetadataRoute } from "next";
|
||||
import { getBaseUrl } from "@/lib/utils";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export default function robots(): MetadataRoute.Robots {
|
||||
const baseUrl = getBaseUrl();
|
||||
|
||||
return {
|
||||
rules: [
|
||||
{
|
||||
userAgent: "*",
|
||||
allow: "/",
|
||||
disallow: ["/dashboard/*", "/api/auth/*", "/reset-password"],
|
||||
},
|
||||
],
|
||||
sitemap: `${baseUrl}/sitemap.xml`,
|
||||
};
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { getBaseUrl, buildUrl } from "@/lib/utils";
|
||||
import type { MetadataRoute } from "next";
|
||||
|
||||
export const dynamic = "force-static";
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
const baseUrl = getBaseUrl();
|
||||
const now = new Date();
|
||||
|
||||
return [
|
||||
{
|
||||
url: baseUrl,
|
||||
lastModified: now,
|
||||
changeFrequency: "weekly",
|
||||
priority: 1,
|
||||
},
|
||||
{
|
||||
url: buildUrl("/signin"),
|
||||
lastModified: now,
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.6,
|
||||
},
|
||||
{
|
||||
url: buildUrl("/signup"),
|
||||
lastModified: now,
|
||||
changeFrequency: "yearly",
|
||||
priority: 0.6,
|
||||
},
|
||||
{
|
||||
url: buildUrl("/contributors"),
|
||||
lastModified: now,
|
||||
changeFrequency: "monthly",
|
||||
priority: 0.7,
|
||||
},
|
||||
];
|
||||
}
|
||||
@@ -1,8 +1,5 @@
|
||||
import HeroBgDark from "@/public/images/hero-dithered-black.png";
|
||||
import HeroBgLight from "public/images/hero-dithered-white.png";
|
||||
import { type ComponentProps } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import Image from "next/image";
|
||||
|
||||
type BgAngelsProps = {
|
||||
className?: string;
|
||||
@@ -12,23 +9,21 @@ type BgAngelsProps = {
|
||||
export default function BgAngels({ className, alt }: BgAngelsProps) {
|
||||
return (
|
||||
<>
|
||||
<Image
|
||||
src={HeroBgDark}
|
||||
<img
|
||||
src="/images/hero-dithered-black.png"
|
||||
alt={alt ? alt : "angel"}
|
||||
width={478}
|
||||
height={718}
|
||||
loading="eager"
|
||||
className={cn(className, "hidden dark:block")}
|
||||
priority
|
||||
/>
|
||||
<Image
|
||||
src={HeroBgLight}
|
||||
<img
|
||||
src="/images/hero-dithered-white.png"
|
||||
alt={alt ? alt : "angel"}
|
||||
width={478}
|
||||
height={718}
|
||||
loading="eager"
|
||||
className={cn(className, "block dark:hidden")}
|
||||
priority
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { AnimatedGroup } from "@/components/ui/animated-group";
|
||||
import BgAngels from "@/components/brand-assets/bg-angels";
|
||||
import { WaitlistForm } from "@/components/home/waitlist";
|
||||
import HeroLight from "@/public/images/hero-light.png";
|
||||
import HeroDark from "@/public/images/hero-dark.png";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import Header from "@/components/home/header";
|
||||
import Footer from "@/components/home/footer";
|
||||
import { type Variants } from "motion/react";
|
||||
import Image from "next/image";
|
||||
|
||||
const transitionVariants: { item: Variants } = {
|
||||
item: {
|
||||
@@ -84,23 +81,17 @@ export default function Hero() {
|
||||
>
|
||||
<div className="border-border mx-auto w-full max-w-3xl rounded-xl border bg-gray-50/5 p-2 backdrop-blur-xs sm:max-w-4xl sm:min-w-0 sm:translate-x-0">
|
||||
<div className="absolute inset-0 -z-10 rounded-xl bg-gradient-to-br from-black to-[#7FBEE4] opacity-30 blur-[60px]" />
|
||||
<Image
|
||||
src={HeroDark}
|
||||
<img
|
||||
src="/images/hero-dark.png"
|
||||
alt="Hero"
|
||||
className="z-10 ml-0 hidden h-auto w-full rounded-lg object-cover sm:mx-auto dark:block"
|
||||
unoptimized
|
||||
loading="lazy"
|
||||
placeholder="blur"
|
||||
sizes="(max-width: 768px) 100vw, 80vw"
|
||||
/>
|
||||
<Image
|
||||
src={HeroLight}
|
||||
<img
|
||||
src="/images/hero-light.png"
|
||||
alt="Hero"
|
||||
className="z-10 ml-0 block h-auto w-full rounded-lg object-cover sm:mx-auto dark:hidden"
|
||||
unoptimized
|
||||
loading="lazy"
|
||||
placeholder="blur"
|
||||
sizes="(max-width: 768px) 100vw, 80vw"
|
||||
/>
|
||||
</div>
|
||||
</AnimatedGroup>
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
import { createContext, useContext, useEffect, useState, type ReactNode } from "react";
|
||||
import type { DriveProvider, DriveProviderSlug } from "@nimbus/shared";
|
||||
import { useNavigate, useLocation } from "@tanstack/react-router";
|
||||
import { providerToSlug, slugToProvider } from "@nimbus/shared";
|
||||
import { useUserInfoProvider } from "./user-info-provider";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
|
||||
interface AccountProvider {
|
||||
defaultProviderSlug: DriveProviderSlug | null;
|
||||
@@ -18,8 +18,9 @@ const DefaultAccountProviderContext = createContext<AccountProvider | undefined>
|
||||
|
||||
export function DefaultAccountProvider({ children }: { children: ReactNode }) {
|
||||
const { user, error: userInfoError, isLoading: userInfoIsPending } = useUserInfoProvider();
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const pathname = location.pathname;
|
||||
|
||||
const [state, setState] = useState<AccountProvider>(() => ({
|
||||
defaultProviderSlug: null,
|
||||
@@ -78,7 +79,7 @@ export function DefaultAccountProvider({ children }: { children: ReactNode }) {
|
||||
}
|
||||
const path = `/dashboard/${providerSlug}/${accountId}`;
|
||||
if (pathname !== path) {
|
||||
router.push(path);
|
||||
navigate({ to: path as any });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
import { useTheme as useNextTheme } from "next-themes";
|
||||
import { useIsMounted } from "./useIsMounted";
|
||||
|
||||
export const useTheme = () => {
|
||||
const { theme, setTheme } = useNextTheme();
|
||||
const isMounted = useIsMounted();
|
||||
|
||||
const toggleTheme = (): void => {
|
||||
setTheme(theme === "dark" ? "light" : "dark");
|
||||
};
|
||||
|
||||
return { theme, toggleTheme, isMounted };
|
||||
};
|
||||
44
apps/web/src/main.tsx
Normal file
44
apps/web/src/main.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { RouterProvider, createRouter } from "@tanstack/react-router";
|
||||
import { routeTree } from "./routeTree.gen";
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { StrictMode } from "react";
|
||||
import "./globals.css";
|
||||
|
||||
// Create a new router instance
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
defaultPreload: "intent",
|
||||
defaultPreloadStaleTime: 0,
|
||||
context: undefined!,
|
||||
defaultPendingComponent: () => (
|
||||
<div className="flex h-screen w-full items-center justify-center">
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<div className="border-primary h-8 w-8 animate-spin rounded-full border-b-2"></div>
|
||||
<p className="text-muted-foreground text-sm">Loading...</p>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
// Render the app
|
||||
const rootElement = document.getElementById("root");
|
||||
|
||||
if (!rootElement) {
|
||||
throw new Error("Root element not found. Make sure there is a div with id='root' in your index.html");
|
||||
}
|
||||
|
||||
if (!rootElement.innerHTML) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</StrictMode>
|
||||
);
|
||||
}
|
||||
388
apps/web/src/routeTree.gen.ts
Normal file
388
apps/web/src/routeTree.gen.ts
Normal file
@@ -0,0 +1,388 @@
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file was automatically generated by TanStack Router.
|
||||
// You should NOT make any changes in this file as it will be overwritten.
|
||||
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
|
||||
|
||||
import { Route as ProtectedDashboardProviderSlugAccountIdRouteImport } from "./routes/_protected/dashboard/$providerSlug.$accountId";
|
||||
import { Route as ProtectedDashboardSettingsRouteImport } from "./routes/_protected/dashboard/settings";
|
||||
import { Route as ProtectedDashboardIndexRouteImport } from "./routes/_protected/dashboard/index";
|
||||
import { Route as PublicForgotPasswordRouteImport } from "./routes/_public/forgot-password";
|
||||
import { Route as PublicResetPasswordRouteImport } from "./routes/_public/reset-password";
|
||||
import { Route as PublicContributorsRouteImport } from "./routes/_public/contributors";
|
||||
import { Route as ProtectedDashboardRouteImport } from "./routes/_protected/dashboard";
|
||||
import { Route as PublicVerifyEmailRouteImport } from "./routes/_public/verify-email";
|
||||
import { Route as PublicPrivacyRouteImport } from "./routes/_public/privacy";
|
||||
import { Route as PublicSignupRouteImport } from "./routes/_public/signup";
|
||||
import { Route as PublicSigninRouteImport } from "./routes/_public/signin";
|
||||
import { Route as PublicTermsRouteImport } from "./routes/_public/terms";
|
||||
import { Route as ProtectedRouteImport } from "./routes/_protected";
|
||||
import { Route as PublicRouteImport } from "./routes/_public";
|
||||
import { Route as rootRouteImport } from "./routes/__root";
|
||||
import { Route as IndexRouteImport } from "./routes/index";
|
||||
import { Route as DebugRouteImport } from "./routes/debug";
|
||||
|
||||
const DebugRoute = DebugRouteImport.update({
|
||||
id: "/debug",
|
||||
path: "/debug",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const PublicRoute = PublicRouteImport.update({
|
||||
id: "/_public",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const ProtectedRoute = ProtectedRouteImport.update({
|
||||
id: "/_protected",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const IndexRoute = IndexRouteImport.update({
|
||||
id: "/",
|
||||
path: "/",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const PublicVerifyEmailRoute = PublicVerifyEmailRouteImport.update({
|
||||
id: "/verify-email",
|
||||
path: "/verify-email",
|
||||
getParentRoute: () => PublicRoute,
|
||||
} as any);
|
||||
const PublicTermsRoute = PublicTermsRouteImport.update({
|
||||
id: "/terms",
|
||||
path: "/terms",
|
||||
getParentRoute: () => PublicRoute,
|
||||
} as any);
|
||||
const PublicSignupRoute = PublicSignupRouteImport.update({
|
||||
id: "/signup",
|
||||
path: "/signup",
|
||||
getParentRoute: () => PublicRoute,
|
||||
} as any);
|
||||
const PublicSigninRoute = PublicSigninRouteImport.update({
|
||||
id: "/signin",
|
||||
path: "/signin",
|
||||
getParentRoute: () => PublicRoute,
|
||||
} as any);
|
||||
const PublicResetPasswordRoute = PublicResetPasswordRouteImport.update({
|
||||
id: "/reset-password",
|
||||
path: "/reset-password",
|
||||
getParentRoute: () => PublicRoute,
|
||||
} as any);
|
||||
const PublicPrivacyRoute = PublicPrivacyRouteImport.update({
|
||||
id: "/privacy",
|
||||
path: "/privacy",
|
||||
getParentRoute: () => PublicRoute,
|
||||
} as any);
|
||||
const PublicForgotPasswordRoute = PublicForgotPasswordRouteImport.update({
|
||||
id: "/forgot-password",
|
||||
path: "/forgot-password",
|
||||
getParentRoute: () => PublicRoute,
|
||||
} as any);
|
||||
const PublicContributorsRoute = PublicContributorsRouteImport.update({
|
||||
id: "/contributors",
|
||||
path: "/contributors",
|
||||
getParentRoute: () => PublicRoute,
|
||||
} as any);
|
||||
const ProtectedDashboardRoute = ProtectedDashboardRouteImport.update({
|
||||
id: "/dashboard",
|
||||
path: "/dashboard",
|
||||
getParentRoute: () => ProtectedRoute,
|
||||
} as any);
|
||||
const ProtectedDashboardIndexRoute = ProtectedDashboardIndexRouteImport.update({
|
||||
id: "/",
|
||||
path: "/",
|
||||
getParentRoute: () => ProtectedDashboardRoute,
|
||||
} as any);
|
||||
const ProtectedDashboardSettingsRoute = ProtectedDashboardSettingsRouteImport.update({
|
||||
id: "/settings",
|
||||
path: "/settings",
|
||||
getParentRoute: () => ProtectedDashboardRoute,
|
||||
} as any);
|
||||
const ProtectedDashboardProviderSlugAccountIdRoute = ProtectedDashboardProviderSlugAccountIdRouteImport.update({
|
||||
id: "/$providerSlug/$accountId",
|
||||
path: "/$providerSlug/$accountId",
|
||||
getParentRoute: () => ProtectedDashboardRoute,
|
||||
} as any);
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
"/": typeof IndexRoute;
|
||||
"/debug": typeof DebugRoute;
|
||||
"/dashboard": typeof ProtectedDashboardRouteWithChildren;
|
||||
"/contributors": typeof PublicContributorsRoute;
|
||||
"/forgot-password": typeof PublicForgotPasswordRoute;
|
||||
"/privacy": typeof PublicPrivacyRoute;
|
||||
"/reset-password": typeof PublicResetPasswordRoute;
|
||||
"/signin": typeof PublicSigninRoute;
|
||||
"/signup": typeof PublicSignupRoute;
|
||||
"/terms": typeof PublicTermsRoute;
|
||||
"/verify-email": typeof PublicVerifyEmailRoute;
|
||||
"/dashboard/settings": typeof ProtectedDashboardSettingsRoute;
|
||||
"/dashboard/": typeof ProtectedDashboardIndexRoute;
|
||||
"/dashboard/$providerSlug/$accountId": typeof ProtectedDashboardProviderSlugAccountIdRoute;
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
"/": typeof IndexRoute;
|
||||
"/debug": typeof DebugRoute;
|
||||
"/contributors": typeof PublicContributorsRoute;
|
||||
"/forgot-password": typeof PublicForgotPasswordRoute;
|
||||
"/privacy": typeof PublicPrivacyRoute;
|
||||
"/reset-password": typeof PublicResetPasswordRoute;
|
||||
"/signin": typeof PublicSigninRoute;
|
||||
"/signup": typeof PublicSignupRoute;
|
||||
"/terms": typeof PublicTermsRoute;
|
||||
"/verify-email": typeof PublicVerifyEmailRoute;
|
||||
"/dashboard/settings": typeof ProtectedDashboardSettingsRoute;
|
||||
"/dashboard": typeof ProtectedDashboardIndexRoute;
|
||||
"/dashboard/$providerSlug/$accountId": typeof ProtectedDashboardProviderSlugAccountIdRoute;
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
__root__: typeof rootRouteImport;
|
||||
"/": typeof IndexRoute;
|
||||
"/_protected": typeof ProtectedRouteWithChildren;
|
||||
"/_public": typeof PublicRouteWithChildren;
|
||||
"/debug": typeof DebugRoute;
|
||||
"/_protected/dashboard": typeof ProtectedDashboardRouteWithChildren;
|
||||
"/_public/contributors": typeof PublicContributorsRoute;
|
||||
"/_public/forgot-password": typeof PublicForgotPasswordRoute;
|
||||
"/_public/privacy": typeof PublicPrivacyRoute;
|
||||
"/_public/reset-password": typeof PublicResetPasswordRoute;
|
||||
"/_public/signin": typeof PublicSigninRoute;
|
||||
"/_public/signup": typeof PublicSignupRoute;
|
||||
"/_public/terms": typeof PublicTermsRoute;
|
||||
"/_public/verify-email": typeof PublicVerifyEmailRoute;
|
||||
"/_protected/dashboard/settings": typeof ProtectedDashboardSettingsRoute;
|
||||
"/_protected/dashboard/": typeof ProtectedDashboardIndexRoute;
|
||||
"/_protected/dashboard/$providerSlug/$accountId": typeof ProtectedDashboardProviderSlugAccountIdRoute;
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fullPaths:
|
||||
| "/"
|
||||
| "/debug"
|
||||
| "/dashboard"
|
||||
| "/contributors"
|
||||
| "/forgot-password"
|
||||
| "/privacy"
|
||||
| "/reset-password"
|
||||
| "/signin"
|
||||
| "/signup"
|
||||
| "/terms"
|
||||
| "/verify-email"
|
||||
| "/dashboard/settings"
|
||||
| "/dashboard/"
|
||||
| "/dashboard/$providerSlug/$accountId";
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
to:
|
||||
| "/"
|
||||
| "/debug"
|
||||
| "/contributors"
|
||||
| "/forgot-password"
|
||||
| "/privacy"
|
||||
| "/reset-password"
|
||||
| "/signin"
|
||||
| "/signup"
|
||||
| "/terms"
|
||||
| "/verify-email"
|
||||
| "/dashboard/settings"
|
||||
| "/dashboard"
|
||||
| "/dashboard/$providerSlug/$accountId";
|
||||
id:
|
||||
| "__root__"
|
||||
| "/"
|
||||
| "/_protected"
|
||||
| "/_public"
|
||||
| "/debug"
|
||||
| "/_protected/dashboard"
|
||||
| "/_public/contributors"
|
||||
| "/_public/forgot-password"
|
||||
| "/_public/privacy"
|
||||
| "/_public/reset-password"
|
||||
| "/_public/signin"
|
||||
| "/_public/signup"
|
||||
| "/_public/terms"
|
||||
| "/_public/verify-email"
|
||||
| "/_protected/dashboard/settings"
|
||||
| "/_protected/dashboard/"
|
||||
| "/_protected/dashboard/$providerSlug/$accountId";
|
||||
fileRoutesById: FileRoutesById;
|
||||
}
|
||||
export interface RootRouteChildren {
|
||||
IndexRoute: typeof IndexRoute;
|
||||
ProtectedRoute: typeof ProtectedRouteWithChildren;
|
||||
PublicRoute: typeof PublicRouteWithChildren;
|
||||
DebugRoute: typeof DebugRoute;
|
||||
}
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface FileRoutesByPath {
|
||||
"/debug": {
|
||||
id: "/debug";
|
||||
path: "/debug";
|
||||
fullPath: "/debug";
|
||||
preLoaderRoute: typeof DebugRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/_public": {
|
||||
id: "/_public";
|
||||
path: "";
|
||||
fullPath: "";
|
||||
preLoaderRoute: typeof PublicRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/_protected": {
|
||||
id: "/_protected";
|
||||
path: "";
|
||||
fullPath: "";
|
||||
preLoaderRoute: typeof ProtectedRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/": {
|
||||
id: "/";
|
||||
path: "/";
|
||||
fullPath: "/";
|
||||
preLoaderRoute: typeof IndexRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/_public/verify-email": {
|
||||
id: "/_public/verify-email";
|
||||
path: "/verify-email";
|
||||
fullPath: "/verify-email";
|
||||
preLoaderRoute: typeof PublicVerifyEmailRouteImport;
|
||||
parentRoute: typeof PublicRoute;
|
||||
};
|
||||
"/_public/terms": {
|
||||
id: "/_public/terms";
|
||||
path: "/terms";
|
||||
fullPath: "/terms";
|
||||
preLoaderRoute: typeof PublicTermsRouteImport;
|
||||
parentRoute: typeof PublicRoute;
|
||||
};
|
||||
"/_public/signup": {
|
||||
id: "/_public/signup";
|
||||
path: "/signup";
|
||||
fullPath: "/signup";
|
||||
preLoaderRoute: typeof PublicSignupRouteImport;
|
||||
parentRoute: typeof PublicRoute;
|
||||
};
|
||||
"/_public/signin": {
|
||||
id: "/_public/signin";
|
||||
path: "/signin";
|
||||
fullPath: "/signin";
|
||||
preLoaderRoute: typeof PublicSigninRouteImport;
|
||||
parentRoute: typeof PublicRoute;
|
||||
};
|
||||
"/_public/reset-password": {
|
||||
id: "/_public/reset-password";
|
||||
path: "/reset-password";
|
||||
fullPath: "/reset-password";
|
||||
preLoaderRoute: typeof PublicResetPasswordRouteImport;
|
||||
parentRoute: typeof PublicRoute;
|
||||
};
|
||||
"/_public/privacy": {
|
||||
id: "/_public/privacy";
|
||||
path: "/privacy";
|
||||
fullPath: "/privacy";
|
||||
preLoaderRoute: typeof PublicPrivacyRouteImport;
|
||||
parentRoute: typeof PublicRoute;
|
||||
};
|
||||
"/_public/forgot-password": {
|
||||
id: "/_public/forgot-password";
|
||||
path: "/forgot-password";
|
||||
fullPath: "/forgot-password";
|
||||
preLoaderRoute: typeof PublicForgotPasswordRouteImport;
|
||||
parentRoute: typeof PublicRoute;
|
||||
};
|
||||
"/_public/contributors": {
|
||||
id: "/_public/contributors";
|
||||
path: "/contributors";
|
||||
fullPath: "/contributors";
|
||||
preLoaderRoute: typeof PublicContributorsRouteImport;
|
||||
parentRoute: typeof PublicRoute;
|
||||
};
|
||||
"/_protected/dashboard": {
|
||||
id: "/_protected/dashboard";
|
||||
path: "/dashboard";
|
||||
fullPath: "/dashboard";
|
||||
preLoaderRoute: typeof ProtectedDashboardRouteImport;
|
||||
parentRoute: typeof ProtectedRoute;
|
||||
};
|
||||
"/_protected/dashboard/": {
|
||||
id: "/_protected/dashboard/";
|
||||
path: "/";
|
||||
fullPath: "/dashboard/";
|
||||
preLoaderRoute: typeof ProtectedDashboardIndexRouteImport;
|
||||
parentRoute: typeof ProtectedDashboardRoute;
|
||||
};
|
||||
"/_protected/dashboard/settings": {
|
||||
id: "/_protected/dashboard/settings";
|
||||
path: "/settings";
|
||||
fullPath: "/dashboard/settings";
|
||||
preLoaderRoute: typeof ProtectedDashboardSettingsRouteImport;
|
||||
parentRoute: typeof ProtectedDashboardRoute;
|
||||
};
|
||||
"/_protected/dashboard/$providerSlug/$accountId": {
|
||||
id: "/_protected/dashboard/$providerSlug/$accountId";
|
||||
path: "/$providerSlug/$accountId";
|
||||
fullPath: "/dashboard/$providerSlug/$accountId";
|
||||
preLoaderRoute: typeof ProtectedDashboardProviderSlugAccountIdRouteImport;
|
||||
parentRoute: typeof ProtectedDashboardRoute;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
interface ProtectedDashboardRouteChildren {
|
||||
ProtectedDashboardSettingsRoute: typeof ProtectedDashboardSettingsRoute;
|
||||
ProtectedDashboardIndexRoute: typeof ProtectedDashboardIndexRoute;
|
||||
ProtectedDashboardProviderSlugAccountIdRoute: typeof ProtectedDashboardProviderSlugAccountIdRoute;
|
||||
}
|
||||
|
||||
const ProtectedDashboardRouteChildren: ProtectedDashboardRouteChildren = {
|
||||
ProtectedDashboardSettingsRoute: ProtectedDashboardSettingsRoute,
|
||||
ProtectedDashboardIndexRoute: ProtectedDashboardIndexRoute,
|
||||
ProtectedDashboardProviderSlugAccountIdRoute: ProtectedDashboardProviderSlugAccountIdRoute,
|
||||
};
|
||||
|
||||
const ProtectedDashboardRouteWithChildren = ProtectedDashboardRoute._addFileChildren(ProtectedDashboardRouteChildren);
|
||||
|
||||
interface ProtectedRouteChildren {
|
||||
ProtectedDashboardRoute: typeof ProtectedDashboardRouteWithChildren;
|
||||
}
|
||||
|
||||
const ProtectedRouteChildren: ProtectedRouteChildren = {
|
||||
ProtectedDashboardRoute: ProtectedDashboardRouteWithChildren,
|
||||
};
|
||||
|
||||
const ProtectedRouteWithChildren = ProtectedRoute._addFileChildren(ProtectedRouteChildren);
|
||||
|
||||
interface PublicRouteChildren {
|
||||
PublicContributorsRoute: typeof PublicContributorsRoute;
|
||||
PublicForgotPasswordRoute: typeof PublicForgotPasswordRoute;
|
||||
PublicPrivacyRoute: typeof PublicPrivacyRoute;
|
||||
PublicResetPasswordRoute: typeof PublicResetPasswordRoute;
|
||||
PublicSigninRoute: typeof PublicSigninRoute;
|
||||
PublicSignupRoute: typeof PublicSignupRoute;
|
||||
PublicTermsRoute: typeof PublicTermsRoute;
|
||||
PublicVerifyEmailRoute: typeof PublicVerifyEmailRoute;
|
||||
}
|
||||
|
||||
const PublicRouteChildren: PublicRouteChildren = {
|
||||
PublicContributorsRoute: PublicContributorsRoute,
|
||||
PublicForgotPasswordRoute: PublicForgotPasswordRoute,
|
||||
PublicPrivacyRoute: PublicPrivacyRoute,
|
||||
PublicResetPasswordRoute: PublicResetPasswordRoute,
|
||||
PublicSigninRoute: PublicSigninRoute,
|
||||
PublicSignupRoute: PublicSignupRoute,
|
||||
PublicTermsRoute: PublicTermsRoute,
|
||||
PublicVerifyEmailRoute: PublicVerifyEmailRoute,
|
||||
};
|
||||
|
||||
const PublicRouteWithChildren = PublicRoute._addFileChildren(PublicRouteChildren);
|
||||
|
||||
const rootRouteChildren: RootRouteChildren = {
|
||||
IndexRoute: IndexRoute,
|
||||
ProtectedRoute: ProtectedRouteWithChildren,
|
||||
PublicRoute: PublicRouteWithChildren,
|
||||
DebugRoute: DebugRoute,
|
||||
};
|
||||
export const routeTree = rootRouteImport._addFileChildren(rootRouteChildren)._addFileTypes<FileRouteTypes>();
|
||||
76
apps/web/src/routes/__root.tsx
Normal file
76
apps/web/src/routes/__root.tsx
Normal file
@@ -0,0 +1,76 @@
|
||||
import { createRootRoute, Outlet, ErrorComponent } from "@tanstack/react-router";
|
||||
import { ReactQueryProvider } from "@/components/providers/query-provider";
|
||||
import { ThemeProvider } from "@/components/providers/theme-provider";
|
||||
import { AppProviders } from "@/components/providers/app-providers";
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
import { TanStackRouterDevtools } from "@tanstack/router-devtools";
|
||||
import { geistSans, geistMono, manrope } from "@/utils/fonts";
|
||||
import { Toaster } from "sonner";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: RootComponent,
|
||||
notFoundComponent: NotFound,
|
||||
errorComponent: RootErrorComponent,
|
||||
});
|
||||
|
||||
function RootComponent() {
|
||||
return (
|
||||
<ReactQueryProvider>
|
||||
<AppProviders>
|
||||
<ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
|
||||
<div
|
||||
className={`bg-background text-foreground relative min-h-screen ${geistSans.variable} ${geistMono.variable} ${manrope.variable}`}
|
||||
>
|
||||
<Suspense fallback={<div className="flex h-screen items-center justify-center">Loading...</div>}>
|
||||
<Outlet />
|
||||
</Suspense>
|
||||
<Toaster position="top-center" richColors theme="system" />
|
||||
</div>
|
||||
<Suspense fallback={null}>
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
</Suspense>
|
||||
</ThemeProvider>
|
||||
</AppProviders>
|
||||
<Suspense fallback={null}>
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</Suspense>
|
||||
</ReactQueryProvider>
|
||||
);
|
||||
}
|
||||
|
||||
function RootErrorComponent({ error }: { error: Error }) {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center p-4">
|
||||
<div className="max-w-md text-center">
|
||||
<h1 className="text-2xl font-bold text-red-600">Oops! Something went wrong</h1>
|
||||
<p className="text-muted-foreground mt-4">{error.message}</p>
|
||||
<div className="mt-4 space-x-2">
|
||||
<button
|
||||
onClick={() => window.location.reload()}
|
||||
className="bg-primary text-primary-foreground rounded px-4 py-2"
|
||||
>
|
||||
Reload Page
|
||||
</button>
|
||||
<a href="/" className="text-primary underline">
|
||||
Go Home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NotFound() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="text-center">
|
||||
<h1 className="text-6xl font-bold">404</h1>
|
||||
<p className="text-muted-foreground mt-4 text-xl">Page not found</p>
|
||||
<a href="/" className="text-primary mt-6 inline-block text-lg underline">
|
||||
Return Home
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
44
apps/web/src/routes/_protected.tsx
Normal file
44
apps/web/src/routes/_protected.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
|
||||
import { authClient } from "@nimbus/auth/auth-client";
|
||||
|
||||
export const Route = createFileRoute("/_protected")({
|
||||
beforeLoad: async ({ location }) => {
|
||||
try {
|
||||
// Check if user is authenticated
|
||||
const session = await authClient.getSession();
|
||||
|
||||
if (!session?.data?.user) {
|
||||
// Redirect to signin with the current path as redirect parameter
|
||||
throw redirect({
|
||||
to: "/signin",
|
||||
search: {
|
||||
redirect: location.pathname,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Return session data to be available in route context
|
||||
return {
|
||||
session,
|
||||
};
|
||||
} catch (error) {
|
||||
// If it's already a redirect, re-throw it
|
||||
if (error instanceof Error && error.message.includes("redirect")) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// For other errors, redirect to signin
|
||||
throw redirect({
|
||||
to: "/signin",
|
||||
search: {
|
||||
redirect: location.pathname,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
component: ProtectedLayout,
|
||||
});
|
||||
|
||||
function ProtectedLayout() {
|
||||
return <Outlet />;
|
||||
}
|
||||
17
apps/web/src/routes/_protected/dashboard.tsx
Normal file
17
apps/web/src/routes/_protected/dashboard.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { DefaultAccountProvider } from "@/components/providers/default-account-provider";
|
||||
import { UserInfoProvider } from "@/components/providers/user-info-provider";
|
||||
import { createFileRoute, Outlet } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createFileRoute("/_protected/dashboard")({
|
||||
component: DashboardLayout,
|
||||
});
|
||||
|
||||
function DashboardLayout() {
|
||||
return (
|
||||
<UserInfoProvider>
|
||||
<DefaultAccountProvider>
|
||||
<Outlet />
|
||||
</DefaultAccountProvider>
|
||||
</UserInfoProvider>
|
||||
);
|
||||
}
|
||||
@@ -1,18 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import DndKitProvider from "@/components/providers/dnd-kit-provider";
|
||||
import { FileTable } from "@/components/dashboard/file-browser";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { useGetFiles } from "@/hooks/useFileOperations";
|
||||
import { Header } from "@/components/dashboard/header";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function DrivePage() {
|
||||
const searchParams = useSearchParams();
|
||||
const folderId = searchParams.get("folderId") ?? "root";
|
||||
type DashboardSearch = {
|
||||
folderId?: string;
|
||||
};
|
||||
|
||||
export const Route = createFileRoute("/_protected/dashboard/$providerSlug/$accountId")({
|
||||
component: DrivePage,
|
||||
validateSearch: (search: Record<string, unknown>): DashboardSearch => {
|
||||
return {
|
||||
folderId: (search.folderId as string) || undefined,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function DrivePage() {
|
||||
const { folderId } = Route.useSearch();
|
||||
const currentFolderId = folderId ?? "root";
|
||||
|
||||
const { data, isLoading, refetch, error } = useGetFiles({
|
||||
parentId: folderId,
|
||||
parentId: currentFolderId,
|
||||
pageSize: 30,
|
||||
pageToken: undefined,
|
||||
// TODO: implement sorting, filtering, pagination, and a generalized web content/view interfaces
|
||||
@@ -22,7 +33,7 @@ export default function DrivePage() {
|
||||
return (
|
||||
<>
|
||||
<Suspense fallback={null}>
|
||||
<DndKitProvider parentId={folderId}>
|
||||
<DndKitProvider parentId={currentFolderId}>
|
||||
<Header />
|
||||
<FileTable files={data || []} isLoading={isLoading} refetch={refetch} error={error} />
|
||||
</DndKitProvider>
|
||||
@@ -1,16 +1,19 @@
|
||||
"use client";
|
||||
|
||||
import { useUserInfoProvider } from "@/components/providers/user-info-provider";
|
||||
import { LoadingStatePage } from "@/components/loading-state-page";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export default function DashboardPage() {
|
||||
export const Route = createFileRoute("/_protected/dashboard/")({
|
||||
component: DashboardPage,
|
||||
});
|
||||
|
||||
function DashboardPage() {
|
||||
const { error } = useUserInfoProvider();
|
||||
const title = "Loading your dashboard...";
|
||||
const description = "Please wait while we fetch your provider and account information.";
|
||||
const errorTitle = "Error loading your dashboard";
|
||||
const errorDescription = "Please try again later.";
|
||||
|
||||
// DefaultAccountProvider navigates to the default account /dashboard/:providerSlug/:accountId, if on /dashboard. In layout.tsx
|
||||
// DefaultAccountProvider navigates to the default account /dashboard/:providerSlug/:accountId
|
||||
|
||||
return (
|
||||
<LoadingStatePage
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { type ApiResponse, type DriveProvider } from "@nimbus/shared";
|
||||
import { useEffect, useState, type ChangeEvent } from "react";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { authClient } from "@nimbus/auth/auth-client";
|
||||
import env from "@nimbus/env/client";
|
||||
import { toast } from "sonner";
|
||||
@@ -16,9 +15,13 @@ import { ConnectedAccountsSection } from "@/components/settings/connected-accoun
|
||||
import { ProfileSection } from "@/components/settings/profile-section";
|
||||
import { SettingsHeader } from "@/components/settings/header";
|
||||
|
||||
export const Route = createFileRoute("/_protected/dashboard/settings")({
|
||||
component: SettingsPage,
|
||||
});
|
||||
|
||||
// TODO(feat): back button in header goes to a callbackUrl
|
||||
|
||||
export default function SettingsPage() {
|
||||
function SettingsPage() {
|
||||
const { user, accounts, refreshUser, refreshAccounts } = useUserInfoProvider();
|
||||
const { defaultAccountId } = useDefaultAccountProvider();
|
||||
const { unlinkAccount } = useUnlinkAccount();
|
||||
33
apps/web/src/routes/_public.tsx
Normal file
33
apps/web/src/routes/_public.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
|
||||
import { authClient } from "@nimbus/auth/auth-client";
|
||||
|
||||
export const Route = createFileRoute("/_public")({
|
||||
beforeLoad: async ({ location }) => {
|
||||
try {
|
||||
// Check if user is authenticated
|
||||
const session = await authClient.getSession();
|
||||
|
||||
// If user is authenticated and trying to access signin/signup, redirect to dashboard
|
||||
if (session?.data?.user) {
|
||||
const authPaths = ["/signin", "/signup"];
|
||||
if (authPaths.includes(location.pathname)) {
|
||||
throw redirect({
|
||||
to: "/dashboard",
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// If it's a redirect, re-throw it
|
||||
if (error instanceof Error && error.message.includes("redirect")) {
|
||||
throw error;
|
||||
}
|
||||
// For other errors, just continue (allow access to public routes)
|
||||
console.error("Auth check error in public route:", error);
|
||||
}
|
||||
},
|
||||
component: PublicLayout,
|
||||
});
|
||||
|
||||
function PublicLayout() {
|
||||
return <Outlet />;
|
||||
}
|
||||
@@ -7,16 +7,10 @@ import {
|
||||
} from "@/components/landing-page/legal/text";
|
||||
import { COMPANY_NAME, CONTACT_EMAIL, LEGAL_UPDATE_DATE, URL, WEBSITE_NAME } from "@nimbus/shared";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
// this is a copy of Analogs privacy component with some changes
|
||||
// https://github.com/analogdotnow/Analog/blob/main/apps/web/src/app/(legal)/privacy/page.tsx
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Privacy Policy - Nimbus",
|
||||
};
|
||||
|
||||
export default function PrivacyPage() {
|
||||
export function PrivacyPageContent() {
|
||||
return (
|
||||
<main className="relative">
|
||||
<div className="relative container mx-auto px-4 py-16">
|
||||
16
apps/web/src/routes/_public/contributors.tsx
Normal file
16
apps/web/src/routes/_public/contributors.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
import Contributors from "@/components/contributors/contributors";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import Header from "@/components/home/header";
|
||||
|
||||
export const Route = createFileRoute("/_public/contributors")({
|
||||
component: ContributorsPage,
|
||||
});
|
||||
|
||||
function ContributorsPage() {
|
||||
return (
|
||||
<div>
|
||||
<Header />
|
||||
<Contributors />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import { ForgotPasswordForm } from "@/components/auth/forgot-password-form";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
export const Route = createFileRoute("/_public/forgot-password")({
|
||||
component: ForgotPasswordPage,
|
||||
});
|
||||
|
||||
function ForgotPasswordPage() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full justify-center sm:items-center">
|
||||
<div className="size-full max-w-md px-2 py-10 sm:max-w-sm">
|
||||
10
apps/web/src/routes/_public/privacy.tsx
Normal file
10
apps/web/src/routes/_public/privacy.tsx
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { PrivacyPageContent } from "./-privacy-content";
|
||||
|
||||
export const Route = createFileRoute("/_public/privacy")({
|
||||
component: PrivacyPage,
|
||||
});
|
||||
|
||||
function PrivacyPage() {
|
||||
return <PrivacyPageContent />;
|
||||
}
|
||||
@@ -1,6 +1,11 @@
|
||||
import { ResetPasswordForm } from "@/components/auth/reset-password-form";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export const Route = createFileRoute("/_public/reset-password")({
|
||||
component: ResetPasswordPage,
|
||||
});
|
||||
|
||||
function ResetPasswordContent() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full justify-center sm:items-center">
|
||||
@@ -11,7 +16,7 @@ function ResetPasswordContent() {
|
||||
);
|
||||
}
|
||||
|
||||
export default function ResetPasswordPage() {
|
||||
function ResetPasswordPage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<ResetPasswordContent />
|
||||
@@ -1,8 +1,18 @@
|
||||
import { SigninFormSkeleton } from "@/components/auth/skeletons/signin-form";
|
||||
import { SignInForm } from "@/components/auth/signin-form";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function SigninPage() {
|
||||
export const Route = createFileRoute("/_public/signin")({
|
||||
component: SigninPage,
|
||||
validateSearch: (search: Record<string, unknown>) => {
|
||||
return {
|
||||
redirect: (search.redirect as string) || undefined,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function SigninPage() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full justify-center sm:items-center">
|
||||
<div className="size-full max-w-md px-2 py-10 sm:max-w-sm">
|
||||
@@ -1,8 +1,13 @@
|
||||
import { SignupFormSkeleton } from "@/components/auth/skeletons/signup-form";
|
||||
import { SignupForm } from "@/components/auth/signup-form";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function SignupPage() {
|
||||
export const Route = createFileRoute("/_public/signup")({
|
||||
component: SignupPage,
|
||||
});
|
||||
|
||||
function SignupPage() {
|
||||
return (
|
||||
<div className="flex min-h-svh w-full justify-center sm:items-center">
|
||||
<div className="size-full max-w-md px-2 py-10 sm:max-w-sm">
|
||||
@@ -1,16 +1,13 @@
|
||||
import { LegalHeading1, LegalHeading2, LegalParagraph, LegalTextLink } from "@/components/landing-page/legal/text";
|
||||
import { COMPANY_NAME, COUNTRY, LEGAL_UPDATE_DATE, STATE, URL } from "@nimbus/shared";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Card, CardContent } from "@/components/ui/card";
|
||||
import type { Metadata } from "next";
|
||||
|
||||
// this is a copy of Analogs terms component with some changes
|
||||
// https://github.com/analogdotnow/Analog/blob/main/apps/web/src/app/(legal)/terms/page.tsx
|
||||
export const Route = createFileRoute("/_public/terms")({
|
||||
component: TermsPage,
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: `Terms of Service - ${COMPANY_NAME}`,
|
||||
};
|
||||
|
||||
export default function TermsPage() {
|
||||
function TermsPage() {
|
||||
return (
|
||||
<main>
|
||||
<div className="container mx-auto px-4 py-16">
|
||||
@@ -1,7 +1,12 @@
|
||||
import { VerifyEmailContent } from "@/components/auth/verify-email-content";
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export default function VerifyEmailPage() {
|
||||
export const Route = createFileRoute("/_public/verify-email")({
|
||||
component: VerifyEmailPage,
|
||||
});
|
||||
|
||||
function VerifyEmailPage() {
|
||||
return (
|
||||
<Suspense>
|
||||
<div className="flex min-h-svh w-full justify-center sm:items-center">
|
||||
164
apps/web/src/routes/debug.tsx
Normal file
164
apps/web/src/routes/debug.tsx
Normal file
@@ -0,0 +1,164 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { authClient } from "@nimbus/auth/auth-client";
|
||||
import { useState, useEffect } from "react";
|
||||
|
||||
export const Route = createFileRoute("/debug")({
|
||||
component: DebugPage,
|
||||
});
|
||||
|
||||
function DebugPage() {
|
||||
const [authState, setAuthState] = useState<any>(null);
|
||||
const [authError, setAuthError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const checkAuth = async () => {
|
||||
try {
|
||||
const session = await authClient.getSession();
|
||||
setAuthState(session);
|
||||
} catch (error) {
|
||||
setAuthError(error instanceof Error ? error.message : "Unknown error");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
checkAuth();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="bg-background min-h-screen p-8">
|
||||
<div className="mx-auto max-w-4xl space-y-6">
|
||||
<div className="border-border bg-card rounded-lg border p-6">
|
||||
<h1 className="text-3xl font-bold">Debug Information</h1>
|
||||
<p className="text-muted-foreground mt-2">TanStack Router + Vite Migration Debug Page</p>
|
||||
</div>
|
||||
|
||||
{/* Router Status */}
|
||||
<div className="border-border bg-card rounded-lg border p-6">
|
||||
<h2 className="text-xl font-semibold">✅ Router Status</h2>
|
||||
<p className="text-muted-foreground mt-2">If you can see this page, TanStack Router is working correctly!</p>
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm">Current Path:</span>
|
||||
<code className="bg-muted rounded px-2 py-1 text-sm">{window.location.pathname}</code>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm">Search:</span>
|
||||
<code className="bg-muted rounded px-2 py-1 text-sm">{window.location.search || "(none)"}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Authentication Status */}
|
||||
<div className="border-border bg-card rounded-lg border p-6">
|
||||
<h2 className="text-xl font-semibold">🔐 Authentication Status</h2>
|
||||
{loading ? (
|
||||
<p className="text-muted-foreground mt-2">Checking authentication...</p>
|
||||
) : authError ? (
|
||||
<div className="mt-4">
|
||||
<p className="text-red-600">Error: {authError}</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm">Authenticated:</span>
|
||||
<code className="bg-muted rounded px-2 py-1 text-sm">{authState?.user ? "Yes" : "No"}</code>
|
||||
</div>
|
||||
{authState?.user && (
|
||||
<>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm">User Email:</span>
|
||||
<code className="bg-muted rounded px-2 py-1 text-sm">{authState.user.email}</code>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm">User Name:</span>
|
||||
<code className="bg-muted rounded px-2 py-1 text-sm">{authState.user.name || "(not set)"}</code>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Environment Check */}
|
||||
<div className="border-border bg-card rounded-lg border p-6">
|
||||
<h2 className="text-xl font-semibold">🌍 Environment</h2>
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm">Mode:</span>
|
||||
<code className="bg-muted rounded px-2 py-1 text-sm">{import.meta.env.MODE}</code>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm">Dev:</span>
|
||||
<code className="bg-muted rounded px-2 py-1 text-sm">{import.meta.env.DEV ? "Yes" : "No"}</code>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm">Prod:</span>
|
||||
<code className="bg-muted rounded px-2 py-1 text-sm">{import.meta.env.PROD ? "Yes" : "No"}</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Links */}
|
||||
<div className="border-border bg-card rounded-lg border p-6">
|
||||
<h2 className="text-xl font-semibold">🔗 Quick Links</h2>
|
||||
<div className="mt-4 grid grid-cols-2 gap-2">
|
||||
<a href="/" className="text-primary hover:underline">
|
||||
→ Home
|
||||
</a>
|
||||
<a href="/signin" className="text-primary hover:underline">
|
||||
→ Sign In
|
||||
</a>
|
||||
<a href="/signup" className="text-primary hover:underline">
|
||||
→ Sign Up
|
||||
</a>
|
||||
<a href="/dashboard" className="text-primary hover:underline">
|
||||
→ Dashboard
|
||||
</a>
|
||||
<a href="/terms" className="text-primary hover:underline">
|
||||
→ Terms
|
||||
</a>
|
||||
<a href="/privacy" className="text-primary hover:underline">
|
||||
→ Privacy
|
||||
</a>
|
||||
<a href="/contributors" className="text-primary hover:underline">
|
||||
→ Contributors
|
||||
</a>
|
||||
<a href="/nonexistent" className="text-primary hover:underline">
|
||||
→ 404 Test
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Info */}
|
||||
<div className="border-border bg-card rounded-lg border p-6">
|
||||
<h2 className="text-xl font-semibold">💻 System Info</h2>
|
||||
<div className="mt-4 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm">User Agent:</span>
|
||||
<code className="bg-muted rounded px-2 py-1 text-xs">{navigator.userAgent}</code>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-mono text-sm">Viewport:</span>
|
||||
<code className="bg-muted rounded px-2 py-1 text-sm">
|
||||
{window.innerWidth} x {window.innerHeight}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Console Log */}
|
||||
<div className="border-border bg-card rounded-lg border p-6">
|
||||
<h2 className="text-xl font-semibold">📝 Instructions</h2>
|
||||
<div className="text-muted-foreground mt-4 space-y-2 text-sm">
|
||||
<p>1. Open your browser's developer console (F12)</p>
|
||||
<p>2. Check the Console tab for any errors</p>
|
||||
<p>3. Check the Network tab to see if all assets are loading</p>
|
||||
<p>4. Try navigating to different routes using the links above</p>
|
||||
<p>5. If you see a blank page on other routes, check the console for errors</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
21
apps/web/src/routes/index.tsx
Normal file
21
apps/web/src/routes/index.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import Hero from "@/components/home/hero";
|
||||
import { Suspense } from "react";
|
||||
|
||||
export const Route = createFileRoute("/")({
|
||||
component: HomePage,
|
||||
});
|
||||
|
||||
function HomePage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex min-h-screen items-center justify-center">
|
||||
<div className="border-primary h-8 w-8 animate-spin rounded-full border-b-2"></div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Hero />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
@@ -1,8 +1,17 @@
|
||||
import { Manrope } from "next/font/google";
|
||||
// Google Fonts are loaded via index.html or CSS
|
||||
// This file exports font configuration for use in the app
|
||||
|
||||
export const manrope = Manrope({
|
||||
subsets: ["latin"],
|
||||
weight: ["200", "300", "400", "500", "600", "700", "800"],
|
||||
export const manrope = {
|
||||
variable: "--font-manrope",
|
||||
display: "swap",
|
||||
});
|
||||
className: "font-manrope",
|
||||
};
|
||||
|
||||
export const geistSans = {
|
||||
variable: "--font-geist-sans",
|
||||
className: "font-geist-sans",
|
||||
};
|
||||
|
||||
export const geistMono = {
|
||||
variable: "--font-geist-mono",
|
||||
className: "font-geist-mono",
|
||||
};
|
||||
|
||||
1
apps/web/src/vite-env.d.ts
vendored
Normal file
1
apps/web/src/vite-env.d.ts
vendored
Normal file
@@ -0,0 +1 @@
|
||||
/// <reference types="vite/client" />
|
||||
@@ -1,13 +1,14 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"jsx": "preserve",
|
||||
"lib": ["esnext", "dom", "dom.iterable"],
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
"lib": ["ES2020", "dom", "dom.iterable"],
|
||||
"target": "ES2020",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
@@ -23,12 +24,6 @@
|
||||
"path": "../../packages/shared/tsconfig.json"
|
||||
}
|
||||
],
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
".next/types/**/*.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
"src/components/providers/useDefaultAccount"
|
||||
],
|
||||
"include": ["src", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
|
||||
35
apps/web/vite.config.ts
Normal file
35
apps/web/vite.config.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import tanstackRouter from "@tanstack/router-plugin/vite";
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import { defineConfig } from "vite";
|
||||
import path from "path";
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
tanstackRouter({
|
||||
routesDirectory: "./src/routes",
|
||||
generatedRouteTree: "./src/routeTree.gen.ts",
|
||||
routeFileIgnorePrefix: "-",
|
||||
quoteStyle: "double",
|
||||
}),
|
||||
react(),
|
||||
tsconfigPaths(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
"@/public": path.resolve(__dirname, "./public"),
|
||||
},
|
||||
},
|
||||
server: {
|
||||
port: 3000,
|
||||
host: true,
|
||||
},
|
||||
build: {
|
||||
outDir: "dist",
|
||||
sourcemap: true,
|
||||
},
|
||||
optimizeDeps: {
|
||||
exclude: ["@nimbus/auth", "@nimbus/env", "@nimbus/server", "@nimbus/shared"],
|
||||
},
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import defineConfig from "@nimbus/vitest";
|
||||
|
||||
export default defineConfig;
|
||||
@@ -1,48 +0,0 @@
|
||||
services:
|
||||
# Database Service
|
||||
db:
|
||||
image: postgres:16
|
||||
container_name: nimbus-test-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: nimbus-test
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres-data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
networks:
|
||||
- nimbus-test-network
|
||||
|
||||
# S3 Service
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
container_name: nimbus-test-minio
|
||||
ports:
|
||||
- "127.0.0.1:9000:9000" # MinIO API
|
||||
- "127.0.0.1:9001:9001" # MinIO Console
|
||||
environment:
|
||||
- MINIO_ROOT_USER=minioadmin
|
||||
- MINIO_ROOT_PASSWORD=minioadmin
|
||||
- MINIO_REGION=us-east-1
|
||||
command: server /data --console-address :9001
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/ready"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
name: nimbus-test-db-data
|
||||
|
||||
networks:
|
||||
nimbus-test-network:
|
||||
driver: bridge
|
||||
@@ -56,59 +56,59 @@ services:
|
||||
networks:
|
||||
- nimbus-network
|
||||
|
||||
# Server Service
|
||||
server:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: apps/server/Dockerfile
|
||||
target: runner
|
||||
container_name: nimbus-server
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
PORT: ${SERVER_PORT:-1284}
|
||||
DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
# Valkey connection settings with username/password
|
||||
VALKEY_HOST: ${VALKEY_HOST}
|
||||
VALKEY_PORT: ${VALKEY_PORT}
|
||||
VALKEY_USERNAME: ${VALKEY_USERNAME}
|
||||
VALKEY_PASSWORD: ${VALKEY_PASSWORD}
|
||||
# Connection URL format with username and password
|
||||
VALKEY_URL: valkey://${VALKEY_USERNAME}:${VALKEY_PASSWORD}@${VALKEY_HOST}:${VALKEY_PORT}/0
|
||||
ports:
|
||||
- "1284:1284"
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
cache:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- nimbus-network
|
||||
# # Server Service
|
||||
# server:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: apps/server/Dockerfile
|
||||
# target: runner
|
||||
# container_name: nimbus-server
|
||||
# restart: unless-stopped
|
||||
# env_file: .env
|
||||
# environment:
|
||||
# NODE_ENV: production
|
||||
# PORT: ${SERVER_PORT:-1284}
|
||||
# DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}
|
||||
# # Valkey connection settings with username/password
|
||||
# VALKEY_HOST: ${VALKEY_HOST}
|
||||
# VALKEY_PORT: ${VALKEY_PORT}
|
||||
# VALKEY_USERNAME: ${VALKEY_USERNAME}
|
||||
# VALKEY_PASSWORD: ${VALKEY_PASSWORD}
|
||||
# # Connection URL format with username and password
|
||||
# VALKEY_URL: valkey://${VALKEY_USERNAME}:${VALKEY_PASSWORD}@${VALKEY_HOST}:${VALKEY_PORT}/0
|
||||
# ports:
|
||||
# - "1284:1284"
|
||||
# depends_on:
|
||||
# db:
|
||||
# condition: service_healthy
|
||||
# cache:
|
||||
# condition: service_healthy
|
||||
# networks:
|
||||
# - nimbus-network
|
||||
|
||||
# Web Service
|
||||
web:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: apps/web/Dockerfile
|
||||
target: runner
|
||||
args:
|
||||
NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL}
|
||||
NEXT_PUBLIC_FRONTEND_URL: ${NEXT_PUBLIC_FRONTEND_URL}
|
||||
container_name: nimbus-web
|
||||
restart: unless-stopped
|
||||
env_file: .env
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL}
|
||||
NEXT_PUBLIC_FRONTEND_URL: ${NEXT_PUBLIC_FRONTEND_URL}
|
||||
ports:
|
||||
- "3000:3000"
|
||||
depends_on:
|
||||
server:
|
||||
condition: service_started
|
||||
networks:
|
||||
- nimbus-network
|
||||
# # Web Service
|
||||
# web:
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: apps/web/Dockerfile
|
||||
# target: runner
|
||||
# args:
|
||||
# NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL}
|
||||
# NEXT_PUBLIC_FRONTEND_URL: ${NEXT_PUBLIC_FRONTEND_URL}
|
||||
# container_name: nimbus-web
|
||||
# restart: unless-stopped
|
||||
# env_file: .env
|
||||
# environment:
|
||||
# NODE_ENV: production
|
||||
# NEXT_PUBLIC_BACKEND_URL: ${NEXT_PUBLIC_BACKEND_URL}
|
||||
# NEXT_PUBLIC_FRONTEND_URL: ${NEXT_PUBLIC_FRONTEND_URL}
|
||||
# ports:
|
||||
# - "3000:3000"
|
||||
# depends_on:
|
||||
# server:
|
||||
# condition: service_started
|
||||
# networks:
|
||||
# - nimbus-network
|
||||
|
||||
volumes:
|
||||
postgres-data:
|
||||
|
||||
@@ -5,7 +5,6 @@ const indexEntry = "src/index.{ts,js}";
|
||||
const project = "**/*.{ts,js}";
|
||||
|
||||
const ignoreUtils = ["src/utils/*"];
|
||||
const ignoreViteV8 = ["@vitest/coverage-v8"];
|
||||
const ignoreComponents = ["**/components/**"];
|
||||
|
||||
const config: KnipConfig = {
|
||||
@@ -45,11 +44,6 @@ const config: KnipConfig = {
|
||||
"packages/tsconfig": {
|
||||
entry: "base.json",
|
||||
},
|
||||
|
||||
"packages/vitest": {
|
||||
entry: "src/index.ts",
|
||||
ignoreDependencies: ignoreViteV8,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
32
package.json
32
package.json
@@ -3,10 +3,6 @@
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"@changesets/cli": "^2.29.5",
|
||||
"@nimbus/vitest": "workspace:*",
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"@vitest/ui": "^3.2.4",
|
||||
"dotenv": "^17.2.1",
|
||||
"glob": "^11.0.3",
|
||||
"husky": "^9.1.7",
|
||||
"knip": "^5.62.0",
|
||||
@@ -16,42 +12,26 @@
|
||||
"prettier-plugin-sort-imports": "^1.8.8",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"turbo": "^2.5.6",
|
||||
"typescript": "^5.9.2",
|
||||
"vite-tsconfig-paths": "^5.1.4",
|
||||
"vitest": "^3.2.4",
|
||||
"vitest-mock-extended": "^3.1.0"
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"packageManager": "bun@1.2.17",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"clean-install": "bun install --force",
|
||||
"check-types": "tsc --noEmit",
|
||||
"knip": "knip-bun --config knip.config.ts",
|
||||
"ch": "changeset",
|
||||
"dev:web": "bun --elide-lines 0 --filter @nimbus/web --env-file .env dev",
|
||||
"dev:server": "bun --elide-lines 0 --filter @nimbus/server --env-file .env dev",
|
||||
"dev": "turbo dev",
|
||||
"dev:verbose": "bun --elide-lines 0 --filter \"*\" --env-file .env dev",
|
||||
"gh:env:sync:preview": "./scripts/gh-env-sync.sh --env preview",
|
||||
"gh:env:sync:staging": "./scripts/gh-env-sync.sh --env staging",
|
||||
"gh:env:sync:production": "./scripts/gh-env-sync.sh --env production",
|
||||
"db:up": "bun run --cwd=packages/db docker:up",
|
||||
"db:down": "bun run --cwd=packages/db docker:down",
|
||||
"db:reset": "bun run --cwd=packages/db docker:reset",
|
||||
"db:push": "bun --elide-lines 0 --filter @nimbus/db --env-file .env push",
|
||||
"db:studio": "bun --elide-lines 0 --filter @nimbus/db --env-file .env studio",
|
||||
"cache:up": "bun run --cwd=packages/cache docker:up",
|
||||
"cache:down": "bun run --cwd=packages/cache docker:down",
|
||||
"cache:reset": "bun run --cwd=packages/cache docker:reset",
|
||||
"build:web": "bun run --cwd=apps/web build",
|
||||
"build:server": "bun run --cwd=apps/server build",
|
||||
"build": "turbo build",
|
||||
"start": "turbo start",
|
||||
"env:sync": "bun run scripts/copy-env-files.ts",
|
||||
"env:sync:dry-run": "bun run scripts/copy-env-files.ts --dry-run",
|
||||
"env:clean": "bun run scripts/delete-child-env-files.ts",
|
||||
"env:clean:dry-run": "bun run scripts/delete-child-env-files.ts --dry-run",
|
||||
"env:clean-sync": "bun run env:clean && bun run env:sync",
|
||||
"docker:build": "docker compose build",
|
||||
"docker:up": "docker compose up",
|
||||
"docker:down": "docker compose down",
|
||||
@@ -59,15 +39,6 @@
|
||||
"docker:reset": "bun run docker:remove && bun run docker:up",
|
||||
"format": "bun prettier . --write --list-different",
|
||||
"lint": "bun run oxlint --fix && cd apps/web && bun run lint --fix",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest",
|
||||
"test:ui": "vitest --ui",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:coverage:ui": "vitest --coverage --ui",
|
||||
"test:coverage:dev": "vitest run --coverage --coverage.thresholds.statements=0 --coverage.thresholds.branches=0 --coverage.thresholds.functions=0 --coverage.thresholds.lines=0",
|
||||
"test:docker:up": "docker compose -f docker-compose.test.yml up -d",
|
||||
"test:docker:down": "docker compose -f docker-compose.test.yml down",
|
||||
"test:docker:remove": "docker compose -f docker-compose.test.yml down --rmi local -v",
|
||||
"prepare": "husky"
|
||||
},
|
||||
"type": "module",
|
||||
@@ -76,7 +47,6 @@
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"@better-auth/stripe": "^1.3.24",
|
||||
"stripe": "^19.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,6 @@
|
||||
"./auth-client": "./src/auth-client.ts",
|
||||
"./auth": "./src/auth.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nimbus/vitest": "workspace:*"
|
||||
},
|
||||
"dependencies": {
|
||||
"@better-auth/stripe": "^1.3.24",
|
||||
"@nimbus/cache": "workspace:*",
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import { dbMock, mockFindFirst, mockSet, mockUpdate, mockWhere } from "@nimbus/db/mock";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { afterAccountCreation, auth } from "../src/auth";
|
||||
import { betterAuth } from "better-auth";
|
||||
|
||||
// Mock better-auth
|
||||
vi.mock("better-auth", () => ({
|
||||
betterAuth: vi.fn(() => ({
|
||||
api: {
|
||||
getSession: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
user: { id: "user123", email: "test@example.com" },
|
||||
})
|
||||
),
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock @nimbus/env/server
|
||||
vi.mock("@nimbus/env/server", () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
DATABASE_URL: "mock-db-url",
|
||||
FRONTEND_URL: "https://frontend.com",
|
||||
BACKEND_URL: "https://backend.com",
|
||||
GOOGLE_CLIENT_ID: "google-id",
|
||||
GOOGLE_CLIENT_SECRET: "google-secret",
|
||||
MICROSOFT_CLIENT_ID: "ms-id",
|
||||
MICROSOFT_CLIENT_SECRET: "ms-secret",
|
||||
BOX_CLIENT_ID: "box-id",
|
||||
BOX_CLIENT_SECRET: "box-secret",
|
||||
DROPBOX_CLIENT_ID: "dropbox-id",
|
||||
DROPBOX_CLIENT_SECRET: "dropbox-secret",
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock send-mail
|
||||
vi.mock("../src/utils/send-mail", () => ({
|
||||
sendMail: vi.fn(),
|
||||
}));
|
||||
|
||||
// TESTS
|
||||
describe("createAuth", () => {
|
||||
it("should return a valid auth object", () => {
|
||||
expect(auth).toBeDefined();
|
||||
expect(betterAuth).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("afterAccountCreation", () => {
|
||||
const account = {
|
||||
id: "account1",
|
||||
accountId: "acc123",
|
||||
providerId: "google",
|
||||
userId: "user1",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset the actual mocked functions
|
||||
mockFindFirst.mockReset();
|
||||
mockUpdate.mockReset();
|
||||
mockSet.mockReset();
|
||||
mockWhere.mockReset();
|
||||
});
|
||||
|
||||
it("should update user with default account/provider id if not set", async () => {
|
||||
// Mock findFirst to return a user without default IDs
|
||||
mockFindFirst.mockResolvedValue({
|
||||
id: "user1",
|
||||
defaultAccountId: "accountid",
|
||||
defaultProviderId: "google",
|
||||
} as any);
|
||||
|
||||
await afterAccountCreation(dbMock, account);
|
||||
|
||||
// For now, just test that findFirst was called
|
||||
expect(mockFindFirst).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should do nothing if user not found", async () => {
|
||||
mockFindFirst.mockResolvedValueOnce(undefined);
|
||||
|
||||
await afterAccountCreation(dbMock, account);
|
||||
|
||||
expect(mockFindFirst).toHaveBeenCalled();
|
||||
expect(mockSet).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should do nothing if default IDs already set", async () => {
|
||||
mockFindFirst.mockResolvedValueOnce({
|
||||
id: "user1",
|
||||
defaultAccountId: "acc123",
|
||||
defaultProviderId: "google",
|
||||
} as any);
|
||||
|
||||
await afterAccountCreation(dbMock, account);
|
||||
|
||||
expect(mockFindFirst).toHaveBeenCalled();
|
||||
expect(mockSet).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import defineConfig from "@nimbus/vitest";
|
||||
|
||||
export default defineConfig;
|
||||
8
packages/cache/package.json
vendored
8
packages/cache/package.json
vendored
@@ -8,19 +8,15 @@
|
||||
"./rate-limiters": "./src/rate-limiters.ts"
|
||||
},
|
||||
"scripts": {
|
||||
"dev": "docker compose up",
|
||||
"lint": "bun run oxlint . --fix",
|
||||
"format": "bun prettier . --write --list-different",
|
||||
"docker:up": "docker compose up -d",
|
||||
"docker:down": "docker compose down",
|
||||
"docker:remove": "docker compose down --rmi local -v",
|
||||
"docker:reset": "bun run docker:remove && bun run docker:up"
|
||||
"docker:down": "docker compose down"
|
||||
},
|
||||
"dependencies": {
|
||||
"iovalkey": "^0.3.3",
|
||||
"rate-limiter-flexible": "^7.2.0",
|
||||
"@nimbus/env": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nimbus/vitest": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
1
packages/cache/tests/cache.test.ts
vendored
1
packages/cache/tests/cache.test.ts
vendored
@@ -1 +0,0 @@
|
||||
// TODO: the model for the KV requires different tests. I removed the old ones to not confuse others
|
||||
3
packages/cache/vitest.config.ts
vendored
3
packages/cache/vitest.config.ts
vendored
@@ -1,3 +0,0 @@
|
||||
import defineConfig from "@nimbus/vitest";
|
||||
|
||||
export default defineConfig;
|
||||
@@ -4,27 +4,18 @@
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "docker compose up",
|
||||
"lint": "bun run oxlint . --fix",
|
||||
"format": "bun prettier . --write --list-different",
|
||||
"generate": "bun --bun run drizzle-kit generate --config=drizzle.config.ts",
|
||||
"migrate": "bun --bun run drizzle-kit migrate --config=drizzle.config.ts",
|
||||
"push": "bun --bun run drizzle-kit push --config=drizzle.config.ts",
|
||||
"pull": "bun --bun run drizzle-kit pull --config=drizzle.config.ts",
|
||||
"check": "bun --bun run drizzle-kit check --config=drizzle.config.ts",
|
||||
"upgrade": "bun --bun run drizzle-kit up --config=drizzle.config.ts",
|
||||
"studio": "bun --bun run drizzle-kit studio --config=drizzle.config.ts",
|
||||
"docker:up": "docker compose up -d",
|
||||
"docker:down": "docker compose down",
|
||||
"docker:remove": "docker compose down --rmi local -v",
|
||||
"docker:reset": "bun run docker:remove && bun run docker:up"
|
||||
"docker:down": "docker compose down"
|
||||
},
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./schema": "./schema.ts",
|
||||
"./mock": "./src/mock.ts"
|
||||
"./schema": "./schema.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nimbus/vitest": "workspace:*",
|
||||
"drizzle-kit": "^0.31.4"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { mockDeep, type DeepMockProxy } from "vitest-mock-extended";
|
||||
import type { DB } from "./index";
|
||||
import { vi } from "vitest";
|
||||
|
||||
// Create a deep mock of the DB type
|
||||
export const dbMock: DeepMockProxy<DB> = mockDeep<DB>();
|
||||
|
||||
// Extract the mock functions for easy access in tests
|
||||
export const mockFindFirst = dbMock.query.user.findFirst;
|
||||
export const mockSet = vi.fn();
|
||||
export const mockWhere = vi.fn();
|
||||
|
||||
// Configure the mock's behavior to represent the chained methods
|
||||
dbMock.update.mockReturnValue({
|
||||
set: mockSet,
|
||||
} as any);
|
||||
|
||||
// Export the main update mock function as well
|
||||
export const mockUpdate = dbMock.update;
|
||||
@@ -1 +0,0 @@
|
||||
// TODO: the model for the DB requires different tests. I removed the old ones to not confuse others
|
||||
@@ -1,3 +0,0 @@
|
||||
import defineConfig from "@nimbus/vitest";
|
||||
|
||||
export default defineConfig;
|
||||
3
packages/env/package.json
vendored
3
packages/env/package.json
vendored
@@ -10,8 +10,5 @@
|
||||
"dependencies": {
|
||||
"@t3-oss/env-core": "^0.13.8",
|
||||
"zod": "^4.0.14"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nimbus/vitest": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
3
packages/env/vitest.config.ts
vendored
3
packages/env/vitest.config.ts
vendored
@@ -1,3 +0,0 @@
|
||||
import defineConfig from "@nimbus/vitest";
|
||||
|
||||
export default defineConfig;
|
||||
@@ -9,8 +9,5 @@
|
||||
"dependencies": {
|
||||
"zod": "^4.0.14",
|
||||
"@nimbus/db": "workspace:*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nimbus/vitest": "workspace:*"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,67 +0,0 @@
|
||||
import { limitedAccessAccountSchema, updateAccountSchema } from "../src";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("limitedAccessAccountSchema", () => {
|
||||
const validData = {
|
||||
id: "user_123",
|
||||
providerId: "google",
|
||||
accountId: "acc_456",
|
||||
scope: "read_write",
|
||||
nickname: "my-drive",
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
it("should validate with all required fields", () => {
|
||||
const parsed = limitedAccessAccountSchema.parse(validData);
|
||||
expect(parsed).toEqual(validData);
|
||||
});
|
||||
|
||||
it("should allow null nickname", () => {
|
||||
const data = { ...validData, nickname: null };
|
||||
const parsed = limitedAccessAccountSchema.parse(data);
|
||||
expect(parsed.nickname).toBeNull();
|
||||
});
|
||||
|
||||
it("should throw if providerId is invalid", () => {
|
||||
const data = { ...validData, providerId: "invalid" };
|
||||
expect(() => limitedAccessAccountSchema.parse(data)).toThrow();
|
||||
});
|
||||
|
||||
it("should throw if nickname is too long", () => {
|
||||
const data = { ...validData, nickname: "a".repeat(60) };
|
||||
expect(() => limitedAccessAccountSchema.parse(data)).toThrow();
|
||||
});
|
||||
|
||||
it("should throw if required fields are missing", () => {
|
||||
const data = { ...validData };
|
||||
delete (data as any).id;
|
||||
expect(() => limitedAccessAccountSchema.parse(data)).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateAccountSchema", () => {
|
||||
it("should allow valid id and nickname", () => {
|
||||
const result = updateAccountSchema.parse({
|
||||
id: "user_123",
|
||||
nickname: "updated-name",
|
||||
});
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it("should allow null nickname", () => {
|
||||
const result = updateAccountSchema.parse({
|
||||
id: "user_123",
|
||||
nickname: null,
|
||||
});
|
||||
expect(result.nickname).toBeNull();
|
||||
});
|
||||
|
||||
it("should throw if id is missing", () => {
|
||||
expect(() => updateAccountSchema.parse({ nickname: "hello" })).toThrow();
|
||||
});
|
||||
|
||||
it("should throw if nickname is too short", () => {
|
||||
expect(() => updateAccountSchema.parse({ id: "123", nickname: "" })).toThrow();
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
import { emailObjectSchema, emailSchema, sendMailSchema } from "../src";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("emailSchema", () => {
|
||||
it("should pass for a valid email", () => {
|
||||
const result = emailSchema.safeParse("john.doe@example.com");
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail for an email without TLD", () => {
|
||||
const result = emailSchema.safeParse("john.doe@example");
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("should fail for an invalid domain label", () => {
|
||||
const result = emailSchema.safeParse("john@exa_mple.com");
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("should fail for missing domain", () => {
|
||||
const result = emailSchema.safeParse("john@");
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sendMailSchema", () => {
|
||||
it("should pass with all valid fields", () => {
|
||||
const result = sendMailSchema.safeParse({
|
||||
to: "test@example.com",
|
||||
subject: "Hello",
|
||||
text: "World",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail if subject is missing", () => {
|
||||
const result = sendMailSchema.safeParse({
|
||||
to: "test@example.com",
|
||||
text: "Body",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("should fail if text is empty", () => {
|
||||
const result = sendMailSchema.safeParse({
|
||||
to: "test@example.com",
|
||||
subject: "Subject",
|
||||
text: "",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("should fail if email is invalid", () => {
|
||||
const result = sendMailSchema.safeParse({
|
||||
to: "invalid-email",
|
||||
subject: "Subject",
|
||||
text: "Text",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("emailObjectSchema", () => {
|
||||
it("should pass with valid email", () => {
|
||||
const result = emailObjectSchema.safeParse({ email: "valid@example.com" });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail with invalid email", () => {
|
||||
const result = emailObjectSchema.safeParse({ email: "invalid-email" });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,42 +0,0 @@
|
||||
import { formatFileSize, getFileExtension } from "../src";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("formatFileSize", () => {
|
||||
it("should format bytes correctly", () => {
|
||||
expect(formatFileSize(500)).toBe("500 B");
|
||||
expect(formatFileSize(1024)).toBe("1.00 KB");
|
||||
expect(formatFileSize(1536)).toBe("1.50 KB");
|
||||
expect(formatFileSize(1048576)).toBe("1.00 MB");
|
||||
expect(formatFileSize(1073741824)).toBe("1.00 GB");
|
||||
});
|
||||
|
||||
it("should handle strings as input", () => {
|
||||
expect(formatFileSize("2048")).toBe("2.00 KB");
|
||||
});
|
||||
|
||||
it("should return 'Invalid size' for invalid inputs", () => {
|
||||
expect(formatFileSize("abc")).toBe("Invalid size");
|
||||
expect(formatFileSize(undefined)).toBe("Invalid size");
|
||||
expect(formatFileSize(undefined)).toBe("Invalid size");
|
||||
expect(formatFileSize(-100)).toBe("Invalid size");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFileExtension", () => {
|
||||
it("should return correct extension", () => {
|
||||
expect(getFileExtension("document.pdf")).toBe("pdf");
|
||||
expect(getFileExtension("archive.tar.gz")).toBe("gz");
|
||||
expect(getFileExtension("image.PNG")).toBe("png");
|
||||
});
|
||||
|
||||
it("should return empty string if no extension", () => {
|
||||
expect(getFileExtension("filename")).toBe("");
|
||||
expect(getFileExtension("filename.")).toBe("");
|
||||
expect(getFileExtension("noextension.")).toBe("");
|
||||
});
|
||||
|
||||
it("should handle hidden files properly", () => {
|
||||
expect(getFileExtension(".env")).toBe("env");
|
||||
expect(getFileExtension(".config.local")).toBe("local");
|
||||
});
|
||||
});
|
||||
@@ -1,110 +0,0 @@
|
||||
import {
|
||||
createFileSchema,
|
||||
downloadFileSchema,
|
||||
fileIdSchema,
|
||||
getFilesSchema,
|
||||
updateFileSchema,
|
||||
uploadFileQuerySchema,
|
||||
} from "../src/validators/file";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("fileIdSchema", () => {
|
||||
it("should pass with valid file ID", () => {
|
||||
const result = fileIdSchema.safeParse("abc123");
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail with empty ID", () => {
|
||||
const result = fileIdSchema.safeParse("");
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getFilesSchema", () => {
|
||||
it("should return default values when input is empty", () => {
|
||||
const result = getFilesSchema.parse({});
|
||||
expect(result.parentId).toBe("root");
|
||||
expect(result.pageSize).toBe(30);
|
||||
});
|
||||
|
||||
it("should fail with too large pageSize", () => {
|
||||
const result = getFilesSchema.safeParse({ pageSize: 1000 });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateFileSchema", () => {
|
||||
it("should pass with valid name and fileId", () => {
|
||||
const result = updateFileSchema.safeParse({
|
||||
fileId: "abc123",
|
||||
name: "Valid Name",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail with empty name", () => {
|
||||
const result = updateFileSchema.safeParse({
|
||||
fileId: "abc123",
|
||||
name: "",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createFileSchema", () => {
|
||||
it("should pass with required fields", () => {
|
||||
const result = createFileSchema.safeParse({
|
||||
name: "file.txt",
|
||||
mimeType: "text/plain",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail with empty name", () => {
|
||||
const result = createFileSchema.safeParse({
|
||||
name: "",
|
||||
mimeType: "text/plain",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("uploadFileQuerySchema", () => {
|
||||
it("should pass with valid parentId", () => {
|
||||
const result = uploadFileQuerySchema.safeParse({
|
||||
parentId: "parent123",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail with empty parentId", () => {
|
||||
const result = uploadFileQuerySchema.safeParse({
|
||||
parentId: "",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("downloadFileSchema", () => {
|
||||
it("should pass with only fileId", () => {
|
||||
const result = downloadFileSchema.safeParse({
|
||||
fileId: "file123",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail with invalid fileId", () => {
|
||||
const result = downloadFileSchema.safeParse({
|
||||
fileId: "",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("should pass with acknowledgeAbuse true", () => {
|
||||
const result = downloadFileSchema.safeParse({
|
||||
fileId: "file123",
|
||||
acknowledgeAbuse: true,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,96 +0,0 @@
|
||||
import { forgotPasswordSchema, signInSchema, signUpSchema, resetPasswordSchema } from "../src/validators/password";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("forgotPasswordSchema", () => {
|
||||
it("should pass with a valid email", () => {
|
||||
const result = forgotPasswordSchema.safeParse({ email: "test@example.com" });
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail with invalid email", () => {
|
||||
const result = forgotPasswordSchema.safeParse({ email: "invalid-email" });
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("signInSchema", () => {
|
||||
it("should pass with valid data", () => {
|
||||
const result = signInSchema.safeParse({
|
||||
email: "user@example.com",
|
||||
password: "StrongPass123",
|
||||
remember: true,
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail with weak password", () => {
|
||||
const result = signInSchema.safeParse({
|
||||
email: "user@example.com",
|
||||
password: "weak",
|
||||
remember: true,
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("signUpSchema", () => {
|
||||
it("should pass with matching passwords", () => {
|
||||
const result = signUpSchema.safeParse({
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
email: "john@example.com",
|
||||
password: "StrongPass123",
|
||||
confirmPassword: "StrongPass123",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail when passwords do not match", () => {
|
||||
const result = signUpSchema.safeParse({
|
||||
firstName: "John",
|
||||
lastName: "Doe",
|
||||
email: "john@example.com",
|
||||
password: "StrongPass123",
|
||||
confirmPassword: "DifferentPass123",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.format().confirmPassword?._errors).toContain("Passwords don't match");
|
||||
});
|
||||
|
||||
it("should fail if firstName is missing", () => {
|
||||
const result = signUpSchema.safeParse({
|
||||
lastName: "Doe",
|
||||
email: "john@example.com",
|
||||
password: "StrongPass123",
|
||||
confirmPassword: "StrongPass123",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("resetPasswordSchema", () => {
|
||||
it("should pass when passwords match and are strong", () => {
|
||||
const result = resetPasswordSchema.safeParse({
|
||||
password: "StrongPass123",
|
||||
confirmPassword: "StrongPass123",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail when passwords do not match", () => {
|
||||
const result = resetPasswordSchema.safeParse({
|
||||
password: "StrongPass123",
|
||||
confirmPassword: "Mismatch123",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error?.format().confirmPassword?._errors).toContain("Passwords don't match");
|
||||
});
|
||||
|
||||
it("should fail when password is weak", () => {
|
||||
const result = resetPasswordSchema.safeParse({
|
||||
password: "weakpass",
|
||||
confirmPassword: "weakpass",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,112 +0,0 @@
|
||||
import {
|
||||
driveProviderParamSchema,
|
||||
driveProviderSchema,
|
||||
driveProviderSlugParamSchema,
|
||||
driveProviderSlugSchema,
|
||||
providerSchema,
|
||||
providerToSlug,
|
||||
slugToProvider,
|
||||
} from "../src/validators/provider";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("driveProviderSchema", () => {
|
||||
it("should accept 'google'", () => {
|
||||
const result = driveProviderSchema.safeParse("google");
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should reject 'icloud'", () => {
|
||||
const result = driveProviderSchema.safeParse("icloud");
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("driveProviderSlugSchema", () => {
|
||||
it("should accept 'g'", () => {
|
||||
const result = driveProviderSlugSchema.safeParse("g");
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should reject 'z'", () => {
|
||||
const result = driveProviderSlugSchema.safeParse("z");
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("driveProviderParamSchema", () => {
|
||||
it("should pass with valid values", () => {
|
||||
const result = driveProviderParamSchema.safeParse({
|
||||
providerId: "microsoft",
|
||||
accountId: "123",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail with invalid provider", () => {
|
||||
const result = driveProviderParamSchema.safeParse({
|
||||
providerId: "apple",
|
||||
accountId: "123",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("driveProviderSlugParamSchema", () => {
|
||||
it("should pass with valid values", () => {
|
||||
const result = driveProviderSlugParamSchema.safeParse({
|
||||
providerSlug: "g",
|
||||
accountId: "abc",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail with invalid slug", () => {
|
||||
const result = driveProviderSlugParamSchema.safeParse({
|
||||
providerSlug: "z",
|
||||
accountId: "abc",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("providerSchema", () => {
|
||||
it("should accept 'google'", () => {
|
||||
const result = providerSchema.safeParse("google");
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should accept 'credential'", () => {
|
||||
const result = providerSchema.safeParse("credential");
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should accept null", () => {
|
||||
const result = providerSchema.safeParse(null);
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should reject 'icloud'", () => {
|
||||
const result = providerSchema.safeParse("icloud");
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("slugToProvider", () => {
|
||||
it("should return 'google' for 'g'", () => {
|
||||
expect(slugToProvider("g")).toBe("google");
|
||||
});
|
||||
|
||||
it("should return undefined for 'z'", () => {
|
||||
expect(slugToProvider("z" as any)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("providerToSlug", () => {
|
||||
it("should return 'm' for 'microsoft'", () => {
|
||||
expect(providerToSlug("microsoft")).toBe("m");
|
||||
});
|
||||
|
||||
it("should return undefined for 'icloud'", () => {
|
||||
expect(providerToSlug("icloud" as any)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -1,127 +0,0 @@
|
||||
import {
|
||||
addTagsToFileSchema,
|
||||
createTagSchema,
|
||||
hexColorSchema,
|
||||
removeTagsFromFileSchema,
|
||||
tagIdSchema,
|
||||
tagNameSchema,
|
||||
updateTagSchema,
|
||||
} from "../src";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("tagNameSchema", () => {
|
||||
it("should pass for valid string", () => {
|
||||
const result = tagNameSchema.safeParse("Important");
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail for empty string", () => {
|
||||
const result = tagNameSchema.safeParse("");
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("hexColorSchema", () => {
|
||||
it("should pass for valid hex", () => {
|
||||
const result = hexColorSchema.safeParse("#FF5733");
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail for invalid hex", () => {
|
||||
const result = hexColorSchema.safeParse("FF5733");
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("tagIdSchema", () => {
|
||||
it("should pass with valid ID", () => {
|
||||
const result = tagIdSchema.safeParse("abc123");
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail with empty ID", () => {
|
||||
const result = tagIdSchema.safeParse("");
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createTagSchema", () => {
|
||||
it("should pass with name and optional parentId", () => {
|
||||
const result = createTagSchema.safeParse({
|
||||
name: "Work",
|
||||
color: "#123456",
|
||||
parentId: "parent-id",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should use default color when not provided", () => {
|
||||
const result = createTagSchema.safeParse({
|
||||
name: "Work",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.data?.color).toBe("#808080");
|
||||
});
|
||||
|
||||
it("should fail with invalid color", () => {
|
||||
const result = createTagSchema.safeParse({
|
||||
name: "Work",
|
||||
color: "red",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateTagSchema", () => {
|
||||
it("should pass with partial fields", () => {
|
||||
const result = updateTagSchema.safeParse({
|
||||
id: "tag123",
|
||||
name: "New Name",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail with invalid color", () => {
|
||||
const result = updateTagSchema.safeParse({
|
||||
id: "tag123",
|
||||
color: "red",
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("addTagsToFileSchema", () => {
|
||||
it("should pass with valid fileId and tagIds", () => {
|
||||
const result = addTagsToFileSchema.safeParse({
|
||||
fileId: "file-123",
|
||||
tagIds: ["tag-1", "tag-2"],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail with empty tagIds array", () => {
|
||||
const result = addTagsToFileSchema.safeParse({
|
||||
fileId: "file-123",
|
||||
tagIds: [],
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("should fail with empty fileId", () => {
|
||||
const result = addTagsToFileSchema.safeParse({
|
||||
fileId: "",
|
||||
tagIds: ["tag-1"],
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("removeTagsFromFileSchema", () => {
|
||||
it("should pass with valid data", () => {
|
||||
const result = removeTagsFromFileSchema.safeParse({
|
||||
fileId: "file-123",
|
||||
tagIds: ["tag-1", "tag-2"],
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -1,63 +0,0 @@
|
||||
import { userSchema, updateUserSchema } from "../src/validators/user";
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
describe("userSchema", () => {
|
||||
it("should pass with valid data", () => {
|
||||
const result = userSchema.safeParse({
|
||||
id: "user-1",
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
emailVerified: true,
|
||||
image: null,
|
||||
defaultAccountId: null,
|
||||
defaultProviderId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail if createdAt is missing", () => {
|
||||
const result = userSchema.safeParse({
|
||||
id: "user-1",
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
emailVerified: true,
|
||||
image: null,
|
||||
defaultAccountId: null,
|
||||
defaultProviderId: null,
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
|
||||
it("should fail if emailVerified is not boolean", () => {
|
||||
const result = userSchema.safeParse({
|
||||
id: "user-1",
|
||||
name: "John Doe",
|
||||
email: "john@example.com",
|
||||
emailVerified: "yes", // ❌ should be boolean
|
||||
image: null,
|
||||
defaultAccountId: null,
|
||||
defaultProviderId: null,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("updateUserSchema", () => {
|
||||
it("should pass with valid IDs", () => {
|
||||
const result = updateUserSchema.safeParse({
|
||||
defaultAccountId: "acc-123",
|
||||
defaultProviderId: "g",
|
||||
});
|
||||
expect(result.success).toBe(true);
|
||||
});
|
||||
|
||||
it("should fail with missing fields", () => {
|
||||
const result = updateUserSchema.safeParse({});
|
||||
expect(result.success).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import defineConfig from "@nimbus/vitest";
|
||||
|
||||
export default defineConfig;
|
||||
@@ -1,13 +0,0 @@
|
||||
{
|
||||
"name": "@nimbus/vitest",
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitest/coverage-v8": "^3.2.4",
|
||||
"vitest": "^3.2.4"
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import tsconfigPaths from "vite-tsconfig-paths";
|
||||
import { defineConfig } from "vitest/config";
|
||||
import { config } from "dotenv";
|
||||
|
||||
// Load environment variables from .env file
|
||||
config();
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [tsconfigPaths()],
|
||||
test: {
|
||||
globals: true,
|
||||
sequence: {
|
||||
concurrent: true,
|
||||
},
|
||||
include: [
|
||||
"**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}",
|
||||
"**/tests/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}",
|
||||
],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
reporter: ["html", "json-summary", "lcov"],
|
||||
// Only include source files from current workspace
|
||||
include: ["**/src/**/*"],
|
||||
exclude: [
|
||||
"**/node_modules/**",
|
||||
"**/tests/**",
|
||||
"**/*.test.ts",
|
||||
"**/*.spec.ts",
|
||||
"**/dist/**",
|
||||
"**/build/**",
|
||||
"**/coverage/**",
|
||||
"**/*.d.ts",
|
||||
"**/*.config.*",
|
||||
"**/*.setup.*",
|
||||
"**/index.ts", // Often just re-exports
|
||||
],
|
||||
// TODO(tests): enable when we have better test coverage
|
||||
thresholds: {
|
||||
// statements: 80,
|
||||
// branches: 80,
|
||||
// functions: 80,
|
||||
// lines: 80,
|
||||
},
|
||||
reportsDirectory: "./coverage",
|
||||
// Clean coverage directory before each run
|
||||
clean: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,3 +0,0 @@
|
||||
import defineConfig from "@nimbus/vitest";
|
||||
|
||||
export default defineConfig;
|
||||
Reference in New Issue
Block a user