TANNNERRRRRRRROUTTERRRRRR

This commit is contained in:
logscore
2025-10-13 21:06:27 -06:00
parent 67c1362e77
commit f52f3fcec4
88 changed files with 1290 additions and 3462 deletions

1
.rules/GENERAL.md Normal file
View File

@@ -0,0 +1 @@
- Only do the minimum required changes to accomplish the stated task.

View File

@@ -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.

View File

@@ -46,7 +46,6 @@
},
"devDependencies": {
"@microsoft/microsoft-graph-types": "^2.40.0",
"@nimbus/vitest": "workspace:*",
"@types/pg": "^8.15.4"
}
}

View File

@@ -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");
});
});
});

View File

@@ -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();
});
});
});

View File

@@ -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",
},
},
};

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
});
});
});

View File

@@ -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);
}

View File

@@ -1,3 +0,0 @@
import defineConfig from "@nimbus/vitest";
export default defineConfig;

View File

@@ -1,3 +0,0 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}

19
apps/web/index.html Normal file
View 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>

View File

@@ -1,9 +0,0 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
reactStrictMode: true,
output: "standalone",
transpilePackages: ["@t3-oss/env-core"],
};
export default nextConfig;

View File

@@ -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"
}
}

View File

@@ -1,5 +1,7 @@
import tailwindcss from "@tailwindcss/postcss";
const config = {
plugins: ["@tailwindcss/postcss"],
plugins: [tailwindcss],
};
export default config;

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,7 @@
User-agent: *
Allow: /
Disallow: /dashboard/
Disallow: /api/auth/
Disallow: /reset-password
Sitemap: https://nimbus.example.com/sitemap.xml

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -1,5 +0,0 @@
import { ProtectedRoute } from "@/components/providers/protected-route";
export default function ProtectedLayout({ children }: { children: React.ReactNode }) {
return <ProtectedRoute>{children}</ProtectedRoute>;
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>;
}

View File

@@ -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>
);
}

View File

@@ -1,7 +0,0 @@
"use client";
import Hero from "@/components/home/hero";
export default function Home() {
return <Hero />;
}

View File

@@ -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`,
};
}

View File

@@ -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,
},
];
}

View File

@@ -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
/>
</>
);

View File

@@ -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>

View File

@@ -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 });
}
};

View File

@@ -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
View 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>
);
}

View 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>();

View 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>
);
}

View 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 />;
}

View 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>
);
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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();

View 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 />;
}

View File

@@ -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">

View 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>
);
}

View File

@@ -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">

View 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 />;
}

View File

@@ -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 />

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View 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>
);
}

View 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>
);
}

View File

@@ -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
View File

@@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@@ -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
View 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"],
},
});

View File

@@ -1,3 +0,0 @@
import defineConfig from "@nimbus/vitest";
export default defineConfig;

872
bun.lock

File diff suppressed because it is too large Load Diff

View File

@@ -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

View File

@@ -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:

View File

@@ -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,
},
},
};

View File

@@ -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"
}
}

View File

@@ -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:*",

View File

@@ -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();
});
});

View File

@@ -1,3 +0,0 @@
import defineConfig from "@nimbus/vitest";
export default defineConfig;

View File

@@ -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:*"
}
}

View File

@@ -1 +0,0 @@
// TODO: the model for the KV requires different tests. I removed the old ones to not confuse others

View File

@@ -1,3 +0,0 @@
import defineConfig from "@nimbus/vitest";
export default defineConfig;

View File

@@ -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": {

View File

@@ -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;

View File

@@ -1 +0,0 @@
// TODO: the model for the DB requires different tests. I removed the old ones to not confuse others

View File

@@ -1,3 +0,0 @@
import defineConfig from "@nimbus/vitest";
export default defineConfig;

View File

@@ -10,8 +10,5 @@
"dependencies": {
"@t3-oss/env-core": "^0.13.8",
"zod": "^4.0.14"
},
"devDependencies": {
"@nimbus/vitest": "workspace:*"
}
}

View File

@@ -1,3 +0,0 @@
import defineConfig from "@nimbus/vitest";
export default defineConfig;

View File

@@ -9,8 +9,5 @@
"dependencies": {
"zod": "^4.0.14",
"@nimbus/db": "workspace:*"
},
"devDependencies": {
"@nimbus/vitest": "workspace:*"
}
}

View File

@@ -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();
});
});

View File

@@ -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);
});
});

View File

@@ -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");
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -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();
});
});

View File

@@ -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);
});
});

View File

@@ -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);
});
});

View File

@@ -1,3 +0,0 @@
import defineConfig from "@nimbus/vitest";
export default defineConfig;

View File

@@ -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"
}
}

View File

@@ -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,
},
},
});

View File

@@ -1,3 +0,0 @@
import defineConfig from "@nimbus/vitest";
export default defineConfig;