diff --git a/.dockerignore b/.dockerignore
index 55d99cf..a6e6cde 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,5 +1,4 @@
.git
.gitignore
-Dockerfile
node_modules/
**/__tests__
\ No newline at end of file
diff --git a/.github/workflows/publish-ghcr-stack.yml b/.github/workflows/publish-ghcr-stack.yml
new file mode 100644
index 0000000..cd440c8
--- /dev/null
+++ b/.github/workflows/publish-ghcr-stack.yml
@@ -0,0 +1,97 @@
+name: Create and publish Docker Compose stack to ghcr
+
+on:
+ workflow_run:
+ workflows: ["Run Tests"]
+ types:
+ - completed
+
+env:
+ REGISTRY: ghcr.io
+ BACKEND_IMAGE_NAME: ${{ github.repository }}-backend
+ FRONTEND_IMAGE_NAME: ${{ github.repository }}-frontend
+
+jobs:
+ build-and-push-images:
+ runs-on: ubuntu-latest
+ permissions:
+ contents: read
+ packages: write
+ attestations: write
+ id-token: write
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Get latest tag and commit distance
+ id: get_version_info
+ run: |
+ # Get the latest tag
+ LATEST_TAG=$(git describe --tags --abbrev=0)
+
+ # Remove 'v' prefix from latest tag
+ BASE_VERSION=${LATEST_TAG#v}
+
+ # Get the number of commits since the last tag
+ COMMIT_DISTANCE=$(git rev-list --count ${LATEST_TAG}..HEAD)
+
+ # Construct new version with build number
+ NEW_VERSION="${BASE_VERSION}.${COMMIT_DISTANCE}"
+
+ echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT
+ echo "Generated version: ${NEW_VERSION}"
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ # Build and push backend image
+ - name: Extract metadata for Backend
+ id: meta-backend
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.BACKEND_IMAGE_NAME }}
+ tags: |
+ type=raw,value=${{ steps.get_version_info.outputs.version }}
+ type=raw,value=latest
+
+ - name: Build and push Backend image
+ id: push-backend
+ uses: docker/build-push-action@v5
+ with:
+ context: ./backend/api
+ file: ./backend/api/Dockerfile
+ push: true
+ tags: ${{ steps.meta-backend.outputs.tags }}
+ labels: ${{ steps.meta-backend.outputs.labels }}
+ build-args: |
+ DATABASE_URL=postgresql://admin:saisab@postgres:5432/rust_sqlx?schema=public
+
+ # Build and push frontend image
+ - name: Extract metadata for Frontend
+ id: meta-frontend
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.FRONTEND_IMAGE_NAME }}
+ tags: |
+ type=raw,value=${{ steps.get_version_info.outputs.version }}
+ type=raw,value=latest
+
+ - name: Build and push Frontend image
+ id: push-frontend
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ file: ./Dockerfile
+ push: true
+ tags: ${{ steps.meta-frontend.outputs.tags }}
+ labels: ${{ steps.meta-frontend.outputs.labels }}
+ build-args: |
+ API_URL=http://backend:8000
+ NODE_ENV=production
diff --git a/.github/workflows/publish-ghcr.yml b/.github/workflows/publish-ghcr.yml
deleted file mode 100644
index 60855af..0000000
--- a/.github/workflows/publish-ghcr.yml
+++ /dev/null
@@ -1,75 +0,0 @@
-name: Create and publish a Docker image to ghcr
-
-on:
- workflow_run:
- workflows: ["Run Tests"]
- types:
- - completed
-
-env:
- REGISTRY: ghcr.io
- IMAGE_NAME: ${{ github.repository }}
-
-jobs:
- build-and-push-image:
- runs-on: ubuntu-latest
- permissions:
- contents: read
- packages: write
- attestations: write
- id-token: write
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
-
- - name: Get latest tag and commit distance
- id: get_version_info
- run: |
- # Get the latest tag
- LATEST_TAG=$(git describe --tags --abbrev=0)
-
- # Remove 'v' prefix from latest tag
- BASE_VERSION=${LATEST_TAG#v}
-
- # Get the number of commits since the last tag
- COMMIT_DISTANCE=$(git rev-list --count ${LATEST_TAG}..HEAD)
-
- # Construct new version with build number
- NEW_VERSION="v${BASE_VERSION}.${COMMIT_DISTANCE}"
-
- echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT
- echo "Generated version: ${NEW_VERSION}"
-
- - name: Log in to the Container registry
- uses: docker/login-action@v3
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Extract metadata (tags, labels) for Docker
- id: meta
- uses: docker/metadata-action@v5
- with:
- images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
- tags: |
- type=raw,value=${{ steps.get_version_info.outputs.version }}
-
- - name: Build and push Docker image
- id: push
- uses: docker/build-push-action@v5
- with:
- context: .
- push: true
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
-
- - name: Generate artifact attestation
- uses: actions/attest-build-provenance@v1
- with:
- subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
- subject-digest: ${{ steps.push.outputs.digest }}
- push-to-registry: true
diff --git a/.gitignore b/.gitignore
index d32cc78..3f53430 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,3 +38,5 @@ yarn-error.log*
# typescript
*.tsbuildinfo
next-env.d.ts
+
+/target
\ No newline at end of file
diff --git a/__tests__/Home.test.tsx b/__tests__/Home.test.tsx
index c37ca19..3f0c080 100644
--- a/__tests__/Home.test.tsx
+++ b/__tests__/Home.test.tsx
@@ -1,143 +1,46 @@
-import { render, screen, fireEvent } from "@testing-library/react";
-import Home from "../src/app/page";
+// __tests__/Home.test.tsx
+import { render, screen } from "@testing-library/react";
+import Home from "@/app/page";
import "@testing-library/jest-dom";
-// Mock data (assuming 10 projects for pagination test)
-jest.mock("../src/data.json", () => [
+// Mock fetch
+global.fetch = jest.fn();
+
+const mockProjects = [
{
id: "1",
- projectName: "Project 1",
- imageUrl: "/path/to/image1.jpg",
- subImageUrl: "/path/to/subimage1.jpg",
+ projectname: "Project 1",
+ projecturl: "#",
+ imageurl: "/path/to/image1.jpg",
+ subimageurl: "/path/to/subimage1.jpg",
description: "Description 1",
- url: "#",
- },
- {
- id: "2",
- projectName: "Project 2",
- imageUrl: "/path/to/image2.jpg",
- subImageUrl: "/path/to/subimage2.jpg",
- description: "Description 2",
- url: "#",
- },
- {
- id: "3",
- projectName: "Project 3",
- imageUrl: "/path/to/image3.jpg",
- subImageUrl: "/path/to/subimage3.jpg",
- description: "Description 3",
- url: "#",
- },
- {
- id: "4",
- projectName: "Project 4",
- imageUrl: "/path/to/image4.jpg",
- subImageUrl: "/path/to/subimage4.jpg",
- description: "Description 4",
- url: "#",
- },
- {
- id: "5",
- projectName: "Project 5",
- imageUrl: "/path/to/image5.jpg",
- subImageUrl: "/path/to/subimage5.jpg",
- description: "Description 5",
- url: "#",
+ about: "About 1",
+ createdAt: "2024-01-01",
+ published: true,
+ updatedAt: "2024-01-01",
},
- {
- id: "6",
- projectName: "Project 6",
- imageUrl: "/path/to/image6.jpg",
- subImageUrl: "/path/to/subimage6.jpg",
- description: "Description 6",
- url: "#",
- },
- {
- id: "7",
- projectName: "Project 7",
- imageUrl: "/path/to/image7.jpg",
- subImageUrl: "/path/to/subimage7.jpg",
- description: "Description 7",
- url: "#",
- },
- {
- id: "8",
- projectName: "Project 8",
- imageUrl: "/path/to/image8.jpg",
- subImageUrl: "/path/to/subimage8.jpg",
- description: "Description 8",
- url: "#",
- },
- {
- id: "9",
- projectName: "Project 9",
- imageUrl: "/path/to/image9.jpg",
- subImageUrl: "/path/to/subimage9.jpg",
- description: "Description 9",
- url: "#",
- },
-]);
+];
describe("Home Page", () => {
- test("renders the correct number of cards for the current page", () => {
- render();
-
- // Initially, it should show the first 8 cards (cards per page = 8)
- const cards = screen.getAllByRole("link");
- expect(cards).toHaveLength(8);
-
- // Check the first card's text
- expect(screen.getByText("Project 1")).toBeInTheDocument();
- });
-
- test("renders 2nd page correctly when Next is clicked", () => {
- render();
-
- // Click Next to go to the second page
- const nextButton = screen.getByText("Next");
- fireEvent.click(nextButton);
-
- // It should now show cards 9 and 10
- const cards = screen.getAllByRole("link");
- expect(cards).toHaveLength(1); // Since there are only 9 cards, the second page will have only 1 card.
-
- expect(screen.getByText("Project 9")).toBeInTheDocument();
- });
-
- test("disables the Previous button on the first page", () => {
- render();
-
- // The Previous button should be disabled on the first page
- const prevButton = screen.getByText("Previous");
- expect(prevButton).toBeDisabled();
+ beforeEach(() => {
+ (global.fetch as jest.Mock).mockResolvedValue({
+ ok: true,
+ json: async () => ({
+ projects: mockProjects,
+ results: mockProjects.length,
+ status: "success",
+ }),
+ });
});
- test("disables the Next button on the last page", () => {
- render();
-
- let nextButton = screen.getByText("Next") as HTMLButtonElement;
-
- // Loop until the Next button becomes disabled
- while (!nextButton.disabled) {
- fireEvent.click(nextButton);
- nextButton = screen.getByText("Next") as HTMLButtonElement;
- }
-
- // Once on the last page, the Next button should be disabled
- expect(nextButton).toBeDisabled();
+ it("renders the page", async () => {
+ render(await Home());
+ expect(screen.getByText("Project 1")).toBeInTheDocument();
});
- test("navigates correctly when page numbers are clicked", () => {
- render();
-
- // Check if the second page button is clickable
- const page2Button = screen.getByText("2");
- fireEvent.click(page2Button);
-
- // After clicking the page 2 button, it should show the next set of cards
- const cards = screen.getAllByRole("link");
- expect(cards).toHaveLength(1); // Since there are only 9 cards, the second page will have 1 card.
-
- expect(screen.getByText("Project 9")).toBeInTheDocument();
- });
+ // it("handles API error gracefully", async () => {
+ // (global.fetch as jest.Mock).mockRejectedValueOnce(new Error("API Error"));
+ // render(await Home());
+ // expect(screen.getByText("No projects available.")).toBeInTheDocument();
+ // });
});
diff --git a/__tests__/Project.test.tsx b/__tests__/Project.test.tsx
index 6d37c83..22ab8aa 100644
--- a/__tests__/Project.test.tsx
+++ b/__tests__/Project.test.tsx
@@ -1,11 +1,11 @@
-/* eslint-disable testing-library/no-node-access */
+/* eslint-disable jsx-a11y/alt-text */
/* eslint-disable @next/next/no-img-element */
/* eslint-disable @typescript-eslint/no-explicit-any */
-/* eslint-disable jsx-a11y/alt-text */
import { render, screen } from "@testing-library/react";
-import Project from "@/app/projects/[id]/page";
+import ProjectPage from "@/app/projects/[id]/page";
import "@testing-library/jest-dom";
+// Mock next/image
jest.mock("next/image", () => ({
__esModule: true,
default: (props: any) => {
@@ -13,81 +13,81 @@ jest.mock("next/image", () => ({
},
}));
-jest.mock("../src/data.json", () => [
- {
- id: "1",
- projectName: "Project 1",
- subImageUrl: "/path/to/image1.jpg",
- description: "Description 1",
- url: "http://example.com",
- },
- {
- id: "2",
- projectName: "Project 2",
- subImageUrl: "/path/to/image2.jpg",
- description: "Description 2",
- url: "http://example2.com",
- },
-]);
+// Mock next/link
+jest.mock("next/link", () => ({
+ __esModule: true,
+ default: ({
+ children,
+ href,
+ }: {
+ children: React.ReactNode;
+ href: string;
+ }) => {children},
+}));
+// Mock next/navigation
jest.mock("next/navigation", () => ({
notFound: jest.fn(),
}));
+
+// Mock SimilarProjects component
jest.mock("../src/app/Component/SimilarProjects", () => {
return function MockSimilarProjects() {
return
Similar Projects
;
};
});
-describe("Project Page", () => {
- it("renders the correct project based on the id from the URL", async () => {
- const mockParams = Promise.resolve({ id: "1" });
- render(await Project({ params: mockParams }));
+// Mock fetch function
+global.fetch = jest.fn();
- const projectName = await screen.findByText("Project 1");
- const projectDescription = await screen.findByText("Description 1");
- expect(projectName).toBeInTheDocument();
- expect(projectDescription).toBeInTheDocument();
+// Mock project data
+const mockProject = {
+ id: "1",
+ projectname: "Test Project",
+ projecturl: "https://test.com",
+ imageurl: "/test-image.jpg",
+ subimageurl: "/test-sub-image.jpg",
+ description: "Test Description",
+ about: "Test About Section",
+ createdAt: "2024-01-01",
+ published: true,
+ updatedAt: "2024-01-01",
+};
- const image = await screen.findByAltText("Project Thumbnail");
- expect(image).toHaveAttribute("src", "/path/to/image1.jpg");
- });
+const mockApiResponse = {
+ projects: [mockProject],
+ results: 1,
+ status: "success",
+};
- it("renders the similar projects section", async () => {
- const mockParams = Promise.resolve({ id: "1" });
- render(await Project({ params: mockParams }));
-
- const similarProjectsSection = await screen.findByText("You may also like");
- expect(similarProjectsSection).toBeInTheDocument();
-
- const similarProjectsComponent = await screen.findByTestId(
- "similar-projects"
- );
- expect(similarProjectsComponent).toBeInTheDocument();
+describe("ProjectPage", () => {
+ beforeEach(() => {
+ jest.clearAllMocks();
+ (global.fetch as jest.Mock).mockResolvedValue({
+ ok: true,
+ json: async () => mockApiResponse,
+ });
});
- it("renders the Visit button and links correctly", async () => {
- const mockParams = Promise.resolve({ id: "1" });
- render(await Project({ params: mockParams }));
+ it("renders project details correctly", async () => {
+ const params = Promise.resolve({ id: "1" });
+ render(await ProjectPage({ params }));
- const visitButtons = await screen.findAllByText("Visit");
- expect(visitButtons.length).toBeGreaterThan(0);
+ // Check if main project details are rendered
+ expect(screen.getByText("Test Project")).toBeInTheDocument();
+ expect(screen.getByText("Test Description")).toBeInTheDocument();
+ expect(screen.getByText("Test About Section")).toBeInTheDocument();
- visitButtons.forEach((button) => {
- const link = button.closest("a");
- expect(link).toHaveAttribute("href", "http://example.com");
- expect(link).toHaveAttribute("target", "_blank");
- });
- });
-
- it("displays the project screenshots", async () => {
- const mockParams = Promise.resolve({ id: "1" });
- render(await Project({ params: mockParams }));
+ // Check if navigation elements are present
+ expect(screen.getByText("Projects")).toBeInTheDocument();
+ expect(screen.getAllByText("Visit").length).toBeGreaterThan(0); // Ensures buttons exist
- const screenshotImages = await screen.findAllByAltText("Screenshot");
- expect(screenshotImages).toHaveLength(2);
- screenshotImages.forEach((img) => {
- expect(img).toHaveAttribute("src", "/images/screenshot1.png");
+ // Optionally, verify each button individually
+ screen.getAllByText("Visit").forEach((button) => {
+ expect(button).toBeInTheDocument();
});
+
+ // Check if similar projects section is rendered
+ expect(screen.getByTestId("similar-projects")).toBeInTheDocument();
});
});
diff --git a/__tests__/SimilarProjects.test.tsx b/__tests__/SimilarProjects.test.tsx
deleted file mode 100644
index 6545b98..0000000
--- a/__tests__/SimilarProjects.test.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import { render, screen } from "@testing-library/react";
-import SimilarProjects from "@/app/Component/SimilarProjects";
-import "@testing-library/jest-dom";
-
-// Mock the data import
-jest.mock("../src/data.json", () => [
- { id: "1", projectName: "Project 1", imageUrl: "/path/to/image1.jpg" },
- { id: "2", projectName: "Project 2", imageUrl: "/path/to/image2.jpg" },
- { id: "3", projectName: "Project 3", imageUrl: "/path/to/image3.jpg" },
- { id: "4", projectName: "Project 4", imageUrl: "/path/to/image4.jpg" },
- { id: "5", projectName: "Project 5", imageUrl: "/path/to/image5.jpg" },
-]);
-
-describe("SimilarProjects Component", () => {
- test("renders similar projects excluding the current project", () => {
- render();
-
- // Check if the correct number of projects (excluding the current project) are displayed
- const projectLinks = screen.getAllByRole("link");
- expect(projectLinks).toHaveLength(4);
-
- // Check if the correct projects are rendered
- expect(screen.getByText("Project 2")).toBeInTheDocument();
- expect(screen.getByText("Project 3")).toBeInTheDocument();
- expect(screen.getByText("Project 4")).toBeInTheDocument();
- expect(screen.getByText("Project 5")).toBeInTheDocument();
-
- // Ensure the current project (Project 1) is not displayed
- expect(screen.queryByText("Project 1")).not.toBeInTheDocument();
- });
-
- test("renders image for each project", () => {
- render();
-
- // Check if images for projects are rendered correctly
- const images = screen.getAllByRole("img");
- expect(images).toHaveLength(4);
-
- // Verify image alt text matches project names
- expect(images[0]).toHaveAttribute("alt", "Project 2");
- expect(images[1]).toHaveAttribute("alt", "Project 3");
- expect(images[2]).toHaveAttribute("alt", "Project 4");
- expect(images[3]).toHaveAttribute("alt", "Project 5");
- });
-
- test("renders the correct links for each project", () => {
- render();
-
- // Check if each link has the correct href for the project id
- const projectLinks = screen.getAllByRole("link");
- expect(projectLinks[0]).toHaveAttribute("href", "/projects/2");
- expect(projectLinks[1]).toHaveAttribute("href", "/projects/3");
- expect(projectLinks[2]).toHaveAttribute("href", "/projects/4");
- expect(projectLinks[3]).toHaveAttribute("href", "/projects/5");
- });
-});
diff --git a/backend/api/.gitignore b/backend/api/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/backend/api/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/backend/api/.sqlx/query-29356ceade5be500173e8655241a35973a4333a3d19ec0b9e3dfabbd0bc97b8c.json b/backend/api/.sqlx/query-29356ceade5be500173e8655241a35973a4333a3d19ec0b9e3dfabbd0bc97b8c.json
new file mode 100644
index 0000000..c7d0d41
--- /dev/null
+++ b/backend/api/.sqlx/query-29356ceade5be500173e8655241a35973a4333a3d19ec0b9e3dfabbd0bc97b8c.json
@@ -0,0 +1,76 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "SELECT * FROM projects WHERE id = $1",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Uuid"
+ },
+ {
+ "ordinal": 1,
+ "name": "projectname",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 2,
+ "name": "projecturl",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 3,
+ "name": "imageurl",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 4,
+ "name": "subimageurl",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 5,
+ "name": "description",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 6,
+ "name": "about",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 7,
+ "name": "created_at",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 8,
+ "name": "published",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 9,
+ "name": "updated_at",
+ "type_info": "Timestamptz"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Uuid"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true
+ ]
+ },
+ "hash": "29356ceade5be500173e8655241a35973a4333a3d19ec0b9e3dfabbd0bc97b8c"
+}
diff --git a/backend/api/.sqlx/query-a5ba908419fb3e456bdd2daca41ba06cc3212ffffb8520fc7dbbcc8b60ada314.json b/backend/api/.sqlx/query-a5ba908419fb3e456bdd2daca41ba06cc3212ffffb8520fc7dbbcc8b60ada314.json
new file mode 100644
index 0000000..fd6a664
--- /dev/null
+++ b/backend/api/.sqlx/query-a5ba908419fb3e456bdd2daca41ba06cc3212ffffb8520fc7dbbcc8b60ada314.json
@@ -0,0 +1,14 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "DELETE FROM projects WHERE id = $1",
+ "describe": {
+ "columns": [],
+ "parameters": {
+ "Left": [
+ "Uuid"
+ ]
+ },
+ "nullable": []
+ },
+ "hash": "a5ba908419fb3e456bdd2daca41ba06cc3212ffffb8520fc7dbbcc8b60ada314"
+}
diff --git a/backend/api/.sqlx/query-bffbd10cfdc8dd1c8f27b6ade5d240e88a8f7b98ddbe0ce47d21503a25092248.json b/backend/api/.sqlx/query-bffbd10cfdc8dd1c8f27b6ade5d240e88a8f7b98ddbe0ce47d21503a25092248.json
new file mode 100644
index 0000000..4ed8077
--- /dev/null
+++ b/backend/api/.sqlx/query-bffbd10cfdc8dd1c8f27b6ade5d240e88a8f7b98ddbe0ce47d21503a25092248.json
@@ -0,0 +1,82 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n INSERT INTO projects (\n projectname, \n projecturl, \n imageurl, \n subimageurl, \n description, \n about,\n published\n ) \n VALUES ($1, $2, $3, $4, $5, $6, $7) \n RETURNING *\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Uuid"
+ },
+ {
+ "ordinal": 1,
+ "name": "projectname",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 2,
+ "name": "projecturl",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 3,
+ "name": "imageurl",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 4,
+ "name": "subimageurl",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 5,
+ "name": "description",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 6,
+ "name": "about",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 7,
+ "name": "created_at",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 8,
+ "name": "published",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 9,
+ "name": "updated_at",
+ "type_info": "Timestamptz"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Varchar",
+ "Text",
+ "Varchar",
+ "Varchar",
+ "Varchar",
+ "Varchar",
+ "Bool"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true
+ ]
+ },
+ "hash": "bffbd10cfdc8dd1c8f27b6ade5d240e88a8f7b98ddbe0ce47d21503a25092248"
+}
diff --git a/backend/api/.sqlx/query-d168469a496ffe016d418b774b7b522532d17e64e54b07ddb61fbe1d83073368.json b/backend/api/.sqlx/query-d168469a496ffe016d418b774b7b522532d17e64e54b07ddb61fbe1d83073368.json
new file mode 100644
index 0000000..734646e
--- /dev/null
+++ b/backend/api/.sqlx/query-d168469a496ffe016d418b774b7b522532d17e64e54b07ddb61fbe1d83073368.json
@@ -0,0 +1,84 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n UPDATE projects \n SET projectname = $1,\n projecturl = $2,\n imageurl = $3,\n subimageurl = $4,\n description = $5,\n about = $6,\n published = $7,\n updated_at = $8 \n WHERE id = $9\n RETURNING *\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Uuid"
+ },
+ {
+ "ordinal": 1,
+ "name": "projectname",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 2,
+ "name": "projecturl",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 3,
+ "name": "imageurl",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 4,
+ "name": "subimageurl",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 5,
+ "name": "description",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 6,
+ "name": "about",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 7,
+ "name": "created_at",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 8,
+ "name": "published",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 9,
+ "name": "updated_at",
+ "type_info": "Timestamptz"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Varchar",
+ "Text",
+ "Varchar",
+ "Varchar",
+ "Varchar",
+ "Varchar",
+ "Bool",
+ "Timestamptz",
+ "Uuid"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true
+ ]
+ },
+ "hash": "d168469a496ffe016d418b774b7b522532d17e64e54b07ddb61fbe1d83073368"
+}
diff --git a/backend/api/.sqlx/query-dc900d9177f1c883702f5ac299df9407fe134317d4594ccb01085dab7af77a70.json b/backend/api/.sqlx/query-dc900d9177f1c883702f5ac299df9407fe134317d4594ccb01085dab7af77a70.json
new file mode 100644
index 0000000..f7f7cbc
--- /dev/null
+++ b/backend/api/.sqlx/query-dc900d9177f1c883702f5ac299df9407fe134317d4594ccb01085dab7af77a70.json
@@ -0,0 +1,77 @@
+{
+ "db_name": "PostgreSQL",
+ "query": "\n SELECT \n id,\n projectname,\n projecturl,\n imageurl,\n subimageurl,\n description,\n about,\n published,\n created_at as \"created_at: _\",\n updated_at as \"updated_at: _\"\n FROM projects \n ORDER BY id \n LIMIT $1 OFFSET $2\n ",
+ "describe": {
+ "columns": [
+ {
+ "ordinal": 0,
+ "name": "id",
+ "type_info": "Uuid"
+ },
+ {
+ "ordinal": 1,
+ "name": "projectname",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 2,
+ "name": "projecturl",
+ "type_info": "Text"
+ },
+ {
+ "ordinal": 3,
+ "name": "imageurl",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 4,
+ "name": "subimageurl",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 5,
+ "name": "description",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 6,
+ "name": "about",
+ "type_info": "Varchar"
+ },
+ {
+ "ordinal": 7,
+ "name": "published",
+ "type_info": "Bool"
+ },
+ {
+ "ordinal": 8,
+ "name": "created_at: _",
+ "type_info": "Timestamptz"
+ },
+ {
+ "ordinal": 9,
+ "name": "updated_at: _",
+ "type_info": "Timestamptz"
+ }
+ ],
+ "parameters": {
+ "Left": [
+ "Int8",
+ "Int8"
+ ]
+ },
+ "nullable": [
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ false,
+ true,
+ true,
+ true
+ ]
+ },
+ "hash": "dc900d9177f1c883702f5ac299df9407fe134317d4594ccb01085dab7af77a70"
+}
diff --git a/backend/api/Cargo.lock b/backend/api/Cargo.lock
new file mode 100644
index 0000000..b781820
--- /dev/null
+++ b/backend/api/Cargo.lock
@@ -0,0 +1,2784 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "addr2line"
+version = "0.24.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+dependencies = [
+ "gimli",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+
+[[package]]
+name = "ahash"
+version = "0.8.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "android-tzdata"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "api"
+version = "0.1.0"
+dependencies = [
+ "axum",
+ "chrono",
+ "dotenv",
+ "serde",
+ "serde_json",
+ "sqlx",
+ "tokio",
+ "tower-http",
+ "uuid",
+]
+
+[[package]]
+name = "async-channel"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35"
+dependencies = [
+ "concurrent-queue",
+ "event-listener 2.5.3",
+ "futures-core",
+]
+
+[[package]]
+name = "async-channel"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89b47800b0be77592da0afd425cc03468052844aff33b84e33cc696f64e77b6a"
+dependencies = [
+ "concurrent-queue",
+ "event-listener-strategy",
+ "futures-core",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-executor"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30ca9a001c1e8ba5149f91a74362376cc6bc5b919d92d988668657bd570bdcec"
+dependencies = [
+ "async-task",
+ "concurrent-queue",
+ "fastrand 2.3.0",
+ "futures-lite 2.5.0",
+ "slab",
+]
+
+[[package]]
+name = "async-global-executor"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05b1b633a2115cd122d73b955eadd9916c18c8f510ec9cd1686404c60ad1c29c"
+dependencies = [
+ "async-channel 2.3.1",
+ "async-executor",
+ "async-io 2.4.0",
+ "async-lock 3.4.0",
+ "blocking",
+ "futures-lite 2.5.0",
+ "once_cell",
+]
+
+[[package]]
+name = "async-io"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fc5b45d93ef0529756f812ca52e44c221b35341892d3dcc34132ac02f3dd2af"
+dependencies = [
+ "async-lock 2.8.0",
+ "autocfg",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-lite 1.13.0",
+ "log",
+ "parking",
+ "polling 2.8.0",
+ "rustix 0.37.27",
+ "slab",
+ "socket2 0.4.10",
+ "waker-fn",
+]
+
+[[package]]
+name = "async-io"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a2b323ccce0a1d90b449fd71f2a06ca7faa7c54c2751f06c9bd851fc061059"
+dependencies = [
+ "async-lock 3.4.0",
+ "cfg-if",
+ "concurrent-queue",
+ "futures-io",
+ "futures-lite 2.5.0",
+ "parking",
+ "polling 3.7.4",
+ "rustix 0.38.42",
+ "slab",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "async-lock"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "287272293e9d8c41773cec55e365490fe034813a2f172f502d6ddcf75b2f582b"
+dependencies = [
+ "event-listener 2.5.3",
+]
+
+[[package]]
+name = "async-lock"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff6e472cdea888a4bd64f342f09b3f50e1886d32afe8df3d663c01140b811b18"
+dependencies = [
+ "event-listener 5.3.1",
+ "event-listener-strategy",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "async-std"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c634475f29802fde2b8f0b505b1bd00dfe4df7d4a000f0b36f7671197d5c3615"
+dependencies = [
+ "async-channel 1.9.0",
+ "async-global-executor",
+ "async-io 2.4.0",
+ "async-lock 3.4.0",
+ "crossbeam-utils",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-lite 2.5.0",
+ "gloo-timers",
+ "kv-log-macro",
+ "log",
+ "memchr",
+ "once_cell",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+ "wasm-bindgen-futures",
+]
+
+[[package]]
+name = "async-task"
+version = "4.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
+
+[[package]]
+name = "async-trait"
+version = "0.1.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "atoi"
+version = "2.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+
+[[package]]
+name = "axum"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f"
+dependencies = [
+ "async-trait",
+ "axum-core",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustversion",
+ "serde",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "backtrace"
+version = "0.3.74"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
+dependencies = [
+ "addr2line",
+ "cfg-if",
+ "libc",
+ "miniz_oxide",
+ "object",
+ "rustc-demangle",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "base64ct"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+
+[[package]]
+name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "blocking"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703f41c54fc768e63e091340b424302bb1c29ef4aa0c7f10fe849dfb114d29ea"
+dependencies = [
+ "async-channel 2.3.1",
+ "async-task",
+ "futures-io",
+ "futures-lite 2.5.0",
+ "piper",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b"
+
+[[package]]
+name = "cc"
+version = "1.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
+dependencies = [
+ "shlex",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "chrono"
+version = "0.4.39"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
+dependencies = [
+ "android-tzdata",
+ "iana-time-zone",
+ "js-sys",
+ "num-traits",
+ "serde",
+ "wasm-bindgen",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "concurrent-queue"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.9.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
+
+[[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16b80225097f2e5ae4e7179dd2266824648f3e2f49d9134d584b76389d31c4c3"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
+
+[[package]]
+name = "crossbeam-queue"
+version = "0.3.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "der"
+version = "0.7.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
+dependencies = [
+ "const-oid",
+ "pem-rfc7468",
+ "zeroize",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "subtle",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dotenv"
+version = "0.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
+
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
+[[package]]
+name = "either"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
+
+[[package]]
+name = "errno"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "etcetera"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
+dependencies = [
+ "cfg-if",
+ "home",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "event-listener"
+version = "2.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0"
+
+[[package]]
+name = "event-listener"
+version = "5.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6032be9bd27023a771701cc49f9f053c751055f71efb2e0ae5c15809093675ba"
+dependencies = [
+ "concurrent-queue",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "event-listener-strategy"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c3e4e0dd3673c1139bf041f3008816d9cf2946bbfac2945c09e523b8d7b05b2"
+dependencies = [
+ "event-listener 5.3.1",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "fastrand"
+version = "1.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be"
+dependencies = [
+ "instant",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
+name = "flume"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+ "spin",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
+
+[[package]]
+name = "futures-executor"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "futures-util",
+]
+
+[[package]]
+name = "futures-intrusive"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
+dependencies = [
+ "futures-core",
+ "lock_api",
+ "parking_lot",
+]
+
+[[package]]
+name = "futures-io"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
+
+[[package]]
+name = "futures-lite"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
+dependencies = [
+ "fastrand 1.9.0",
+ "futures-core",
+ "futures-io",
+ "memchr",
+ "parking",
+ "pin-project-lite",
+ "waker-fn",
+]
+
+[[package]]
+name = "futures-lite"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1"
+dependencies = [
+ "fastrand 2.3.0",
+ "futures-core",
+ "futures-io",
+ "parking",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
+
+[[package]]
+name = "futures-task"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
+
+[[package]]
+name = "futures-util"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "pin-utils",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "gimli"
+version = "0.31.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+[[package]]
+name = "gloo-timers"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994"
+dependencies = [
+ "futures-channel",
+ "futures-core",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.14.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
+dependencies = [
+ "ahash",
+ "allocator-api2",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+
+[[package]]
+name = "hashlink"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
+dependencies = [
+ "hashbrown 0.14.5",
+]
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
+
+[[package]]
+name = "hermit-abi"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "hkdf"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
+dependencies = [
+ "hmac",
+]
+
+[[package]]
+name = "hmac"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "home"
+version = "0.5.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "http"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hyper"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "256fb8d4bd6413123cc9d91832d78325c48ff41677595be797d90f42969beae0"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4"
+dependencies = [
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "pin-project-lite",
+ "tokio",
+ "tower-service",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows-core",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "icu_collections"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid_transform"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_locid_transform_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid_transform_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
+
+[[package]]
+name = "icu_normalizer"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "utf16_iter",
+ "utf8_iter",
+ "write16",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
+
+[[package]]
+name = "icu_properties"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locid_transform",
+ "icu_properties_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
+
+[[package]]
+name = "icu_provider"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_provider_macros",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_provider_macros"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "idna"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.15.2",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "io-lifetimes"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2"
+dependencies = [
+ "hermit-abi 0.3.9",
+ "libc",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
+
+[[package]]
+name = "js-sys"
+version = "0.3.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6717b6b5b077764fb5966237269cb3c64edddde4b14ce42647430a78ced9e7b7"
+dependencies = [
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "kv-log-macro"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de8b303297635ad57c9f5059fd9cee7a47f8e8daa09df0fcd07dd39fb22977f"
+dependencies = [
+ "log",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+dependencies = [
+ "spin",
+]
+
+[[package]]
+name = "libc"
+version = "0.2.169"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
+
+[[package]]
+name = "libm"
+version = "0.2.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
+
+[[package]]
+name = "libsqlite3-sys"
+version = "0.30.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.3.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519"
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89"
+
+[[package]]
+name = "litemap"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
+dependencies = [
+ "value-bag",
+]
+
+[[package]]
+name = "matchit"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
+
+[[package]]
+name = "md-5"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
+dependencies = [
+ "cfg-if",
+ "digest",
+]
+
+[[package]]
+name = "memchr"
+version = "2.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ffbe83022cedc1d264172192511ae958937694cd57ce297164951b8b3568394"
+dependencies = [
+ "adler2",
+]
+
+[[package]]
+name = "mio"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8614eb2c83d59d1c8cc974dd3f920198647674a0a035e1af1fa58707e317466"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num-bigint-dig"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc84195820f291c7697304f3cbdadd1cb7199c0efc917ff5eafd71225c136151"
+dependencies = [
+ "byteorder",
+ "lazy_static",
+ "libm",
+ "num-integer",
+ "num-iter",
+ "num-traits",
+ "rand",
+ "smallvec",
+ "zeroize",
+]
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+ "libm",
+]
+
+[[package]]
+name = "object"
+version = "0.36.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.20.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+
+[[package]]
+name = "openssl"
+version = "0.10.68"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6174bc48f102d208783c2c84bf931bb75927a617866870de8a4ea85597f871f5"
+dependencies = [
+ "bitflags 2.6.0",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.104"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "45abf306cbf99debc8195b66b7346498d7b10c210de50418b5ccd7ceba08c741"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
+name = "parking"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "pem-rfc7468"
+version = "0.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
+dependencies = [
+ "base64ct",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "915a1e146535de9163f3987b8944ed8cf49a18bb0056bcebcdcece385cece4ff"
+
+[[package]]
+name = "pin-utils"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
+
+[[package]]
+name = "piper"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96c8c490f422ef9a4efd2cb5b42b76c8613d7e7dfc1caf667b8a3350a5acc066"
+dependencies = [
+ "atomic-waker",
+ "fastrand 2.3.0",
+ "futures-io",
+]
+
+[[package]]
+name = "pkcs1"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
+dependencies = [
+ "der",
+ "pkcs8",
+ "spki",
+]
+
+[[package]]
+name = "pkcs8"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
+dependencies = [
+ "der",
+ "spki",
+]
+
+[[package]]
+name = "pkg-config"
+version = "0.3.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
+
+[[package]]
+name = "polling"
+version = "2.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce"
+dependencies = [
+ "autocfg",
+ "bitflags 1.3.2",
+ "cfg-if",
+ "concurrent-queue",
+ "libc",
+ "log",
+ "pin-project-lite",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "polling"
+version = "3.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a604568c3202727d1507653cb121dbd627a58684eb09a820fd746bee38b4442f"
+dependencies = [
+ "cfg-if",
+ "concurrent-queue",
+ "hermit-abi 0.4.0",
+ "pin-project-lite",
+ "rustix 0.38.42",
+ "tracing",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.92"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.38"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "rand"
+version = "0.8.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+dependencies = [
+ "libc",
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+dependencies = [
+ "getrandom",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834"
+dependencies = [
+ "bitflags 2.6.0",
+]
+
+[[package]]
+name = "rsa"
+version = "0.9.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519"
+dependencies = [
+ "const-oid",
+ "digest",
+ "num-bigint-dig",
+ "num-integer",
+ "num-traits",
+ "pkcs1",
+ "pkcs8",
+ "rand_core",
+ "signature",
+ "spki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustc-demangle"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+
+[[package]]
+name = "rustix"
+version = "0.37.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2"
+dependencies = [
+ "bitflags 1.3.2",
+ "errno",
+ "io-lifetimes",
+ "libc",
+ "linux-raw-sys 0.3.8",
+ "windows-sys 0.48.0",
+]
+
+[[package]]
+name = "rustix"
+version = "0.38.42"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85"
+dependencies = [
+ "bitflags 2.6.0",
+ "errno",
+ "libc",
+ "linux-raw-sys 0.4.14",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248"
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "schannel"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.6.0",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1863fd3768cd83c56a7f60faa4dc0d403f1b6df0a38c3c25f44b7894e45370d5"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.216"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b9781016e935a97e8beecf0c933758c97a5520d32930e460142b4cd80c6338e"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.216"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46f859dbbf73865c6627ed570e78961cd3ac92407a2d117204c49232485da55e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.134"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d00f4175c42ee48b15416f6193a959ba3a0d67fc699a0db9ad12df9f83991c7d"
+dependencies = [
+ "itoa",
+ "memchr",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
+dependencies = [
+ "itoa",
+ "serde",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "signal-hook-registry"
+version = "1.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "signature"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "digest",
+ "rand_core",
+]
+
+[[package]]
+name = "slab"
+version = "0.4.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "socket2"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "socket2"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
+dependencies = [
+ "libc",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "spin"
+version = "0.9.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
+dependencies = [
+ "lock_api",
+]
+
+[[package]]
+name = "spki"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
+dependencies = [
+ "base64ct",
+ "der",
+]
+
+[[package]]
+name = "sqlformat"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bba3a93db0cc4f7bdece8bb09e77e2e785c20bfebf79eb8340ed80708048790"
+dependencies = [
+ "nom",
+ "unicode_categories",
+]
+
+[[package]]
+name = "sqlx"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93334716a037193fac19df402f8571269c84a00852f6a7066b5d2616dcd64d3e"
+dependencies = [
+ "sqlx-core",
+ "sqlx-macros",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+]
+
+[[package]]
+name = "sqlx-core"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4d8060b456358185f7d50c55d9b5066ad956956fddec42ee2e8567134a8936e"
+dependencies = [
+ "async-io 1.13.0",
+ "async-std",
+ "atoi",
+ "byteorder",
+ "bytes",
+ "chrono",
+ "crc",
+ "crossbeam-queue",
+ "either",
+ "event-listener 5.3.1",
+ "futures-channel",
+ "futures-core",
+ "futures-intrusive",
+ "futures-io",
+ "futures-util",
+ "hashbrown 0.14.5",
+ "hashlink",
+ "hex",
+ "indexmap",
+ "log",
+ "memchr",
+ "native-tls",
+ "once_cell",
+ "paste",
+ "percent-encoding",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlformat",
+ "thiserror",
+ "tracing",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "sqlx-macros"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cac0692bcc9de3b073e8d747391827297e075c7710ff6276d9f7a1f3d58c6657"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "sqlx-core",
+ "sqlx-macros-core",
+ "syn",
+]
+
+[[package]]
+name = "sqlx-macros-core"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1804e8a7c7865599c9c79be146dc8a9fd8cc86935fa641d3ea58e5f0688abaa5"
+dependencies = [
+ "async-std",
+ "dotenvy",
+ "either",
+ "heck",
+ "hex",
+ "once_cell",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "sha2",
+ "sqlx-core",
+ "sqlx-mysql",
+ "sqlx-postgres",
+ "sqlx-sqlite",
+ "syn",
+ "tempfile",
+ "url",
+]
+
+[[package]]
+name = "sqlx-mysql"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "64bb4714269afa44aef2755150a0fc19d756fb580a67db8885608cf02f47d06a"
+dependencies = [
+ "atoi",
+ "base64",
+ "bitflags 2.6.0",
+ "byteorder",
+ "bytes",
+ "chrono",
+ "crc",
+ "digest",
+ "dotenvy",
+ "either",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "generic-array",
+ "hex",
+ "hkdf",
+ "hmac",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "percent-encoding",
+ "rand",
+ "rsa",
+ "serde",
+ "sha1",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "uuid",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-postgres"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fa91a732d854c5d7726349bb4bb879bb9478993ceb764247660aee25f67c2f8"
+dependencies = [
+ "atoi",
+ "base64",
+ "bitflags 2.6.0",
+ "byteorder",
+ "chrono",
+ "crc",
+ "dotenvy",
+ "etcetera",
+ "futures-channel",
+ "futures-core",
+ "futures-io",
+ "futures-util",
+ "hex",
+ "hkdf",
+ "hmac",
+ "home",
+ "itoa",
+ "log",
+ "md-5",
+ "memchr",
+ "once_cell",
+ "rand",
+ "serde",
+ "serde_json",
+ "sha2",
+ "smallvec",
+ "sqlx-core",
+ "stringprep",
+ "thiserror",
+ "tracing",
+ "uuid",
+ "whoami",
+]
+
+[[package]]
+name = "sqlx-sqlite"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d5b2cf34a45953bfd3daaf3db0f7a7878ab9b7a6b91b422d24a7a9e4c857b680"
+dependencies = [
+ "atoi",
+ "chrono",
+ "flume",
+ "futures-channel",
+ "futures-core",
+ "futures-executor",
+ "futures-intrusive",
+ "futures-util",
+ "libsqlite3-sys",
+ "log",
+ "percent-encoding",
+ "serde",
+ "serde_urlencoded",
+ "sqlx-core",
+ "tracing",
+ "url",
+ "uuid",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "stringprep"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
+dependencies = [
+ "unicode-bidi",
+ "unicode-normalization",
+ "unicode-properties",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.91"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d53cbcb5a243bd33b7858b1d7f4aca2153490815872d86d955d6ea29f743c035"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+
+[[package]]
+name = "synstructure"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tempfile"
+version = "3.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28cce251fcbc87fac86a866eeb0d6c2d536fc16d06f184bb61aeae11aa4cee0c"
+dependencies = [
+ "cfg-if",
+ "fastrand 2.3.0",
+ "once_cell",
+ "rustix 0.38.42",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551"
+dependencies = [
+ "backtrace",
+ "bytes",
+ "libc",
+ "mio",
+ "parking_lot",
+ "pin-project-lite",
+ "signal-hook-registry",
+ "socket2 0.5.8",
+ "tokio-macros",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697"
+dependencies = [
+ "bitflags 2.6.0",
+ "bytes",
+ "http",
+ "pin-project-lite",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.41"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "typenum"
+version = "1.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
+
+[[package]]
+name = "unicode-bidi"
+version = "0.3.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83"
+
+[[package]]
+name = "unicode-normalization"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5033c97c4262335cded6d6fc3e5c18ab755e1a3dc96376350f3d8e9f009ad956"
+dependencies = [
+ "tinyvec",
+]
+
+[[package]]
+name = "unicode-properties"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
+
+[[package]]
+name = "unicode_categories"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
+
+[[package]]
+name = "url"
+version = "2.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+]
+
+[[package]]
+name = "utf16_iter"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "uuid"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8c5f0a0af699448548ad1a2fbf920fb4bee257eae39953ba95cb84891a0446a"
+dependencies = [
+ "getrandom",
+ "serde",
+]
+
+[[package]]
+name = "value-bag"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ef4c4aa54d5d05a279399bfa921ec387b7aba77caf7a682ae8d86785b8fdad2"
+
+[[package]]
+name = "vcpkg"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "waker-fn"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7"
+
+[[package]]
+name = "wasi"
+version = "0.11.0+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+
+[[package]]
+name = "wasite"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a474f6281d1d70c17ae7aa6a613c87fce69a127e2624002df63dcb39d6cf6396"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "wasm-bindgen-macro",
+]
+
+[[package]]
+name = "wasm-bindgen-backend"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f89bb38646b4f81674e8f5c3fb81b562be1fd936d84320f3264486418519c79"
+dependencies = [
+ "bumpalo",
+ "log",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38176d9b44ea84e9184eff0bc34cc167ed044f816accfe5922e54d84cf48eca2"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2cc6181fd9a7492eef6fef1f33961e3695e4579b9872a6f7c83aee556666d4fe"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30d7a95b763d3c45903ed6c81f156801839e5ee968bb07e534c44df0fcd330c2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-backend",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "943aab3fdaaa029a6e0271b35ea10b72b943135afe9bffca82384098ad0e06a6"
+
+[[package]]
+name = "web-sys"
+version = "0.3.76"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04dd7223427d52553d3702c004d3b2fe07c148165faa56313cb00211e31c12bc"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "whoami"
+version = "1.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d"
+dependencies = [
+ "redox_syscall",
+ "wasite",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-core"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
+dependencies = [
+ "windows-targets 0.48.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.5",
+ "windows_aarch64_msvc 0.48.5",
+ "windows_i686_gnu 0.48.5",
+ "windows_i686_msvc 0.48.5",
+ "windows_x86_64_gnu 0.48.5",
+ "windows_x86_64_gnullvm 0.48.5",
+ "windows_x86_64_msvc 0.48.5",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "write16"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
+
+[[package]]
+name = "writeable"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
+
+[[package]]
+name = "yoke"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.7.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+dependencies = [
+ "byteorder",
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.7.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
+
+[[package]]
+name = "zerovec"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.10.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/backend/api/Cargo.toml b/backend/api/Cargo.toml
new file mode 100644
index 0000000..6a3b912
--- /dev/null
+++ b/backend/api/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "api"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+axum = "0.7.9"
+chrono = { version = "0.4.39", features = ["serde"] }
+dotenv = "0.15.0"
+serde = { version = "1.0.216", features = ["derive"] }
+serde_json = "1.0.134"
+sqlx = { version = "0.8.2", features = ["runtime-async-std-native-tls", "postgres", "chrono", "uuid"] }
+tokio = { version = "1.42.0", features = ["full"] }
+tower-http = { version = "0.6.2", features = ["cors"] }
+uuid = { version = "1.11.0", features = ["serde", "v4"] }
+
+[[bin]]
+name = "api"
+path = "src/main.rs"
+
diff --git a/backend/api/Dockerfile b/backend/api/Dockerfile
new file mode 100644
index 0000000..30deda5
--- /dev/null
+++ b/backend/api/Dockerfile
@@ -0,0 +1,43 @@
+# Build Stage
+FROM rust:latest AS builder
+
+WORKDIR /app
+
+# Copy Cargo files
+COPY Cargo.toml Cargo.lock ./
+
+# Fetch dependencies without building the app
+RUN cargo fetch
+
+# Copy the source code and SQLx metadata
+COPY . .
+COPY .sqlx .
+
+# Enable offline mode for SQLx
+ENV SQLX_OFFLINE=true
+
+# Build the final binary
+RUN cargo build --release
+
+
+# Runtime Stage
+FROM ubuntu:22.04
+
+RUN apt-get update && apt-get install -y \
+ ca-certificates \
+ openssl \
+ libssl3 \
+ && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /app
+
+# Copy the migrations folder to the runtime stage
+COPY --from=builder /app/migrations ./migrations
+
+
+
+# Copy the binary
+COPY --from=builder /app/target/release/api .
+
+EXPOSE 8000
+CMD ["./api"]
diff --git a/backend/api/docker-compose.app.yml b/backend/api/docker-compose.app.yml
new file mode 100644
index 0000000..a599883
--- /dev/null
+++ b/backend/api/docker-compose.app.yml
@@ -0,0 +1,45 @@
+version: "3"
+services:
+ postgres:
+ image: postgres:latest
+ container_name: postgres
+ ports:
+ - "6500:5432"
+ volumes:
+ - progresDB:/var/lib/postgresql/data
+ env_file:
+ - ./.env
+ networks:
+ - app_network
+
+ pgAdmin:
+ image: dpage/pgadmin4
+ container_name: pgAdmin
+ env_file:
+ - ./.env
+ ports:
+ - "5050:80"
+ networks:
+ - app_network
+
+ api:
+ build: .
+ container_name: rust_backend
+ ports:
+ - "8000:8000"
+ depends_on:
+ - postgres
+ environment:
+ # Use the service name 'postgres' instead of localhost
+ DATABASE_URL: postgresql://admin:saisab@postgres:5432/rust_sqlx?schema=public
+ volumes:
+ - ./src:/app/src:ro
+ networks:
+ - app_network
+
+networks:
+ app_network:
+ driver: bridge
+
+volumes:
+ progresDB:
diff --git a/backend/api/docker-compose.yml b/backend/api/docker-compose.yml
new file mode 100644
index 0000000..ae825ea
--- /dev/null
+++ b/backend/api/docker-compose.yml
@@ -0,0 +1,20 @@
+version: "3"
+services:
+ postgres:
+ image: postgres:latest
+ container_name: postgres
+ ports:
+ - "6500:5432"
+ volumes:
+ - progresDB:/var/lib/postgresql/data
+ env_file:
+ - ./.env
+ pgAdmin:
+ image: dpage/pgadmin4
+ container_name: pgAdmin
+ env_file:
+ - ./.env
+ ports:
+ - "5050:80"
+volumes:
+ progresDB:
diff --git a/backend/api/migrations/20241223043129_init.down.sql b/backend/api/migrations/20241223043129_init.down.sql
new file mode 100644
index 0000000..dedc115
--- /dev/null
+++ b/backend/api/migrations/20241223043129_init.down.sql
@@ -0,0 +1,4 @@
+-- Add down migration script here
+-- Add down migration script here
+
+DROP TABLE IF EXISTS projects;
diff --git a/backend/api/migrations/20241223043129_init.up.sql b/backend/api/migrations/20241223043129_init.up.sql
new file mode 100644
index 0000000..4a881dd
--- /dev/null
+++ b/backend/api/migrations/20241223043129_init.up.sql
@@ -0,0 +1,47 @@
+CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
+
+CREATE TABLE IF NOT EXISTS projects (
+ id UUID PRIMARY KEY NOT NULL DEFAULT (uuid_generate_v4()),
+ projectname VARCHAR(255) NOT NULL UNIQUE,
+ projecturl TEXT NOT NULL,
+ imageurl VARCHAR(100) NOT NULL,
+ subimageurl VARCHAR(255) NOT NULL,
+ description VARCHAR NOT NULL,
+ about VARCHAR NOT NULL,
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
+ published BOOLEAN DEFAULT FALSE,
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
+);
+
+INSERT INTO projects (
+ projectname,
+ projecturl,
+ imageurl,
+ subimageurl,
+ description,
+ about
+) VALUES
+ (
+ 'Split Contract',
+ 'https://paymentsplitter.cardanoapi.io/',
+ '/images/splitContractC.jpg',
+ '/images/paymentsplitter.png',
+ 'Explore and experience the benefits of clarity and fairness',
+ 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Nihil amet officia assumenda ipsa magni quisquam quas, consectetur nostrum ducimus perspiciatis.'
+ ),
+ (
+ 'Mempool',
+ 'https://mempool.cardanoapi.io/',
+ '/images/mempoolC.jpg',
+ '/images/Mempool.png',
+ 'A complete set of tools for building decentralized applications',
+ 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Nihil amet officia assumenda ipsa magni quisquam quas, consectetur nostrum ducimus perspiciatis.'
+ ),
+ (
+ 'Kuber IDE',
+ 'https://kuberide.com/',
+ '/images/kuberC.jpg',
+ '/images/KuberIDE.png',
+ 'Pro setup environment for writing and trying out plutus contracts and cardano transactions',
+ 'Lorem ipsum dolor sit amet consectetur adipisicing elit. Nihil amet officia assumenda ipsa magni quisquam quas, consectetur nostrum ducimus perspiciatis.'
+ );
\ No newline at end of file
diff --git a/backend/api/src/handler.rs b/backend/api/src/handler.rs
new file mode 100644
index 0000000..60825ae
--- /dev/null
+++ b/backend/api/src/handler.rs
@@ -0,0 +1,234 @@
+use std::sync::Arc;
+
+use axum::{
+ extract::{Path, Query, State},
+ http::StatusCode,
+ response::IntoResponse,
+ Json,
+};
+use serde_json::json;
+
+use crate::{
+ model::ProjectModel,
+ schema::{CreateProjectSchema, FilterOptions, UpdateProjectSchema},
+ AppState,
+};
+
+
+pub async fn health_checker_handler() -> impl IntoResponse {
+ const MESSAGE: &str = "Simple CRUD API with Rust, SQLX, Postgres,and Axum";
+
+ let json_response = serde_json::json!({
+ "status": "success",
+ "message": MESSAGE
+ });
+
+ Json(json_response)
+}
+
+
+pub async fn project_list_handler(
+ opts: Option>,
+ State(data): State>,
+) -> Result)> {
+ let Query(opts) = opts.unwrap_or_default();
+
+ let limit = opts.limit.unwrap_or(10);
+ let offset = (opts.page.unwrap_or(1) - 1) * limit;
+
+ let query_result = sqlx::query_as!(
+ ProjectModel,
+ r#"
+ SELECT
+ id,
+ projectname,
+ projecturl,
+ imageurl,
+ subimageurl,
+ description,
+ about,
+ published,
+ created_at as "created_at: _",
+ updated_at as "updated_at: _"
+ FROM projects
+ ORDER BY id
+ LIMIT $1 OFFSET $2
+ "#,
+ limit as i32,
+ offset as i32
+ )
+ .fetch_all(&data.db)
+ .await;
+
+ if query_result.is_err() {
+ let error_response = serde_json::json!({
+ "status": "fail",
+ "message": "Something bad happened while fetching all note items",
+ });
+ return Err((StatusCode::INTERNAL_SERVER_ERROR, Json(error_response)));
+ }
+
+ let projects = query_result.unwrap();
+
+ let json_response = serde_json::json!({
+ "status": "success",
+ "results": projects.len(),
+ "projects": projects
+ });
+ Ok(Json(json_response))
+}
+
+
+pub async fn create_project_handler(
+ State(data): State>,
+ Json(body): Json,
+) -> Result)> {
+ let query_result = sqlx::query_as!(
+ ProjectModel,
+ r#"
+ INSERT INTO projects (
+ projectname,
+ projecturl,
+ imageurl,
+ subimageurl,
+ description,
+ about,
+ published
+ )
+ VALUES ($1, $2, $3, $4, $5, $6, $7)
+ RETURNING *
+ "#,
+ body.projectname.to_string(),
+ body.projecturl.to_string(),
+ body.imageurl.to_string(),
+ body.subimageurl.to_string(),
+ body.description.to_string(),
+ body.about.to_string(),
+ body.published
+ )
+ .fetch_one(&data.db)
+ .await;
+
+ match query_result {
+ Ok(project) => {
+ let project_response = json!({
+ "status": "success",
+ "data": json!({
+ "project": project
+ })
+ });
+ Ok((StatusCode::CREATED, Json(project_response)))
+ }
+ Err(e) => {
+ if e.to_string()
+ .contains("duplicate key value violates unique constraint")
+ {
+ let error_response = json!({
+ "status": "fail",
+ "message": "Project with that name already exists",
+ });
+ Err((StatusCode::CONFLICT, Json(error_response)))
+ } else {
+ Err((
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({
+ "status": "error",
+ "message": format!("{:?}", e)
+ }))
+ ))
+ }
+ }
+ }
+}
+
+pub async fn edit_project_handler(
+ Path(id): Path,
+ State(data): State>,
+ Json(body): Json,
+) -> Result)> {
+ let query_result = sqlx::query_as!(ProjectModel, "SELECT * FROM projects WHERE id = $1", id)
+ .fetch_one(&data.db)
+ .await;
+
+ if query_result.is_err() {
+ let error_response = json!({
+ "status": "fail",
+ "message": format!("Project with ID: {} not found", id)
+ });
+ return Err((StatusCode::NOT_FOUND, Json(error_response)));
+ }
+
+ let now = chrono::Utc::now();
+ let project = query_result.unwrap();
+
+ let query_result = sqlx::query_as!(
+ ProjectModel,
+ r#"
+ UPDATE projects
+ SET projectname = $1,
+ projecturl = $2,
+ imageurl = $3,
+ subimageurl = $4,
+ description = $5,
+ about = $6,
+ published = $7,
+ updated_at = $8
+ WHERE id = $9
+ RETURNING *
+ "#,
+ body.projectname.unwrap_or(project.projectname),
+ body.projecturl.unwrap_or(project.projecturl),
+ body.imageurl.unwrap_or(project.imageurl),
+ body.subimageurl.unwrap_or(project.subimageurl),
+ body.description.unwrap_or(project.description),
+ body.about.unwrap_or(project.about),
+ body.published.or(project.published),
+ now,
+ id
+ )
+ .fetch_one(&data.db)
+ .await;
+
+ match query_result {
+ Ok(project) => {
+ let project_response = json!({
+ "status": "success",
+ "data": json!({
+ "project": project
+ })
+ });
+ Ok(Json(project_response))
+ }
+ Err(err) => {
+ Err((
+ StatusCode::INTERNAL_SERVER_ERROR,
+ Json(json!({
+ "status": "error",
+ "message": format!("{:?}", err)
+ }))
+ ))
+ }
+ }
+}
+pub async fn delete_project_handler(
+ Path(id): Path,
+ State(data): State>,
+ ) -> Result)> {
+ let rows_affected = sqlx::query!("DELETE FROM projects WHERE id = $1", id)
+ .execute(&data.db)
+ .await
+ .unwrap()
+ .rows_affected();
+
+ if rows_affected == 0 {
+ let error_response = json!({
+ "status": "fail",
+ "message": format!("Project with ID: {} not found", id)
+ });
+ return Err((StatusCode::NOT_FOUND, Json(error_response)));
+ }
+
+ Ok(StatusCode::NO_CONTENT)
+ }
+
+
diff --git a/backend/api/src/main.rs b/backend/api/src/main.rs
new file mode 100644
index 0000000..9e4fdb4
--- /dev/null
+++ b/backend/api/src/main.rs
@@ -0,0 +1,73 @@
+mod handler;
+mod model;
+mod route;
+mod schema;
+
+use std::sync::Arc;
+
+use axum::http::{
+ header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
+ HeaderValue, Method,
+};
+use dotenv::dotenv;
+use route::create_router;
+use tower_http::cors::CorsLayer;
+
+use sqlx::{postgres::PgPoolOptions, Pool, Postgres};
+
+pub struct AppState {
+ db: Pool,
+}
+
+
+
+use sqlx::migrate::Migrator;
+use std::path::Path;
+
+async fn run_migrations(pool: &sqlx::PgPool) -> Result<(), sqlx::Error> {
+ println!("Running database migrations...");
+ sqlx::migrate!("./migrations")
+ .run(pool)
+ .await?;
+ println!("Migrations completed successfully!");
+ Ok(())
+}
+
+#[tokio::main]
+async fn main() {
+ dotenv().ok();
+
+
+ let database_url = std::env::var("DATABASE_URL").expect("DATABASE_URL must be set");
+ let pool = match PgPoolOptions::new()
+ .max_connections(10)
+ .connect(&database_url)
+ .await
+ {
+ Ok(pool) => {
+ println!("✅Connection to the database is successful!");
+ pool
+ }
+ Err(err) => {
+ println!("🔥 Failed to connect to the database: {:?}", err);
+ std::process::exit(1);
+ }
+ };
+ if let Err(err) = run_migrations(&pool).await {
+ println!("🔥 Failed to run migrations: {:?}", err);
+ std::process::exit(1);
+ }
+
+ let cors = CorsLayer::new()
+ .allow_origin("http://localhost:3000".parse::().unwrap())
+ .allow_methods([Method::GET, Method::POST, Method::PATCH, Method::DELETE])
+ .allow_credentials(true)
+ .allow_headers([AUTHORIZATION, ACCEPT, CONTENT_TYPE]);
+
+ let app = create_router(Arc::new(AppState { db: pool.clone() })).layer(cors);
+
+ println!("🚀 Server started successfully");
+ let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
+ axum::serve(listener, app).await.unwrap();
+
+}
diff --git a/backend/api/src/model.rs b/backend/api/src/model.rs
new file mode 100644
index 0000000..4888d72
--- /dev/null
+++ b/backend/api/src/model.rs
@@ -0,0 +1,21 @@
+use serde::{Deserialize, Serialize};
+use sqlx::FromRow;
+use uuid::Uuid;
+
+#[derive(Debug, FromRow, Deserialize, Serialize)]
+#[allow(non_snake_case)]
+pub struct ProjectModel {
+ pub id: Uuid,
+ pub projectname: String,
+ pub projecturl: String,
+ pub imageurl: String,
+ pub subimageurl: String,
+ pub description: String,
+ pub about: String,
+
+ pub published: Option,
+ #[serde(rename = "createdAt")]
+ pub created_at: Option>,
+ #[serde(rename = "updatedAt")]
+ pub updated_at: Option>,
+}
diff --git a/backend/api/src/route.rs b/backend/api/src/route.rs
new file mode 100644
index 0000000..a6f948e
--- /dev/null
+++ b/backend/api/src/route.rs
@@ -0,0 +1,23 @@
+use std::sync::Arc;
+
+use axum::{
+ routing::{delete, get, patch, post},
+ Router,
+};
+
+use crate::{
+ handler::{
+ create_project_handler, delete_project_handler, edit_project_handler, health_checker_handler, project_list_handler,
+ },
+ AppState,
+};
+
+pub fn create_router(app_state: Arc) -> Router {
+ Router::new()
+ .route("/api/test", get(health_checker_handler))
+ .route("/api/projects", get(project_list_handler))
+ .route("/api/projects", post(create_project_handler))
+ .route("/api/projects/:id", patch(edit_project_handler))
+ .route("/api/projects/:id", delete(delete_project_handler))
+ .with_state(app_state)
+}
diff --git a/backend/api/src/schema.rs b/backend/api/src/schema.rs
new file mode 100644
index 0000000..657d20c
--- /dev/null
+++ b/backend/api/src/schema.rs
@@ -0,0 +1,35 @@
+use serde::{Deserialize, Serialize};
+
+#[derive(Deserialize, Debug, Default)]
+pub struct FilterOptions {
+ pub page: Option,
+ pub limit: Option,
+}
+
+#[derive(Deserialize, Debug)]
+pub struct ParamOptions {
+ pub id: String,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct CreateProjectSchema {
+ pub projectname: String,
+ pub projecturl: String,
+ pub imageurl: String,
+ pub subimageurl: String,
+ pub description: String,
+ pub about: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub published: Option,
+}
+
+#[derive(Serialize, Deserialize, Debug)]
+pub struct UpdateProjectSchema {
+ pub projectname: Option,
+ pub projecturl: Option,
+ pub imageurl: Option,
+ pub subimageurl: Option,
+ pub description: Option,
+ pub about: Option,
+ pub published: Option,
+}
diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml
index 3160ddc..404e97b 100644
--- a/docker-compose-dev.yml
+++ b/docker-compose-dev.yml
@@ -1,9 +1,64 @@
services:
- app:
+ postgres:
+ image: postgres:latest
+ container_name: postgres_prod
+ ports:
+ - "6500:5432" # Map host port 6500 to container's port 5432
+ volumes:
+ - progresDB:/var/lib/postgresql/data # Persistent data volume
+ env_file:
+ - ./.env # Make sure the .env file contains necessary database credentials
+ networks:
+ - app_network_prod
+
+ pgAdmin:
+ image: dpage/pgadmin4
+ container_name: pgAdmin_prod
+ env_file:
+ - ./.env # Use the same .env file to configure pgAdmin
+ ports:
+ - "5050:80" # Expose pgAdmin UI on host port 5050
+ networks:
+ - app_network_prod
+
+ backend:
build:
- context: .
- dockerfile: Dockerfile.dev
+ context: ./backend/api
+ dockerfile: Dockerfile
+ container_name: cardanoapiio_backend
ports:
- - "3000:3000"
+ - "8000:8000"
+ depends_on:
+ - postgres
+ environment:
+ # Use the service name 'postgres' instead of localhost
+ DATABASE_URL: postgresql://admin:saisab@postgres:5432/rust_sqlx?schema=public
volumes:
- - .:/src
+ - .backend/api/src:/app/src:ro
+ networks:
+ - app_network_prod
+
+ frontend:
+ build:
+ context: . # Root context for frontend (Next.js app)
+ dockerfile: Dockerfile # Dockerfile location for frontend
+ container_name: nextjs_frontend_prod
+ ports:
+ - "3000:3000" # Expose frontend on host port 3000
+ environment:
+ # The frontend needs to know where the API is hosted
+ API_URL: http://backend:8000 # Use 'backend' as the service name for internal communication
+
+ NODE_ENV: production # Set NODE_ENV to production in the frontend container
+ restart: always # Automatically restart frontend on failure
+ depends_on:
+ - backend # Ensure frontend starts after backend is up
+ networks:
+ - app_network_prod
+
+networks:
+ app_network_prod:
+ driver: bridge # Use the bridge network for communication between containers
+
+volumes:
+ progresDB:
diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml
index 0f9d84f..799f09e 100644
--- a/docker-compose-prod.yml
+++ b/docker-compose-prod.yml
@@ -1,7 +1,61 @@
-version: "3"
services:
- nextjs:
- image: saisab/cardanoapiio-app:latest
+ postgres:
+ image: postgres:latest
+ container_name: postgres_prod
ports:
- - "3000:3000"
- restart: always
+ - "6500:5432" # Map host port 6500 to container's port 5432
+ volumes:
+ - progresDB:/var/lib/postgresql/data # Persistent data volume
+ env_file:
+ - ./.env # Make sure the .env file contains necessary database credentials
+ networks:
+ - app_network_prod
+
+ pgAdmin:
+ image: dpage/pgadmin4
+ container_name: pgAdmin_prod
+ env_file:
+ - ./.env # Use the same .env file to configure pgAdmin
+ ports:
+ - "5050:80" # Expose pgAdmin UI on host port 5050
+ networks:
+ - app_network_prod
+
+ backend:
+ image: saisab/cardanoapiio-backend:latest
+ ports:
+ - "8000:8000" # Expose backend API on host port 8000
+ depends_on:
+ - postgres # Ensure backend starts after postgres is up
+ environment:
+ # Use the service name 'postgres' to refer to the database container internally
+ DATABASE_URL: postgresql://admin:saisab@postgres:5432/rust_sqlx?schema=public
+ volumes:
+ - ./backend/api/src:/app/src:ro # Mount source code to the container for easy updates
+ networks:
+ - app_network_prod
+
+ frontend:
+ build:
+ context: . # Root context for frontend (Next.js app)
+ dockerfile: Dockerfile # Dockerfile location for frontend
+ container_name: nextjs_frontend_prod
+ ports:
+ - "3000:3000" # Expose frontend on host port 3000
+ environment:
+ # The frontend needs to know where the API is hosted
+ API_URL: http://backend:8000 # Use 'backend' as the service name for internal communication
+
+ NODE_ENV: production # Set NODE_ENV to production in the frontend container
+ restart: always # Automatically restart frontend on failure
+ depends_on:
+ - backend # Ensure frontend starts after backend is up
+ networks:
+ - app_network_prod
+
+networks:
+ app_network_prod:
+ driver: bridge # Use the bridge network for communication between containers
+
+volumes:
+ progresDB:
diff --git a/src/app/Component/Pagination.tsx b/src/app/Component/Pagination.tsx
index abf26a5..0a1e464 100644
--- a/src/app/Component/Pagination.tsx
+++ b/src/app/Component/Pagination.tsx
@@ -24,16 +24,24 @@ export default function Pagination({
}: PaginationProps) {
const [currentPage, setCurrentPage] = useState(1);
+ // Ensure data is an array and handle empty data
+ const safeData = Array.isArray(data) ? data : [];
+
// Calculate the index range for the cards to display on the current page
const indexOfLastCard = currentPage * cardsPerPage;
const indexOfFirstCard = indexOfLastCard - cardsPerPage;
- const currentCards = data.slice(indexOfFirstCard, indexOfLastCard);
+ const currentCards = safeData.slice(indexOfFirstCard, indexOfLastCard);
// Handle page change
const handlePageChange = (pageNumber: number): void => {
setCurrentPage(pageNumber);
};
+ // If no data, show a message
+ if (safeData.length === 0) {
+ return No projects available.
;
+ }
+
return (
<>
@@ -52,40 +60,46 @@ export default function Pagination({
{/* Pagination Controls */}
-
+ {totalPages > 1 && (
+ <>
+
- {/* Page Numbers */}
- {[...Array(totalPages)].map((_, index) => (
-
- ))}
+ {/* Page Numbers */}
+ {Array.from({ length: totalPages }, (_, index) => (
+
+ ))}
-
+
+ >
+ )}
>
);
diff --git a/src/app/Component/SimilarProjects.tsx b/src/app/Component/SimilarProjects.tsx
index f644a8b..70e4744 100644
--- a/src/app/Component/SimilarProjects.tsx
+++ b/src/app/Component/SimilarProjects.tsx
@@ -1,13 +1,51 @@
import Image from "next/image";
-import data from "../../data.json";
import Link from "next/link";
interface SimilarProjectsProps {
currentProjectId: string; // or number, depending on the type of `id`
}
-export default function SimilarProjects({
+
+interface Project {
+ id: string;
+ projectname: string;
+ projecturl: string;
+ imageurl: string;
+ subimageurl: string;
+ description: string;
+ about: string;
+ createdAt: string;
+ published: boolean;
+ updatedAt: string;
+}
+
+interface ApiResponse {
+ projects: Project[];
+ result: number;
+ status: string;
+}
+
+async function getProject(): Promise
{
+ try {
+ const response = await fetch(`${process.env.API_URL}/api/projects`, {
+ cache: "no-cache",
+ });
+
+ if (!response.ok) {
+ throw Error("Failed to fetch projects!!");
+ }
+ const data: ApiResponse = await response.json();
+ return data.projects || [];
+ } catch (error) {
+ console.error("Error fetching projects: ", error);
+ return [];
+ }
+}
+
+export default async function SimilarProjects({
currentProjectId,
}: SimilarProjectsProps) {
+ const data = await getProject();
+ console.log(data, "data form pagination");
return (
<>
{
@@ -20,18 +58,19 @@ export default function SimilarProjects({
href={`/projects/${project.id}`}
key={project.id}
className="flex py-3 w-60 items-center gap-2 h-24 hover:text-[#1A80E5] group"
+ data-testid="similar-projects"
>
-
{project.projectName}
+
{project.projectname}
Vice Studio
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 4b3db62..33e597b 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,6 +1,5 @@
import { Metadata } from "next";
-import data from "../data.json";
-import Pagination from "./Component/Pagination"; // New client component
+import Pagination from "./Component/Pagination";
export const metadata: Metadata = {
title: "Cardano API",
@@ -21,14 +20,62 @@ export const metadata: Metadata = {
},
};
-export default function Home() {
+interface Project {
+ id: string;
+ projectname: string;
+ projecturl: string;
+ imageurl: string;
+ subimageurl: string;
+ description: string;
+ about: string;
+ createdAt: string;
+ published: boolean | null;
+ updatedAt: string;
+}
+
+interface ApiResponse {
+ projects: Project[];
+ results: number;
+ status: string;
+}
+
+async function getProjects(): Promise
{
+ try {
+ const response = await fetch(`${process.env.API_URL}/api/projects`, {
+ cache: "no-store",
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch projects!");
+ }
+
+ const data: ApiResponse = await response.json();
+ return data.projects || [];
+ } catch (error) {
+ console.error("Error fetching projects: ", error, process.env.API_URL);
+ return [];
+ }
+}
+
+export default async function Home() {
+ const projects = await getProjects();
const cardsPerPage = 8;
- const totalPages = Math.ceil(data.length / cardsPerPage);
+ const totalPages = Math.max(1, Math.ceil(projects.length / cardsPerPage));
+
+ // Map the API data structure to match what the Pagination component expects
+ const mappedProjects = projects.map((project) => ({
+ id: project.id,
+ projectName: project.projectname,
+ url: project.projecturl,
+ imageUrl: project.imageurl,
+ subImageUrl: project.subimageurl,
+ description: project.description,
+ }));
return (
diff --git a/src/app/projects/[id]/page.tsx b/src/app/projects/[id]/page.tsx
index 7cb7dff..437f59c 100644
--- a/src/app/projects/[id]/page.tsx
+++ b/src/app/projects/[id]/page.tsx
@@ -1,33 +1,69 @@
import Image from "next/image";
-import data from "../../../data.json";
import SimilarProjects from "@/app/Component/SimilarProjects";
import Link from "next/link";
import { notFound } from "next/navigation";
+interface Project {
+ id: string;
+ projectname: string;
+ projecturl: string;
+ imageurl: string;
+ subimageurl: string;
+ description: string;
+ about: string;
+ createdAt: string;
+ published: boolean | null;
+ updatedAt: string;
+}
+
+interface ApiResponse {
+ projects: Project[];
+ results: number;
+ status: string;
+}
+
type Props = {
params: Promise<{ id: string }>;
};
+async function getProject(id: string): Promise
{
+ try {
+ const response = await fetch(`${process.env.API_URL}/api/projects`, {
+ cache: "no-store",
+ });
+
+ if (!response.ok) {
+ throw new Error("Failed to fetch projects!");
+ }
+
+ const data: ApiResponse = await response.json();
+ return data.projects.find((project) => project.id === id) || null;
+ } catch (error) {
+ console.error("Error fetching project:", error);
+ return null;
+ }
+}
+
export async function generateMetadata({ params }: Props) {
const { id } = await params;
if (!id) {
return {};
}
- const project = data.find((project) => project.id === id);
+ const project = await getProject(id);
if (!project) {
return {};
}
- const projectName = project.projectName || "Cardano API";
+ const projectName = project.projectname || "Cardano API";
const projectDescription =
project.description || "A List of Cardano API Projects";
return {
title: projectName,
description: projectDescription,
- metadataBase: new URL('https://cardanoapi.io'),
+ metadataBase: new URL("https://cardanoapi.io"),
openGraph: {
title: projectName,
description: projectDescription,
@@ -45,18 +81,13 @@ export async function generateMetadata({ params }: Props) {
}
const ProjectPage = async ({ params }: Props) => {
- // Ensure `params` has an id
-
const { id } = await params;
- console.log(id);
if (!id) {
return Project not found!
;
}
- // Fetch the project data based on the `id` parameter
- const project = data.find((project) => project.id === id);
+ const project = await getProject(id);
- // If project is not found, trigger a 404 page
if (!project) {
notFound();
}
@@ -67,32 +98,32 @@ const ProjectPage = async ({ params }: Props) => {
Projects
{" "}
- / {project?.projectName}
+ / {project.projectname}
- {project?.projectName}
+ {project.projectname}
- {project?.description}
+ {project.description}
-
+
-
+
@@ -134,10 +165,7 @@ const ProjectPage = async ({ params }: Props) => {
About this app →
- Lorem ipsum dolor sit amet consectetur adipisicing elit. Placeat
- nobis cumque expedita necessitatibus consectetur accusantium ullam
- explicabo non dolore voluptatem fasdlkfjsdaf dsfdsafaljfdas;df
- fasdfsadf.
+ {project.about}