From 37b5de4779d518b2b1e74ed57752f58d55081aca Mon Sep 17 00:00:00 2001 From: itschip Date: Thu, 27 Mar 2025 13:01:11 +0100 Subject: [PATCH 01/96] feat(ci): release drafter --- .github/release-drafter.yml | 12 ++++++++++++ .github/workflows/prerelease.yml | 33 ++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) create mode 100644 .github/release-drafter.yml create mode 100644 .github/workflows/prerelease.yml diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml new file mode 100644 index 0000000..3757cb5 --- /dev/null +++ b/.github/release-drafter.yml @@ -0,0 +1,12 @@ +name-template: 'v$RESOLVED_VERSION' +tag-template: 'v$RESOLVED_VERSION' +template: | + # What's Changed + + $CHANGES + + **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION + + +prerelease-identifier: 'beta' +publish: true diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 0000000..ec16a48 --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,33 @@ +name: Pre Release + +on: + push: + # branches to consider in the event; optional, defaults to all + branches: + - develop + +permissions: + contents: read + +jobs: + update_release_draft: + permissions: + contents: write + pull-requests: write + runs-on: ubuntu-latest + steps: + # (Optional) GitHub Enterprise requires GHE_HOST variable set + #- name: Set GHE_HOST + # run: | + # echo "GHE_HOST=${GITHUB_SERVER_URL##https:\/\/}" >> $GITHUB_ENV + + # Drafts your next Release notes as Pull Requests are merged into "master" + - uses: release-drafter/release-drafter@v6 + # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml + with: + config-name: my-config.yml + prerelease: true + publish: true + prerelease-identifier: 'beta' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From fe4ee9e69d7a3b1715b25b975d7f881ddb7e5f03 Mon Sep 17 00:00:00 2001 From: itschip Date: Thu, 27 Mar 2025 13:01:37 +0100 Subject: [PATCH 02/96] feat(app): wip organization --- api/organization.go | 10 +++ internal/database/database.go | 8 +-- .../query/organization/organization_query.go | 39 ++++++++++ .../http/internalapi/organization_group.go | 32 +++++++++ .../organization/organization_service.go | 71 +++++++++++++++++++ web/src/App.tsx | 11 ++- .../features/auth/routes/ProtectedRoute.tsx | 19 ++++- .../organizations/api/useOrganizations.ts | 20 ++++++ web/src/typings/organizations.ts | 4 ++ 9 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 api/organization.go create mode 100644 internal/database/query/organization/organization_query.go create mode 100644 internal/http/internalapi/organization_group.go create mode 100644 internal/service/organization/organization_service.go create mode 100644 web/src/features/organizations/api/useOrganizations.ts create mode 100644 web/src/typings/organizations.ts diff --git a/api/organization.go b/api/organization.go new file mode 100644 index 0000000..aa09b2a --- /dev/null +++ b/api/organization.go @@ -0,0 +1,10 @@ +package api + +type Organization struct { + ID int64 `json:"id"` + Name string `json:"name"` +} + +type CreateOrganizationRequest struct { + Name string `json:"name"` +} diff --git a/internal/database/database.go b/internal/database/database.go index ba732c5..b385e23 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -1,7 +1,6 @@ package database import ( - "fmt" "time" "github.com/uptrace/bun" @@ -28,17 +27,18 @@ type Session struct { User *User `bun:"rel:belongs-to,join:user_id=id"` } +// not used yet type Organization struct { bun.BaseModel `bun:"table:organization"` ID int64 `bun:"id,pk,autoincrement"` - OwnerID string `bun:"owner_Id"` Name string `bun:"name"` - User *User `bun:"rel:belongs-to,join:owner_id=id"` } +// not used yet type OrganizationMember struct { bun.BaseModel `bun:"table:organization_member"` ID int64 `bun:"id,pk,autoincrement"` + Role string `bun:"role"` UserID int `bun:"user_id"` OrganizationID int `bun:"organization_id"` User *User `bun:"rel:belongs-to,join:user_id=id"` @@ -69,8 +69,6 @@ type Store interface { // TODO: Return error if driver is not supported func New(driver string, dsn string) Store { - fmt.Println("option", driver) - switch driver { case "mysql": return &MySQL{} diff --git a/internal/database/query/organization/organization_query.go b/internal/database/query/organization/organization_query.go new file mode 100644 index 0000000..5338823 --- /dev/null +++ b/internal/database/query/organization/organization_query.go @@ -0,0 +1,39 @@ +package organizationquery + +import ( + "context" + + "github.com/fivemanage/lite/internal/database" + "github.com/uptrace/bun" +) + +func Create(ctx context.Context, db *bun.DB, organization *database.Organization) (bun.Tx, error) { + tx, err := db.BeginTx(ctx, nil) + _, err = tx.NewInsert().Model(organization).Exec(ctx) + if err != nil { + return tx, err + } + + return tx, nil +} + +func Find(ctx context.Context, db *bun.DB, id int64) (*database.Organization, error) { + organization := new(database.Organization) + err := db.NewSelect().Model(organization).Where("id = ?", id).Scan(ctx) + if err != nil { + return nil, err + } + + return organization, nil +} + +func List(ctx context.Context, db *bun.DB) ([]database.Organization, error) { + var organizations []database.Organization + + err := db.NewSelect().Model(&organizations).Scan(ctx) + if err != nil { + return nil, err + } + + return organizations, nil +} diff --git a/internal/http/internalapi/organization_group.go b/internal/http/internalapi/organization_group.go new file mode 100644 index 0000000..1efd3a8 --- /dev/null +++ b/internal/http/internalapi/organization_group.go @@ -0,0 +1,32 @@ +package internalapi + +import ( + "github.com/fivemanage/lite/api" + "github.com/fivemanage/lite/internal/service/organization" + "github.com/labstack/echo/v4" +) + +func registerOrganizationApi(group *echo.Group, organizationService *organization.Service) { + group.POST("/organization", func(c echo.Context) error { + var data api.CreateOrganizationRequest + if err := c.Bind(&data); err != nil { + return echo.NewHTTPError(400, err) + } + + organization, err := organizationService.CreateOrganization(c.Request().Context(), &data) + if err != nil { + return echo.NewHTTPError(500, err) + } + + return c.JSON(200, organization) + }) + + group.GET("/organization", func(c echo.Context) error { + organizations, err := organizationService.ListOrganizations(c.Request().Context()) + if err != nil { + return echo.NewHTTPError(500, err) + } + + return c.JSON(200, organizations) + }) +} diff --git a/internal/service/organization/organization_service.go b/internal/service/organization/organization_service.go new file mode 100644 index 0000000..896ee9a --- /dev/null +++ b/internal/service/organization/organization_service.go @@ -0,0 +1,71 @@ +package organization + +import ( + "context" + + "github.com/fivemanage/lite/api" + "github.com/fivemanage/lite/internal/database" + organizationquery "github.com/fivemanage/lite/internal/database/query/organization" + "github.com/uptrace/bun" +) + +type Service struct { + db *bun.DB +} + +func NewService(db *bun.DB) *Service { + return &Service{ + db: db, + } +} + +func (r *Service) CreateOrganization(ctx context.Context, data *api.CreateOrganizationRequest) (*api.Organization, error) { + dbOrganization := &database.Organization{ + Name: data.Name, + } + + _, err := organizationquery.Create(ctx, r.db, dbOrganization) + if err != nil { + return nil, err + } + + organization := &api.Organization{ + ID: dbOrganization.ID, + Name: dbOrganization.Name, + } + + return organization, nil +} + +func (r *Service) FindOrganizationByID(ctx context.Context, id int64) (*api.Organization, error) { + dbOrganization, err := organizationquery.Find(ctx, r.db, id) + if err != nil { + return nil, err + } + + organization := &api.Organization{ + ID: dbOrganization.ID, + Name: dbOrganization.Name, + } + + return organization, nil +} + +func (r *Service) ListOrganizations(ctx context.Context) ([]*api.Organization, error) { + dbOrganizations, err := organizationquery.List(ctx, r.db) + if err != nil { + return nil, err + } + + organizations := make([]*api.Organization, 0, len(dbOrganizations)) + for _, dbOrganization := range dbOrganizations { + organization := &api.Organization{ + ID: dbOrganization.ID, + Name: dbOrganization.Name, + } + + organizations = append(organizations, organization) + } + + return organizations, nil +} diff --git a/web/src/App.tsx b/web/src/App.tsx index c4537bd..9b446f5 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -5,6 +5,7 @@ import { AppDashboard } from "./features/app/routes/AppDashboard"; import { AppLayout } from "./features/app/components/AppLayout"; import { TokensRoute } from "./features/tokens/routes/TokensRoute"; import { ThemeProvider } from "./components/theme/ThemeProvider"; +import { ProtectedRoute } from "./features/auth/routes/ProtectedRoute"; const queryClient = new QueryClient(); @@ -15,10 +16,14 @@ function App() { } /> - }> - } /> + // authed routes + }> + New Workspace} /> + }> + } /> - } /> + } /> + diff --git a/web/src/features/auth/routes/ProtectedRoute.tsx b/web/src/features/auth/routes/ProtectedRoute.tsx index 9cdb8a0..d48b4f1 100644 --- a/web/src/features/auth/routes/ProtectedRoute.tsx +++ b/web/src/features/auth/routes/ProtectedRoute.tsx @@ -1,5 +1,22 @@ -import { Outlet } from "react-router"; +import { Outlet, useLocation, Navigate, useParams } from "react-router"; + +type AppParams = { + organizationId: string; +}; export const ProtectedRoute: React.FC = () => { + const location = useLocation(); + const params = useParams(); + + if (location.pathname === "/") { + return ; + } + + console.log(params.organizationId); + + if (!params.organizationId) { + return ; + } + return ; }; diff --git a/web/src/features/organizations/api/useOrganizations.ts b/web/src/features/organizations/api/useOrganizations.ts new file mode 100644 index 0000000..df57492 --- /dev/null +++ b/web/src/features/organizations/api/useOrganizations.ts @@ -0,0 +1,20 @@ +import { Organization } from "@/typings/organizations"; +import { ApiError, fetchApi } from "@/utils/http-util"; +import { useQuery } from "@tanstack/react-query"; + +export function useOrganizations() { + const { data, isLoading } = useQuery({ + queryKey: ["organizations"], + queryFn: async () => { + try { + return fetchApi("/api/organization"); + } catch (err) { + if (err instanceof ApiError) { + throw new Error(err.message); + } + } + }, + }); + + return { data, isLoading }; +} diff --git a/web/src/typings/organizations.ts b/web/src/typings/organizations.ts new file mode 100644 index 0000000..fbdf269 --- /dev/null +++ b/web/src/typings/organizations.ts @@ -0,0 +1,4 @@ +export interface Organization { + id: number; + name: string; +} From ef3a1a3593b3ee519b2eb87bb413dfb506734c2e Mon Sep 17 00:00:00 2001 From: itschip Date: Thu, 27 Mar 2025 13:01:47 +0100 Subject: [PATCH 03/96] feat(app/tokens): fix token dialog --- internal/service/token/token_service.go | 6 ++--- .../tokens/components/CreateTokenDialog.tsx | 24 +++++++++++++++---- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/internal/service/token/token_service.go b/internal/service/token/token_service.go index 78dfc34..c722370 100644 --- a/internal/service/token/token_service.go +++ b/internal/service/token/token_service.go @@ -47,9 +47,9 @@ func (r *Service) CreateToken(ctx context.Context, data *api.CreateTokenRequest) return apiToken, nil } -func (r *Service) ListTokens(ctx context.Context) ([]api.ListTokensResponse, error) { +func (r *Service) ListTokens(ctx context.Context) ([]*api.ListTokensResponse, error) { var err error - var response []api.ListTokensResponse + var response []*api.ListTokensResponse tokens, err := tokenquery.List(ctx, r.db) if err != nil { @@ -57,7 +57,7 @@ func (r *Service) ListTokens(ctx context.Context) ([]api.ListTokensResponse, err } for _, token := range tokens { - response = append(response, api.ListTokensResponse{ + response = append(response, &api.ListTokensResponse{ ID: token.ID, Identifier: token.Identifier, }) diff --git a/web/src/features/tokens/components/CreateTokenDialog.tsx b/web/src/features/tokens/components/CreateTokenDialog.tsx index 47b2994..ede569c 100644 --- a/web/src/features/tokens/components/CreateTokenDialog.tsx +++ b/web/src/features/tokens/components/CreateTokenDialog.tsx @@ -3,6 +3,7 @@ import { DialogContent, DialogFooter, DialogHeader, + DialogTitle, DialogTrigger, } from "@/components/ui/Dialog"; import { @@ -20,7 +21,6 @@ import { useState, useTransition } from "react"; import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; import { KeyRound } from "lucide-react"; -import { DialogTitle } from "@radix-ui/react-dialog"; import { useQueryClient } from "@tanstack/react-query"; import { QueryKeys } from "@/typings/query"; @@ -53,6 +53,10 @@ export function CreateTokenDialog() { reset(); } + function handleInteractOutside(e: Event) { + e.preventDefault(); + } + return ( @@ -63,14 +67,26 @@ export function CreateTokenDialog() { - Create token + + {isSuccess ? "Token created!" : "Create token"} + {isSuccess && data ? ( -
{data.token}
+
+

+ Copy the token below and store it in a safe place. +

+

+ You won't be able to see it again. +

+
{data.token}
+
) : (
From bab8f31d7519a38d72ace64e8f13de936fd12e1a Mon Sep 17 00:00:00 2001 From: itschip Date: Thu, 27 Mar 2025 13:03:05 +0100 Subject: [PATCH 04/96] fix(ci): correct release config file --- .github/workflows/prerelease.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml index ec16a48..5c97ac5 100644 --- a/.github/workflows/prerelease.yml +++ b/.github/workflows/prerelease.yml @@ -23,9 +23,8 @@ jobs: # Drafts your next Release notes as Pull Requests are merged into "master" - uses: release-drafter/release-drafter@v6 - # (Optional) specify config name to use, relative to .github/. Default: release-drafter.yml with: - config-name: my-config.yml + config-name: release-drafter.yml prerelease: true publish: true prerelease-identifier: 'beta' From 93eaad378095afd65a2798217a4838f7d8e6c35a Mon Sep 17 00:00:00 2001 From: itschip Date: Thu, 27 Mar 2025 13:21:52 +0100 Subject: [PATCH 05/96] feat(app): update go to 1.24 --- .github/release-drafter.yml | 7 +++++-- go.mod | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index 3757cb5..cd7ac57 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -1,3 +1,7 @@ +prerelease-identifier: 'beta' +prerelease: true +publish: true + name-template: 'v$RESOLVED_VERSION' tag-template: 'v$RESOLVED_VERSION' template: | @@ -8,5 +12,4 @@ template: | **Full Changelog**: https://github.com/$OWNER/$REPOSITORY/compare/$PREVIOUS_TAG...v$RESOLVED_VERSION -prerelease-identifier: 'beta' -publish: true + diff --git a/go.mod b/go.mod index 3c09db4..9a6edaf 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/fivemanage/lite -go 1.22.0 +go 1.24 require ( github.com/aws/aws-sdk-go-v2 v1.30.4 From 9c44a75cb98d2a96bb7865fa7af5f836b11a45a8 Mon Sep 17 00:00:00 2001 From: itschip Date: Thu, 27 Mar 2025 19:44:34 +0100 Subject: [PATCH 06/96] fix(app): patch CVE-2025-30204 this also updates a couple other vulnerable modules --- go.mod | 18 +++++++--------- go.sum | 66 +++++++++++++--------------------------------------------- 2 files changed, 21 insertions(+), 63 deletions(-) diff --git a/go.mod b/go.mod index 9a6edaf..c6f9517 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/go-playground/validator/v10 v10.22.1 github.com/go-sql-driver/mysql v1.8.1 github.com/joho/godotenv v1.5.1 - github.com/labstack/echo/v4 v4.11.4 + github.com/labstack/echo/v4 v4.13.3 github.com/matoous/go-nanoid/v2 v2.1.0 github.com/sirupsen/logrus v1.9.3 github.com/spf13/cobra v1.8.1 @@ -19,8 +19,8 @@ require ( github.com/uptrace/bun/dialect/mysqldialect v1.2.10 github.com/uptrace/bun/dialect/pgdialect v1.2.10 github.com/uptrace/bun/driver/pgdriver v1.2.10 - golang.org/x/crypto v0.33.0 - golang.org/x/oauth2 v0.18.0 + golang.org/x/crypto v0.36.0 + golang.org/x/oauth2 v0.28.0 ) require ( @@ -43,8 +43,6 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/golang-jwt/jwt v3.2.2+incompatible // indirect - github.com/golang/protobuf v1.5.3 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -74,12 +72,10 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 // indirect golang.org/x/mod v0.23.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.30.0 // indirect - golang.org/x/text v0.22.0 // indirect - golang.org/x/time v0.5.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/protobuf v1.33.0 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect + golang.org/x/time v0.8.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect mellium.im/sasl v0.3.2 // indirect diff --git a/go.sum b/go.sum index 9bcc796..6addc4d 100644 --- a/go.sum +++ b/go.sum @@ -57,13 +57,6 @@ github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27 github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y= github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg= -github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= -github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= @@ -78,8 +71,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8= -github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8= +github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY= +github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g= github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0= github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= @@ -156,7 +149,6 @@ github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IU github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.opentelemetry.io/otel v1.34.0 h1:zRLXxLCgL1WyKsPVrgbSdMN4c0FMkDAskSTQP+0hdUY= go.opentelemetry.io/otel v1.34.0/go.mod h1:OWFPOQ+h4G8xpyjgqo4SxJYdDQ/qmRH+wivy7zzx9oI= go.opentelemetry.io/otel/trace v1.34.0 h1:+ouXS2V8Rd4hp4580a8q23bg0azF2nI8cqLYnC8mh/k= @@ -165,54 +157,24 @@ go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= -golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/exp v0.0.0-20231108232855-2478ac86f678 h1:mchzmB1XO2pMaKFRqk/+MV3mgGG96aqaPXaMifQU47w= golang.org/x/exp v0.0.0-20231108232855-2478ac86f678/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.23.0 h1:Zb7khfcRGKk+kqfxFaP5tZqCnDZMjC5VtUBs87Hr6QM= golang.org/x/mod v0.23.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= -golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI= -golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= +golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= -golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= -golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= -golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= -golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= +golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From b3b114367bc99c8952cd6a4b2c755eb4274e8b5e Mon Sep 17 00:00:00 2001 From: itschip Date: Thu, 27 Mar 2025 23:06:11 +0100 Subject: [PATCH 07/96] feat(app): serve react app --- .dockerignore | 1 + build/package/Dockerfile | 35 +++++- cmd/lite/lite.go | 158 +++++++++++++--------------- deployments/docker-compose.test.yml | 42 ++++++++ internal/database/database.go | 4 +- internal/database/mysql.go | 4 +- internal/database/pg.go | 5 +- internal/http/server.go | 30 ++++-- migrate/migrate.go | 16 ++- migrate/migrations/main.go | 11 +- web/package.json | 2 +- web/vite.config.ts | 5 + 12 files changed, 198 insertions(+), 115 deletions(-) create mode 100644 .dockerignore create mode 100644 deployments/docker-compose.test.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..a0ae8ea --- /dev/null +++ b/.dockerignore @@ -0,0 +1 @@ +web/node_modules diff --git a/build/package/Dockerfile b/build/package/Dockerfile index a3bbfab..687a516 100644 --- a/build/package/Dockerfile +++ b/build/package/Dockerfile @@ -1,5 +1,25 @@ -# Builder stage -FROM --platform=arm64 golang:1.22-alpine3.19 as builder +# Frontend +FROM node:22-slim AS frontend + +ENV PNPM_HOME="/pnpm" +ENV PATH="$PNPM_HOME:$PATH" +RUN corepack enable + +WORKDIR /frontend + +COPY web/pnpm-lock.yaml . + +RUN pnpm fetch + +COPY web . + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile +RUN pnpm run build + + + +# Backend +FROM golang:1.24 AS backend WORKDIR /app @@ -9,13 +29,18 @@ RUN go mod download COPY . . +COPY --from=frontend /frontend/dist internal/http/dist + +ENV CGO_ENABLED=0 RUN go build -o /app/main ./cmd/lite/lite.go -# Final stage -FROM --platform=arm64 alpine:3.14 +# Application +FROM alpine:latest WORKDIR /app -COPY --from=builder /app/main /app/main +COPY --from=backend /app/main /app/main + +EXPOSE 8080 CMD ["/app/main"] diff --git a/cmd/lite/lite.go b/cmd/lite/lite.go index 5a7604b..8e9e9d6 100644 --- a/cmd/lite/lite.go +++ b/cmd/lite/lite.go @@ -23,95 +23,87 @@ import ( "github.com/spf13/viper" ) -var ( - rootCmd = &cobra.Command{ - Use: "fivemanage", - Short: "Open-source, easy-to-use gaming-community management service.", - } - - // todo: otel & promhttp - - // this whole code can probably go into a 'run.go' file. - // just to not fill this file with shit - // its gonna be ugly anyways tho - runCmd = &cobra.Command{ - Use: "run", - Short: "Run fivemanage application", - Run: func(cmd *cobra.Command, args []string) { - var err error - - port := viper.GetInt("port") - driver := viper.GetString("driver") - - err = godotenv.Load() - // TODO: Only for development - if err != nil { - log.Fatal("Error loading .env file. Probably becasue we're in production") - } - - db := database.New(driver, "") - store := db.Connect() - migrate.AutoMigrate(cmd.Context(), store) - - storageLayer := storage.New("s3") - - authservice := auth.New(store) - tokenservice := token.NewService(store) - fileservice := file.NewService(store, storageLayer) - - server := http.NewServer( - authservice, - tokenservice, - fileservice, - ) - - // todo: check if we have an admin user - // if not, create an admin user with the ADMIN_PASSWORD ENV - err = authservice.CreateAdminUser() - if err != nil { - logrus.WithError(err).Error("Failed to create admin user") - return - } - - srv := &nethttp.Server{ - Addr: fmt.Sprintf("localhost:%d", port), - Handler: server, - } - - go func() { - fmt.Printf("Server is running on port %d...\n", port) - if err := srv.ListenAndServe(); err != nil && err != nethttp.ErrServerClosed { - log.Fatalf("listen: %s\n", err) - } - }() - - quit := make(chan os.Signal, 1) - signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - <-quit - log.Println("Shutdown Server ...") - - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := srv.Shutdown(ctx); err != nil { - log.Fatal("Server Shutdown:", err) +var rootCmd = &cobra.Command{ + Use: "fivemanage", + Short: "Open-source, easy-to-use gaming-community management service.", + Run: func(cmd *cobra.Command, args []string) { + var err error + + port := viper.GetInt("port") + driver := viper.GetString("driver") + dsn := viper.GetString("dsn") + + err = godotenv.Load() + // TODO: Only for development + if err != nil { + log.Println("Error loading .env file. Probably becasue we're in production") + } + + db := database.New(driver) + store := db.Connect(dsn) + migrate.AutoMigrate(cmd.Context(), store) + + storageLayer := storage.New("s3") + + authservice := auth.New(store) + tokenservice := token.NewService(store) + fileservice := file.NewService(store, storageLayer) + + server := http.NewServer( + authservice, + tokenservice, + fileservice, + ) + + // todo: check if we have an admin user + // if not, create an admin user with the ADMIN_PASSWORD ENV + err = authservice.CreateAdminUser() + if err != nil { + logrus.WithError(err).Error("Failed to create admin user") + return + } + + srv := &nethttp.Server{ + Addr: fmt.Sprintf(":%d", port), + Handler: server, + } + + go func() { + fmt.Printf("Server is running on port %d...\n", port) + if err := srv.ListenAndServe(); err != nil && err != nethttp.ErrServerClosed { + log.Fatalf("listen: %s\n", err) } - select { - case <-ctx.Done(): - log.Println("timeout of 5 seconds.") - } - log.Println("Server exiting") - }, - } -) + }() + + quit := make(chan os.Signal, 1) + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("Shutdown Server ...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Fatal("Server Shutdown:", err) + } + select { + case <-ctx.Done(): + log.Println("timeout of 5 seconds.") + } + log.Println("Server exiting") + }, +} func init() { rootCmd.PersistentFlags().String("driver", "mysql", "Database driver") - runCmd.Flags().Int("port", 8080, "Port to serve Fivemanage") + rootCmd.Flags().Int("port", 8080, "Port to serve Fivemanage") + rootCmd.Flags().String("dsn", "", "Database DSN") viper.BindPFlag("driver", rootCmd.PersistentFlags().Lookup("driver")) - viper.BindPFlag("port", runCmd.Flags().Lookup("port")) - - rootCmd.AddCommand(runCmd) + viper.BindPFlag("port", rootCmd.Flags().Lookup("port")) + viper.BindPFlag("dsn", rootCmd.Flags().Lookup("dsn")) + viper.BindEnv("driver", "DB_DRIVER") + viper.BindEnv("port", "PORT") + viper.BindEnv("dsn", "DSN") rootCmd.AddCommand(migrate.RootCmd) migrate.RootCmd.AddCommand( diff --git a/deployments/docker-compose.test.yml b/deployments/docker-compose.test.yml new file mode 100644 index 0000000..60a07eb --- /dev/null +++ b/deployments/docker-compose.test.yml @@ -0,0 +1,42 @@ +name: "Fivemanage Lite Test" + +services: + + database-test: + image: mysql:8 + container_name: lite_database_test + environment: + MYSQL_ROOT_PASSWORD: root + MYSQL_DATABASE: fivemanage-lite-dev + ports: + - "3306:3306" + volumes: + - ./.docker/mysql_data:/var/lib/mysql + networks: + - fivemanage + jaeger-test: + image: jaegertracing/jaeger:2.4.0 + container_name: jaeger_test + ports: + - "16686:16686" + - "4317:4317" + - "4318:4318" + - "5778:5778" + - "9411:9411" + lite: + image: fivemanage/lite:latest + container_name: lite + environment: + - DB_DRIVER=mysql + - PORT=8080 + - DSN=root:root@tcp(database-test:3306)/fivemanage-lite-dev + ports: + - "8080:8080" + depends_on: + - database-test + networks: + - fivemanage + +networks: + fivemanage: + driver: bridge diff --git a/internal/database/database.go b/internal/database/database.go index b385e23..7261a4f 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -64,11 +64,11 @@ type File struct { } type Store interface { - Connect() *bun.DB + Connect(dsn string) *bun.DB } // TODO: Return error if driver is not supported -func New(driver string, dsn string) Store { +func New(driver string) Store { switch driver { case "mysql": return &MySQL{} diff --git a/internal/database/mysql.go b/internal/database/mysql.go index 09a4a13..b9a72d3 100644 --- a/internal/database/mysql.go +++ b/internal/database/mysql.go @@ -11,8 +11,8 @@ import ( type MySQL struct{} -func (r *MySQL) Connect() *bun.DB { - sqldb, err := sql.Open("mysql", "root:root@tcp(localhost)/fivemanage-lite-dev") +func (r *MySQL) Connect(dsn string) *bun.DB { + sqldb, err := sql.Open("mysql", dsn) if err != nil { log.Fatalf("failed to open mysql connection: %v", err) } diff --git a/internal/database/pg.go b/internal/database/pg.go index 998c5fb..e3f370a 100644 --- a/internal/database/pg.go +++ b/internal/database/pg.go @@ -10,10 +10,7 @@ import ( type PostgreSQL struct{} -func (r *PostgreSQL) Connect() *bun.DB { - dsn := "postgres://postgres:@localhost:5432/test?sslmode=disable" - // dsn := "unix://user:pass@dbname/var/run/postgresql/.s.PGSQL.5432" +func (r *PostgreSQL) Connect(dsn string) *bun.DB { sqldb := sql.OpenDB(pgdriver.NewConnector(pgdriver.WithDSN(dsn))) - return bun.NewDB(sqldb, pgdialect.New()) } diff --git a/internal/http/server.go b/internal/http/server.go index 4cac116..ec98d5b 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -2,7 +2,9 @@ package http import ( "embed" - nethttp "net/http" + "fmt" + "io/fs" + "net/http" "strings" "github.com/fivemanage/lite/internal/http/internalapi" @@ -17,11 +19,7 @@ import ( "github.com/labstack/echo/v4/middleware" ) -// This embed shit shouldn't be this far away. -// When we are ready, we can either build the React code directly into the server or root folder -// or just copy it in the Dockerfile. - -// go:embed ../../../web/dist +//go:embed dist/* var webContent embed.FS type Server struct { @@ -35,17 +33,18 @@ func NewServer( fileservice *file.Service, ) *echo.Echo { app := echo.New() + app.Debug = true // not good, not bad app.Validator = &_validator.CustomValidator{Validator: validator.New()} app.Use(middleware.Recover()) + app.Use(middleware.Logger()) app.Use(middleware.StaticWithConfig(middleware.StaticConfig{ - Root: "web/dist", - Filesystem: nethttp.FS(webContent), + Filesystem: getFileSystem("dist"), HTML5: true, Skipper: func(c echo.Context) bool { - return strings.HasPrefix(c.Request().URL.Path, "/api") + return strings.HasPrefix(c.Path(), "/api") }, })) @@ -58,7 +57,16 @@ func NewServer( ) publicapi.Add(apiGroup, fileservice) - // app.imageRouterGroup(apiGroup) - return app } + +func getFileSystem(path string) http.FileSystem { + fs, err := fs.Sub(webContent, path) + if err != nil { + panic(err) + } + + fmt.Println("Serving static files from", path) + + return http.FS(fs) +} diff --git a/migrate/migrate.go b/migrate/migrate.go index c8575b8..cdaeab4 100644 --- a/migrate/migrate.go +++ b/migrate/migrate.go @@ -24,7 +24,8 @@ var ( Short: "Create migration tables", Run: func(cmd *cobra.Command, args []string) { driver := viper.GetString("driver") - db := database.New(driver, "").Connect() + dsn := viper.GetString("dsn") + db := database.New(driver).Connect(dsn) migrator := migrate.NewMigrator(db, migrations.Migrations) err := migrator.Init(cmd.Context()) @@ -39,7 +40,9 @@ var ( Short: "Migrate database", Run: func(cmd *cobra.Command, args []string) { driver := viper.GetString("driver") - db := database.New(driver, "").Connect() + dsn := viper.GetString("dsn") + + db := database.New(driver).Connect(dsn) migrator := migrate.NewMigrator(db, migrations.Migrations) if err := migrator.Lock(cmd.Context()); err != nil { @@ -62,7 +65,8 @@ var ( Use: "unlock", Run: func(cmd *cobra.Command, args []string) { driver := viper.GetString("driver") - db := database.New(driver, "").Connect() + dsn := viper.GetString("dsn") + db := database.New(driver).Connect(dsn) migrator := migrate.NewMigrator(db, migrations.Migrations) err := migrator.Unlock(cmd.Context()) @@ -76,7 +80,8 @@ var ( Use: "lock", Run: func(cmd *cobra.Command, args []string) { driver := viper.GetString("driver") - db := database.New(driver, "").Connect() + dsn := viper.GetString("dsn") + db := database.New(driver).Connect(dsn) migrator := migrate.NewMigrator(db, migrations.Migrations) err := migrator.Lock(cmd.Context()) @@ -91,7 +96,8 @@ var ( Short: "Create database migration", Run: func(cmd *cobra.Command, args []string) { driver := viper.GetString("driver") - db := database.New(driver, "").Connect() + dsn := viper.GetString("dsn") + db := database.New(driver).Connect(dsn) migrator := migrate.NewMigrator(db, migrations.Migrations) diff --git a/migrate/migrations/main.go b/migrate/migrations/main.go index 781c88d..d746dd2 100644 --- a/migrate/migrations/main.go +++ b/migrate/migrations/main.go @@ -1,11 +1,18 @@ package migrations -import "github.com/uptrace/bun/migrate" +import ( + "embed" + + "github.com/uptrace/bun/migrate" +) + +//go:embed *.go +var sqlMigrations embed.FS var Migrations = migrate.NewMigrations() func init() { - if err := Migrations.DiscoverCaller(); err != nil { + if err := Migrations.Discover(sqlMigrations); err != nil { panic(err) } } diff --git a/web/package.json b/web/package.json index 9a3dd6d..1f61c8e 100644 --- a/web/package.json +++ b/web/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build", + "build": "vite build", "build:dev": "vite build --watch", "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview" diff --git a/web/vite.config.ts b/web/vite.config.ts index fbfeca7..ac9b282 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -5,6 +5,11 @@ import path from "path"; // https://vitejs.dev/config/ export default defineConfig({ plugins: [react()], + build: { + emptyOutDir: true, + assetsDir: "", + }, + base: "/", resolve: { alias: { "@": path.resolve(__dirname, "./src"), From fd147ed1df099d6468140d969d95818131155a75 Mon Sep 17 00:00:00 2001 From: itschip Date: Thu, 27 Mar 2025 23:10:59 +0100 Subject: [PATCH 08/96] ci: remove template-name --- .github/release-drafter.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/release-drafter.yml b/.github/release-drafter.yml index cd7ac57..881c125 100644 --- a/.github/release-drafter.yml +++ b/.github/release-drafter.yml @@ -2,8 +2,6 @@ prerelease-identifier: 'beta' prerelease: true publish: true -name-template: 'v$RESOLVED_VERSION' -tag-template: 'v$RESOLVED_VERSION' template: | # What's Changed From f2716d9ffb1eef896101bb673a8f50fd67d6e1ff Mon Sep 17 00:00:00 2001 From: itschip Date: Mon, 31 Mar 2025 23:12:32 +0200 Subject: [PATCH 09/96] feat(web/organization): new org route --- .air.toml | 2 +- cmd/lite/lite.go | 15 +++++--- internal/http/internalapi/internal_api.go | 8 ++-- .../http/internalapi/organization_group.go | 3 +- internal/http/publicapi/media_group.go | 10 ++--- internal/http/server.go | 5 ++- internal/service/file/file_service.go | 7 ++-- ...0250331204401_create_organization_table.go | 25 +++++++++++++ web/src/App.tsx | 19 ++++++---- .../features/auth/routes/ProtectedRoute.tsx | 13 +++---- .../routes/NewOrganizationRoute.tsx | 37 +++++++++++++++++++ 11 files changed, 111 insertions(+), 33 deletions(-) create mode 100644 migrate/migrations/20250331204401_create_organization_table.go create mode 100644 web/src/features/organizations/routes/NewOrganizationRoute.tsx diff --git a/.air.toml b/.air.toml index 2186020..ccc623f 100644 --- a/.air.toml +++ b/.air.toml @@ -3,7 +3,7 @@ testdata_dir = "testdata" tmp_dir = "tmp" [build] - args_bin = ["run"] + args_bin = [] bin = "./tmp/main" cmd = "go build -o ./tmp/main ./cmd/lite/lite.go" delay = 0 diff --git a/cmd/lite/lite.go b/cmd/lite/lite.go index 8e9e9d6..ce26ca0 100644 --- a/cmd/lite/lite.go +++ b/cmd/lite/lite.go @@ -14,6 +14,7 @@ import ( "github.com/fivemanage/lite/internal/http" "github.com/fivemanage/lite/internal/service/auth" "github.com/fivemanage/lite/internal/service/file" + "github.com/fivemanage/lite/internal/service/organization" "github.com/fivemanage/lite/internal/service/token" "github.com/fivemanage/lite/internal/storage" "github.com/fivemanage/lite/migrate" @@ -33,11 +34,7 @@ var rootCmd = &cobra.Command{ driver := viper.GetString("driver") dsn := viper.GetString("dsn") - err = godotenv.Load() - // TODO: Only for development - if err != nil { - log.Println("Error loading .env file. Probably becasue we're in production") - } + fmt.Println("Starting Fivemanage...") db := database.New(driver) store := db.Connect(dsn) @@ -48,11 +45,13 @@ var rootCmd = &cobra.Command{ authservice := auth.New(store) tokenservice := token.NewService(store) fileservice := file.NewService(store, storageLayer) + organizationservice := organization.NewService(store) server := http.NewServer( authservice, tokenservice, fileservice, + organizationservice, ) // todo: check if we have an admin user @@ -94,6 +93,12 @@ var rootCmd = &cobra.Command{ } func init() { + err := godotenv.Load() + // TODO: Only for development + if err != nil { + log.Println("Error loading .env file. Probably becasue we're in production") + } + rootCmd.PersistentFlags().String("driver", "mysql", "Database driver") rootCmd.Flags().Int("port", 8080, "Port to serve Fivemanage") rootCmd.Flags().String("dsn", "", "Database DSN") diff --git a/internal/http/internalapi/internal_api.go b/internal/http/internalapi/internal_api.go index e1f17c7..3316f07 100644 --- a/internal/http/internalapi/internal_api.go +++ b/internal/http/internalapi/internal_api.go @@ -2,11 +2,13 @@ package internalapi import ( "github.com/fivemanage/lite/internal/service/auth" + "github.com/fivemanage/lite/internal/service/organization" "github.com/fivemanage/lite/internal/service/token" "github.com/labstack/echo/v4" ) -func Add(group *echo.Group, authservice *auth.Auth, tokenservice *token.Service) { - registerAuthApi(group, authservice) - registerTokensApi(group, tokenservice) +func Add(group *echo.Group, authService *auth.Auth, tokenService *token.Service, organizationService *organization.Service) { + registerAuthApi(group, authService) + registerTokensApi(group, tokenService) + registerOrganizationApi(group, organizationService) } diff --git a/internal/http/internalapi/organization_group.go b/internal/http/internalapi/organization_group.go index 1efd3a8..7f55c09 100644 --- a/internal/http/internalapi/organization_group.go +++ b/internal/http/internalapi/organization_group.go @@ -2,6 +2,7 @@ package internalapi import ( "github.com/fivemanage/lite/api" + "github.com/fivemanage/lite/internal/http/httputil" "github.com/fivemanage/lite/internal/service/organization" "github.com/labstack/echo/v4" ) @@ -27,6 +28,6 @@ func registerOrganizationApi(group *echo.Group, organizationService *organizatio return echo.NewHTTPError(500, err) } - return c.JSON(200, organizations) + return c.JSON(200, httputil.Response(organizations)) }) } diff --git a/internal/http/publicapi/media_group.go b/internal/http/publicapi/media_group.go index 639a800..4200523 100644 --- a/internal/http/publicapi/media_group.go +++ b/internal/http/publicapi/media_group.go @@ -21,7 +21,7 @@ func registerMediaApi(group *echo.Group, fileService *file.Service) { }) } - err = fileService.CreateFile(ctx, "image", file, header) + err = fileService.CreateFile(ctx, "", "image", file, header) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err) } @@ -33,14 +33,14 @@ func registerMediaApi(group *echo.Group, fileService *file.Service) { var err error ctx := c.Request().Context() - file, header, err := httputil.File(c.Request(), "image") + file, header, err := httputil.File(c.Request(), "video") if err != nil { return c.JSON(http.StatusInternalServerError, echo.Map{ "error": err.Error(), }) } - err = fileService.CreateFile(ctx, "video", file, header) + err = fileService.CreateFile(ctx, "", "video", file, header) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err) } @@ -52,14 +52,14 @@ func registerMediaApi(group *echo.Group, fileService *file.Service) { var err error ctx := c.Request().Context() - file, header, err := httputil.File(c.Request(), "image") + file, header, err := httputil.File(c.Request(), "audio") if err != nil { return c.JSON(http.StatusInternalServerError, echo.Map{ "error": err.Error(), }) } - err = fileService.CreateFile(ctx, "audio", file, header) + err = fileService.CreateFile(ctx, "", "audio", file, header) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err) } diff --git a/internal/http/server.go b/internal/http/server.go index ec98d5b..367448b 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -12,6 +12,7 @@ import ( _validator "github.com/fivemanage/lite/internal/http/validator" "github.com/fivemanage/lite/internal/service/auth" "github.com/fivemanage/lite/internal/service/file" + "github.com/fivemanage/lite/internal/service/organization" "github.com/fivemanage/lite/internal/service/token" "github.com/labstack/echo/v4" @@ -31,6 +32,7 @@ func NewServer( authservice *auth.Auth, tokenservice *token.Service, fileservice *file.Service, + organizationService *organization.Service, ) *echo.Echo { app := echo.New() app.Debug = true @@ -44,7 +46,7 @@ func NewServer( Filesystem: getFileSystem("dist"), HTML5: true, Skipper: func(c echo.Context) bool { - return strings.HasPrefix(c.Path(), "/api") + return strings.HasPrefix(c.Request().URL.Path, "/api") }, })) @@ -54,6 +56,7 @@ func NewServer( apiGroup, authservice, tokenservice, + organizationService, ) publicapi.Add(apiGroup, fileservice) diff --git a/internal/service/file/file_service.go b/internal/service/file/file_service.go index 994150c..bc7145f 100644 --- a/internal/service/file/file_service.go +++ b/internal/service/file/file_service.go @@ -27,12 +27,13 @@ func NewService(db *bun.DB, storageLayer storage.StorageLayer) *Service { func (s *Service) CreateFile( ctx context.Context, + organizationID string, fileType string, file multipart.File, fileHeader *multipart.FileHeader, ) error { var err error - key, contentType, err := generateFileKey(fileType, file, fileHeader) + key, contentType, err := generateFileKey(organizationID, fileType, file, fileHeader) if err != nil { return err } @@ -60,7 +61,7 @@ func (s *Service) CreateFile( return nil } -func generateFileKey(fileType string, file multipart.File, fileHeader *multipart.FileHeader) (string, string, error) { +func generateFileKey(organizationID, fileType string, file multipart.File, fileHeader *multipart.FileHeader) (string, string, error) { filename, err := crypt.GenerateFilename() if err != nil { return "", "", err @@ -73,7 +74,7 @@ func generateFileKey(fileType string, file multipart.File, fileHeader *multipart } mime := mimetype.Detect(buf) - key := fmt.Sprintf("%s/%s.%s", fileType, filename, mime.Extension()) + key := fmt.Sprintf("%s/%s.%s", organizationID, filename, mime.Extension()) return key, mime.String(), nil } diff --git a/migrate/migrations/20250331204401_create_organization_table.go b/migrate/migrations/20250331204401_create_organization_table.go new file mode 100644 index 0000000..f913165 --- /dev/null +++ b/migrate/migrations/20250331204401_create_organization_table.go @@ -0,0 +1,25 @@ +package migrations + +import ( + "context" + "fmt" + + "github.com/fivemanage/lite/internal/database" + "github.com/uptrace/bun" +) + +func init() { + Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + fmt.Print(" [up migration] ") + db.RegisterModel((*database.Organization)(nil)) + _, err := db.NewCreateTable().Model((*database.Organization)(nil)).Exec(ctx) + if err != nil { + return err + } + + return nil + }, func(ctx context.Context, db *bun.DB) error { + fmt.Print(" [down migration] ") + return nil + }) +} diff --git a/web/src/App.tsx b/web/src/App.tsx index 9b446f5..2029f37 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -6,6 +6,7 @@ import { AppLayout } from "./features/app/components/AppLayout"; import { TokensRoute } from "./features/tokens/routes/TokensRoute"; import { ThemeProvider } from "./components/theme/ThemeProvider"; import { ProtectedRoute } from "./features/auth/routes/ProtectedRoute"; +import { NewOrganizationRoute } from "./features/organizations/routes/NewOrganizationRoute"; const queryClient = new QueryClient(); @@ -15,14 +16,18 @@ function App() { + 404} /> } /> - // authed routes - }> - New Workspace} /> - }> - } /> - - } /> + + } + /> + }> + }> + } /> + } /> + diff --git a/web/src/features/auth/routes/ProtectedRoute.tsx b/web/src/features/auth/routes/ProtectedRoute.tsx index d48b4f1..1f432bb 100644 --- a/web/src/features/auth/routes/ProtectedRoute.tsx +++ b/web/src/features/auth/routes/ProtectedRoute.tsx @@ -1,21 +1,20 @@ -import { Outlet, useLocation, Navigate, useParams } from "react-router"; +import { useOrganizations } from "@/features/organizations/api/useOrganizations"; +import { Outlet, Navigate, useParams } from "react-router"; type AppParams = { organizationId: string; }; export const ProtectedRoute: React.FC = () => { - const location = useLocation(); const params = useParams(); - if (location.pathname === "/") { - return ; + const { data: organizations } = useOrganizations(); + if (organizations && organizations.length === 0) { + return ; } - console.log(params.organizationId); - if (!params.organizationId) { - return ; + return ; } return ; diff --git a/web/src/features/organizations/routes/NewOrganizationRoute.tsx b/web/src/features/organizations/routes/NewOrganizationRoute.tsx new file mode 100644 index 0000000..5adffb2 --- /dev/null +++ b/web/src/features/organizations/routes/NewOrganizationRoute.tsx @@ -0,0 +1,37 @@ +import { Button } from "@/components/ui/Button"; +import { Input } from "@/components/ui/Input"; + +export function NewOrganizationRoute() { + return ( +
+

+ Create organization +

+ +
+
+ + +
+
+
+ +
+ +
+ ); +} From d4d7b2a109c7056d4b9d41cb984b477659e2c4cb Mon Sep 17 00:00:00 2001 From: itschip Date: Thu, 8 May 2025 19:10:53 +0200 Subject: [PATCH 10/96] feat: wip create organization --- internal/auth/user.go | 17 ++++ internal/database/database.go | 6 +- .../query/organization/organization_query.go | 18 +++++ internal/http/internalapi/internal_api.go | 3 + .../http/internalapi/organization_group.go | 8 +- internal/http/middleware/session.go | 37 +++++++++ .../organization/organization_service.go | 23 +++++- internal/storage/s3/s3.go | 11 ++- ...406011009_add_organization_member_table.go | 24 ++++++ web/src/App.tsx | 6 +- web/src/features/app/components/AppLayout.tsx | 3 +- .../features/app/components/AppSidebar.tsx | 9 ++- .../api/useCreateOrganization.ts | 22 ++++++ .../routes/NewOrganizationRoute.tsx | 78 +++++++++++++------ .../routes/OrganizationSelectRoute.tsx | 33 ++++++++ .../features/tokens/routes/TokensRoute.tsx | 2 +- web/src/typings/organizations.ts | 8 ++ 17 files changed, 268 insertions(+), 40 deletions(-) create mode 100644 internal/auth/user.go create mode 100644 internal/http/middleware/session.go create mode 100644 migrate/migrations/20250406011009_add_organization_member_table.go create mode 100644 web/src/features/organizations/api/useCreateOrganization.ts create mode 100644 web/src/features/organizations/routes/OrganizationSelectRoute.tsx diff --git a/internal/auth/user.go b/internal/auth/user.go new file mode 100644 index 0000000..2e2db4e --- /dev/null +++ b/internal/auth/user.go @@ -0,0 +1,17 @@ +package auth + +import ( + "errors" + + "github.com/fivemanage/lite/api" + "github.com/labstack/echo/v4" +) + +func CurrentUser(c echo.Context) (*api.User, error) { + user, ok := c.Get("user").(*api.User) + if !ok { + return nil, errors.New("user not found in context") + } + + return user, nil +} diff --git a/internal/database/database.go b/internal/database/database.go index 7261a4f..859bb14 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -27,20 +27,18 @@ type Session struct { User *User `bun:"rel:belongs-to,join:user_id=id"` } -// not used yet type Organization struct { bun.BaseModel `bun:"table:organization"` ID int64 `bun:"id,pk,autoincrement"` Name string `bun:"name"` } -// not used yet type OrganizationMember struct { bun.BaseModel `bun:"table:organization_member"` ID int64 `bun:"id,pk,autoincrement"` Role string `bun:"role"` - UserID int `bun:"user_id"` - OrganizationID int `bun:"organization_id"` + UserID int64 `bun:"user_id"` + OrganizationID int64 `bun:"organization_id"` User *User `bun:"rel:belongs-to,join:user_id=id"` Organization *Organization `bun:"rel:belongs-to,join:organization_id=id"` } diff --git a/internal/database/query/organization/organization_query.go b/internal/database/query/organization/organization_query.go index 5338823..dabacf4 100644 --- a/internal/database/query/organization/organization_query.go +++ b/internal/database/query/organization/organization_query.go @@ -9,6 +9,10 @@ import ( func Create(ctx context.Context, db *bun.DB, organization *database.Organization) (bun.Tx, error) { tx, err := db.BeginTx(ctx, nil) + if err != nil { + return tx, err + } + _, err = tx.NewInsert().Model(organization).Exec(ctx) if err != nil { return tx, err @@ -37,3 +41,17 @@ func List(ctx context.Context, db *bun.DB) ([]database.Organization, error) { return organizations, nil } + +func CreateMember(ctx context.Context, db *bun.DB, member *database.OrganizationMember) (bun.Tx, error) { + tx, err := db.BeginTx(ctx, nil) + if err != nil { + return tx, err + } + + _, err = tx.NewInsert().Model(member).Exec(ctx) + if err != nil { + return tx, err + } + + return tx, nil +} diff --git a/internal/http/internalapi/internal_api.go b/internal/http/internalapi/internal_api.go index 3316f07..3ed02d8 100644 --- a/internal/http/internalapi/internal_api.go +++ b/internal/http/internalapi/internal_api.go @@ -1,6 +1,7 @@ package internalapi import ( + "github.com/fivemanage/lite/internal/http/middleware" "github.com/fivemanage/lite/internal/service/auth" "github.com/fivemanage/lite/internal/service/organization" "github.com/fivemanage/lite/internal/service/token" @@ -8,6 +9,8 @@ import ( ) func Add(group *echo.Group, authService *auth.Auth, tokenService *token.Service, organizationService *organization.Service) { + group.Use(middleware.Session(authService)) + registerAuthApi(group, authService) registerTokensApi(group, tokenService) registerOrganizationApi(group, organizationService) diff --git a/internal/http/internalapi/organization_group.go b/internal/http/internalapi/organization_group.go index 7f55c09..9fc6ea3 100644 --- a/internal/http/internalapi/organization_group.go +++ b/internal/http/internalapi/organization_group.go @@ -2,6 +2,7 @@ package internalapi import ( "github.com/fivemanage/lite/api" + "github.com/fivemanage/lite/internal/auth" "github.com/fivemanage/lite/internal/http/httputil" "github.com/fivemanage/lite/internal/service/organization" "github.com/labstack/echo/v4" @@ -14,7 +15,12 @@ func registerOrganizationApi(group *echo.Group, organizationService *organizatio return echo.NewHTTPError(400, err) } - organization, err := organizationService.CreateOrganization(c.Request().Context(), &data) + currentUser, err := auth.CurrentUser(c) + if err != nil { + return echo.NewHTTPError(401, err) + } + + organization, err := organizationService.CreateOrganization(c.Request().Context(), &data, currentUser.ID) if err != nil { return echo.NewHTTPError(500, err) } diff --git a/internal/http/middleware/session.go b/internal/http/middleware/session.go new file mode 100644 index 0000000..a6fa3f4 --- /dev/null +++ b/internal/http/middleware/session.go @@ -0,0 +1,37 @@ +package middleware + +import ( + "fmt" + "net/http" + "strings" + + "github.com/fivemanage/lite/internal/service/auth" + "github.com/labstack/echo/v4" +) + +func Session(authService *auth.Auth) echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + ctx := c.Request().Context() + + fmt.Println(c.Request().URL.Path) + // ignore /auth endpoints + if strings.HasPrefix(c.Request().URL.Path, "/api/auth") { + return next(c) + } + + sessionCookie, err := c.Cookie("fmlite_session") + if err != nil { + return c.JSON(http.StatusUnauthorized, "failed to find session cookie") + } + + session, err := authService.UserBySession(ctx, sessionCookie.Value) + if err != nil { + return err + } + + c.Set("user", session) + return next(c) + } + } +} diff --git a/internal/service/organization/organization_service.go b/internal/service/organization/organization_service.go index 896ee9a..75b1bea 100644 --- a/internal/service/organization/organization_service.go +++ b/internal/service/organization/organization_service.go @@ -19,15 +19,34 @@ func NewService(db *bun.DB) *Service { } } -func (r *Service) CreateOrganization(ctx context.Context, data *api.CreateOrganizationRequest) (*api.Organization, error) { +func (r *Service) CreateOrganization(ctx context.Context, data *api.CreateOrganizationRequest, userID int64) (*api.Organization, error) { dbOrganization := &database.Organization{ Name: data.Name, } - _, err := organizationquery.Create(ctx, r.db, dbOrganization) + orgTx, err := organizationquery.Create(ctx, r.db, dbOrganization) if err != nil { return nil, err } + if err := orgTx.Commit(); err != nil { + return nil, err + } + + memberTx, err := organizationquery.CreateMember(ctx, r.db, &database.OrganizationMember{ + Role: "ADMIN", + OrganizationID: dbOrganization.ID, + UserID: userID, + }) + if err != nil { + return nil, err + } + + if err := memberTx.Commit(); err != nil { + if err := orgTx.Rollback(); err != nil { + return nil, err + } + return nil, err + } organization := &api.Organization{ ID: dbOrganization.ID, diff --git a/internal/storage/s3/s3.go b/internal/storage/s3/s3.go index f7c4f90..d4db050 100644 --- a/internal/storage/s3/s3.go +++ b/internal/storage/s3/s3.go @@ -40,17 +40,24 @@ func New() *Storage { // UploadFile will both upload and replace as long as the key is the same func (s *Storage) UploadFile(ctx context.Context, file io.Reader, key, contenType string) error { - s.client.PutObject(ctx, &s3.PutObjectInput{ + _, err := s.client.PutObject(ctx, &s3.PutObjectInput{ Bucket: aws.String(os.Getenv("AWS_BUCKET")), Key: aws.String(key), Body: file, ContentType: aws.String(contenType), }) + if err != nil { + return err + } return nil } func (s *Storage) DeleteFile() error { - s.client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{}) + _, err := s.client.DeleteObject(context.TODO(), &s3.DeleteObjectInput{}) + if err != nil { + return err + } + return nil } diff --git a/migrate/migrations/20250406011009_add_organization_member_table.go b/migrate/migrations/20250406011009_add_organization_member_table.go new file mode 100644 index 0000000..2d6b698 --- /dev/null +++ b/migrate/migrations/20250406011009_add_organization_member_table.go @@ -0,0 +1,24 @@ +package migrations + +import ( + "context" + "fmt" + + "github.com/fivemanage/lite/internal/database" + "github.com/uptrace/bun" +) + +func init() { + Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { + fmt.Print(" [up migration] ") + db.RegisterModel((*database.OrganizationMember)(nil)) + _, err := db.NewCreateTable().Model((*database.OrganizationMember)(nil)).Exec(ctx) + if err != nil { + return err + } + return nil + }, func(ctx context.Context, db *bun.DB) error { + fmt.Print(" [down migration] ") + return nil + }) +} diff --git a/web/src/App.tsx b/web/src/App.tsx index 2029f37..1a91d9a 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,12 +1,14 @@ +import { lazy } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { BrowserRouter, Route, Routes } from "react-router"; import { AuthRoute } from "./features/auth/routes/AuthRoute"; import { AppDashboard } from "./features/app/routes/AppDashboard"; import { AppLayout } from "./features/app/components/AppLayout"; -import { TokensRoute } from "./features/tokens/routes/TokensRoute"; import { ThemeProvider } from "./components/theme/ThemeProvider"; import { ProtectedRoute } from "./features/auth/routes/ProtectedRoute"; import { NewOrganizationRoute } from "./features/organizations/routes/NewOrganizationRoute"; +import { OrganizationSelectRoute } from "./features/organizations/routes/OrganizationSelectRoute"; +const TokensRoute = lazy(() => import("./features/tokens/routes/TokensRoute")); const queryClient = new QueryClient(); @@ -19,6 +21,7 @@ function App() { 404} /> } /> + } /> } @@ -26,6 +29,7 @@ function App() { }> }> } /> + {/* Should probably wrap a suspense around this */} } /> diff --git a/web/src/features/app/components/AppLayout.tsx b/web/src/features/app/components/AppLayout.tsx index ddbcfd5..a24abdd 100644 --- a/web/src/features/app/components/AppLayout.tsx +++ b/web/src/features/app/components/AppLayout.tsx @@ -19,7 +19,6 @@ import { ModeToggle } from "@/components/theme/ModeToggle"; export function AppLayout() { const location = useLocation(); const paths = location.pathname.split("/").filter((p) => p); - console.log(paths); return ( @@ -33,7 +32,7 @@ export function AppLayout() { - {paths[0]} + some dropdown diff --git a/web/src/features/app/components/AppSidebar.tsx b/web/src/features/app/components/AppSidebar.tsx index 86ebd2e..8aa89f8 100644 --- a/web/src/features/app/components/AppSidebar.tsx +++ b/web/src/features/app/components/AppSidebar.tsx @@ -18,18 +18,19 @@ import { SidebarMenuButton, SidebarMenuItem, } from "@/components/ui/Sidebar"; +import { NavLink } from "react-router"; // Menu items. const items = [ { title: "Files", - url: "/app/files", + url: "files", icon: Folders, comingSoon: false, }, { title: "Tokens", - url: "/app/tokens", + url: "tokens", icon: KeyRound, comingSoon: false, }, @@ -70,10 +71,10 @@ export function AppSidebar() { {items.map((item) => ( - + {item.title} - + {item.comingSoon && ( Coming soon diff --git a/web/src/features/organizations/api/useCreateOrganization.ts b/web/src/features/organizations/api/useCreateOrganization.ts new file mode 100644 index 0000000..5c77b0d --- /dev/null +++ b/web/src/features/organizations/api/useCreateOrganization.ts @@ -0,0 +1,22 @@ +import { Organization } from "@/typings/organizations"; +import { ApiError, fetchApi } from "@/utils/http-util"; +import { useMutation } from "@tanstack/react-query"; + +export function useCreateOrganization() { + const { mutate, isPending } = useMutation({ + mutationFn: async (params: Omit) => { + try { + return await fetchApi("/api/organization", { + method: "POST", + body: JSON.stringify(params), + }); + } catch (err) { + if (err instanceof ApiError) { + throw new Error(err.message); + } + } + }, + }); + + return { mutate, isPending }; +} diff --git a/web/src/features/organizations/routes/NewOrganizationRoute.tsx b/web/src/features/organizations/routes/NewOrganizationRoute.tsx index 5adffb2..19afeb3 100644 --- a/web/src/features/organizations/routes/NewOrganizationRoute.tsx +++ b/web/src/features/organizations/routes/NewOrganizationRoute.tsx @@ -1,37 +1,69 @@ import { Button } from "@/components/ui/Button"; import { Input } from "@/components/ui/Input"; +import { useCreateOrganization } from "../api/useCreateOrganization"; +import { useForm } from "react-hook-form"; +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/Form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { + createOrganizationSchema, + CreateOrganizationSchema, +} from "@/typings/organizations"; export function NewOrganizationRoute() { + const { mutate } = useCreateOrganization(); + + const formMethods = useForm({ + defaultValues: { + name: "", + }, + resolver: zodResolver(createOrganizationSchema), + }); + + function handleCreateOrganization(data: CreateOrganizationSchema) { + mutate(data); + } + return (

Create organization

-
-
-
- - + +
+ ( + + + Organization Name* + + + + + + + )} />
-
-
- -
- +
+ +
+ +
); } diff --git a/web/src/features/organizations/routes/OrganizationSelectRoute.tsx b/web/src/features/organizations/routes/OrganizationSelectRoute.tsx new file mode 100644 index 0000000..9cbf95f --- /dev/null +++ b/web/src/features/organizations/routes/OrganizationSelectRoute.tsx @@ -0,0 +1,33 @@ +import { NavLink } from "react-router"; +import { useOrganizations } from "../api/useOrganizations"; + +export function OrganizationSelectRoute() { + const { data, isLoading } = useOrganizations(); + + // switch out to use suspense + if (isLoading) { + return
Loading...
; + } + + return ( +
+
+ {data && + data.map((org) => ( + +
+

{org.name}

+
+
+ ))} +
+
+ ); +} diff --git a/web/src/features/tokens/routes/TokensRoute.tsx b/web/src/features/tokens/routes/TokensRoute.tsx index 004f394..bfeacb9 100644 --- a/web/src/features/tokens/routes/TokensRoute.tsx +++ b/web/src/features/tokens/routes/TokensRoute.tsx @@ -2,7 +2,7 @@ import { Suspense } from "react"; import { CreateTokenDialog } from "../components/CreateTokenDialog"; import { TokensTable } from "../components/TokensTable"; -export function TokensRoute() { +export default function TokensRoute() { return (
diff --git a/web/src/typings/organizations.ts b/web/src/typings/organizations.ts index fbdf269..842488d 100644 --- a/web/src/typings/organizations.ts +++ b/web/src/typings/organizations.ts @@ -1,3 +1,11 @@ +import { z } from "zod"; + +export const createOrganizationSchema = z.object({ + name: z.string(), +}); + +export type CreateOrganizationSchema = z.infer; + export interface Organization { id: number; name: string; From 9db37c1b8c9a575f4f1c4217e3f7b5c0f9932ad4 Mon Sep 17 00:00:00 2001 From: itschip Date: Thu, 8 May 2025 20:02:24 +0200 Subject: [PATCH 11/96] fix: migration issues --- cmd/lite/lite.go | 2 ++ internal/database/database.go | 4 +-- internal/database/query/file/file_query.go | 2 +- internal/service/file/file_service.go | 2 +- migrate/migrate.go | 7 ++++++ .../migrations/20250207222546_add_is_admin.go | 25 ------------------- ... => 20250508174146_create_assets_table.go} | 11 +++++++- 7 files changed, 23 insertions(+), 30 deletions(-) delete mode 100644 migrate/migrations/20250207222546_add_is_admin.go rename migrate/migrations/{20250207213625_add_username_column.go => 20250508174146_create_assets_table.go} (55%) diff --git a/cmd/lite/lite.go b/cmd/lite/lite.go index ce26ca0..7592c2a 100644 --- a/cmd/lite/lite.go +++ b/cmd/lite/lite.go @@ -38,6 +38,8 @@ var rootCmd = &cobra.Command{ db := database.New(driver) store := db.Connect(dsn) + + migrate.InitMigration(cmd.Context(), store) migrate.AutoMigrate(cmd.Context(), store) storageLayer := storage.New("s3") diff --git a/internal/database/database.go b/internal/database/database.go index 859bb14..e7f697a 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -52,8 +52,8 @@ type Token struct { User *User `bun:"rel:belongs-to,join:user_id=id"` } -type File struct { - bun.BaseModel `bun:"table:file"` +type Asset struct { + bun.BaseModel `bun:"table:asset"` Key string `bun:"key"` Size int64 `bun:"size"` Type string `bun:"type"` diff --git a/internal/database/query/file/file_query.go b/internal/database/query/file/file_query.go index 0f7af3a..32224c8 100644 --- a/internal/database/query/file/file_query.go +++ b/internal/database/query/file/file_query.go @@ -7,7 +7,7 @@ import ( "github.com/uptrace/bun" ) -func Create(ctx context.Context, db *bun.DB, file *database.File) (bun.Tx, error) { +func Create(ctx context.Context, db *bun.DB, file *database.Asset) (bun.Tx, error) { tx, err := db.BeginTx(ctx, nil) _, err = tx.NewInsert().Model(file).Exec(ctx) if err != nil { diff --git a/internal/service/file/file_service.go b/internal/service/file/file_service.go index bc7145f..d44bef3 100644 --- a/internal/service/file/file_service.go +++ b/internal/service/file/file_service.go @@ -38,7 +38,7 @@ func (s *Service) CreateFile( return err } - dbFile := &database.File{ + dbFile := &database.Asset{ Type: fileType, Size: fileHeader.Size, Key: key, diff --git a/migrate/migrate.go b/migrate/migrate.go index cdaeab4..bca65c7 100644 --- a/migrate/migrate.go +++ b/migrate/migrate.go @@ -111,6 +111,13 @@ var ( } ) +func InitMigration(ctx context.Context, db *bun.DB) { + migrator := migrate.NewMigrator(db, migrations.Migrations) + if err := migrator.Init(ctx); err != nil { + panic(err) + } +} + func AutoMigrate(ctx context.Context, db *bun.DB) { migrator := migrate.NewMigrator(db, migrations.Migrations) if err := migrator.Lock(ctx); err != nil { diff --git a/migrate/migrations/20250207222546_add_is_admin.go b/migrate/migrations/20250207222546_add_is_admin.go deleted file mode 100644 index d1f9f90..0000000 --- a/migrate/migrations/20250207222546_add_is_admin.go +++ /dev/null @@ -1,25 +0,0 @@ -package migrations - -import ( - "context" - "fmt" - - "github.com/uptrace/bun" -) - -func init() { - Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { - fmt.Print(" [up migration] ") - _, err := db.NewAddColumn(). - Table("user"). - ColumnExpr("is_admin BOOL NOT NULL DEFAULT false"). - Exec(ctx) - if err != nil { - return err - } - return nil - }, func(ctx context.Context, db *bun.DB) error { - fmt.Print(" [down migration] ") - return nil - }) -} diff --git a/migrate/migrations/20250207213625_add_username_column.go b/migrate/migrations/20250508174146_create_assets_table.go similarity index 55% rename from migrate/migrations/20250207213625_add_username_column.go rename to migrate/migrations/20250508174146_create_assets_table.go index 4a74f74..acc0a65 100644 --- a/migrate/migrations/20250207213625_add_username_column.go +++ b/migrate/migrations/20250508174146_create_assets_table.go @@ -4,19 +4,28 @@ import ( "context" "fmt" + "github.com/fivemanage/lite/internal/database" "github.com/uptrace/bun" ) func init() { Migrations.MustRegister(func(ctx context.Context, db *bun.DB) error { fmt.Print(" [up migration] ") - _, err := db.NewAddColumn().Table("user").ColumnExpr("username TEXT NOT NULL").Exec(ctx) + db.RegisterModel((*database.Asset)(nil)) + _, err := db.NewCreateTable().Model((*database.Asset)(nil)).Exec(ctx) if err != nil { return err } + return nil }, func(ctx context.Context, db *bun.DB) error { fmt.Print(" [down migration] ") + + _, err := db.NewDropTable().Model((*database.Asset)(nil)).IfExists().Exec(ctx) + if err != nil { + return err + } + return nil }) } From c5c10dfae77d77f5b72e2e413f988d744d6b3c75 Mon Sep 17 00:00:00 2001 From: itschip Date: Fri, 9 May 2025 00:18:55 +0200 Subject: [PATCH 12/96] feat(organization): fix org tables, session check --- api/organization.go | 2 +- internal/crypt/nano.go | 8 ++ internal/database/database.go | 9 ++- .../query/organization/organization_query.go | 4 +- .../http/internalapi/organization_group.go | 10 +++ .../organization/organization_service.go | 11 ++- web/src/App.tsx | 5 +- web/src/components/theme/ModeToggle.tsx | 6 +- web/src/components/ui/Sidebar.tsx | 2 +- web/src/components/ui/button.tsx | 2 +- web/src/features/app/components/AppLayout.tsx | 29 +++++--- .../features/app/components/AppSidebar.tsx | 2 +- web/src/features/app/routes/AppDashboard.tsx | 4 +- web/src/features/auth/api/useSession.ts | 4 +- .../features/auth/routes/ProtectedRoute.tsx | 7 ++ web/src/features/files/FilesRoute.tsx | 3 + .../api/useCurrentOrganization.ts | 22 ++++++ .../routes/OrganizationSelectRoute.tsx | 14 +++- web/src/global.css | 73 ------------------- web/src/typings/organizations.ts | 2 +- web/src/typings/router.ts | 3 + 21 files changed, 117 insertions(+), 105 deletions(-) create mode 100644 web/src/features/files/FilesRoute.tsx create mode 100644 web/src/features/organizations/api/useCurrentOrganization.ts create mode 100644 web/src/typings/router.ts diff --git a/api/organization.go b/api/organization.go index aa09b2a..6351297 100644 --- a/api/organization.go +++ b/api/organization.go @@ -1,7 +1,7 @@ package api type Organization struct { - ID int64 `json:"id"` + ID string `json:"id"` Name string `json:"name"` } diff --git a/internal/crypt/nano.go b/internal/crypt/nano.go index 665c2aa..5033fb1 100644 --- a/internal/crypt/nano.go +++ b/internal/crypt/nano.go @@ -25,3 +25,11 @@ func GenerateFilename() (string, error) { } return id, nil } + +func GeneratePrimaryKey() (string, error) { + id, err := gonanoid.Generate("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 16) + if err != nil { + return "", nil + } + return id, nil +} diff --git a/internal/database/database.go b/internal/database/database.go index e7f697a..b01f103 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -27,9 +27,11 @@ type Session struct { User *User `bun:"rel:belongs-to,join:user_id=id"` } +// nanoid to keep the ID more url friendly +// might switch to uuid later type Organization struct { bun.BaseModel `bun:"table:organization"` - ID int64 `bun:"id,pk,autoincrement"` + ID string `bun:"id,pk"` Name string `bun:"name"` } @@ -38,7 +40,7 @@ type OrganizationMember struct { ID int64 `bun:"id,pk,autoincrement"` Role string `bun:"role"` UserID int64 `bun:"user_id"` - OrganizationID int64 `bun:"organization_id"` + OrganizationID string `bun:"organization_id"` User *User `bun:"rel:belongs-to,join:user_id=id"` Organization *Organization `bun:"rel:belongs-to,join:organization_id=id"` } @@ -52,8 +54,11 @@ type Token struct { User *User `bun:"rel:belongs-to,join:user_id=id"` } +// nanoid to keep the ID more url friendly +// might switch to uuid later type Asset struct { bun.BaseModel `bun:"table:asset"` + ID string `bun:"id,pk"` Key string `bun:"key"` Size int64 `bun:"size"` Type string `bun:"type"` diff --git a/internal/database/query/organization/organization_query.go b/internal/database/query/organization/organization_query.go index dabacf4..bac3644 100644 --- a/internal/database/query/organization/organization_query.go +++ b/internal/database/query/organization/organization_query.go @@ -21,9 +21,9 @@ func Create(ctx context.Context, db *bun.DB, organization *database.Organization return tx, nil } -func Find(ctx context.Context, db *bun.DB, id int64) (*database.Organization, error) { +func Find(ctx context.Context, db *bun.DB, ID string) (*database.Organization, error) { organization := new(database.Organization) - err := db.NewSelect().Model(organization).Where("id = ?", id).Scan(ctx) + err := db.NewSelect().Model(organization).Where("id = ?", ID).Scan(ctx) if err != nil { return nil, err } diff --git a/internal/http/internalapi/organization_group.go b/internal/http/internalapi/organization_group.go index 9fc6ea3..3ca8032 100644 --- a/internal/http/internalapi/organization_group.go +++ b/internal/http/internalapi/organization_group.go @@ -36,4 +36,14 @@ func registerOrganizationApi(group *echo.Group, organizationService *organizatio return c.JSON(200, httputil.Response(organizations)) }) + + group.GET("/organization/:id", func(c echo.Context) error { + id := c.Param("id") + organization, err := organizationService.FindOrganizationByID(c.Request().Context(), id) + if err != nil { + return echo.NewHTTPError(500, err) + } + + return c.JSON(200, httputil.Response(organization)) + }) } diff --git a/internal/service/organization/organization_service.go b/internal/service/organization/organization_service.go index 75b1bea..0143453 100644 --- a/internal/service/organization/organization_service.go +++ b/internal/service/organization/organization_service.go @@ -4,6 +4,7 @@ import ( "context" "github.com/fivemanage/lite/api" + "github.com/fivemanage/lite/internal/crypt" "github.com/fivemanage/lite/internal/database" organizationquery "github.com/fivemanage/lite/internal/database/query/organization" "github.com/uptrace/bun" @@ -20,8 +21,14 @@ func NewService(db *bun.DB) *Service { } func (r *Service) CreateOrganization(ctx context.Context, data *api.CreateOrganizationRequest, userID int64) (*api.Organization, error) { + orgId, err := crypt.GeneratePrimaryKey() + if err != nil { + return nil, err + } + dbOrganization := &database.Organization{ Name: data.Name, + ID: orgId, } orgTx, err := organizationquery.Create(ctx, r.db, dbOrganization) @@ -56,8 +63,8 @@ func (r *Service) CreateOrganization(ctx context.Context, data *api.CreateOrgani return organization, nil } -func (r *Service) FindOrganizationByID(ctx context.Context, id int64) (*api.Organization, error) { - dbOrganization, err := organizationquery.Find(ctx, r.db, id) +func (r *Service) FindOrganizationByID(ctx context.Context, ID string) (*api.Organization, error) { + dbOrganization, err := organizationquery.Find(ctx, r.db, ID) if err != nil { return nil, err } diff --git a/web/src/App.tsx b/web/src/App.tsx index 1a91d9a..c5e8ad3 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,6 +1,6 @@ import { lazy } from "react"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; -import { BrowserRouter, Route, Routes } from "react-router"; +import { BrowserRouter, Navigate, Route, Routes } from "react-router"; import { AuthRoute } from "./features/auth/routes/AuthRoute"; import { AppDashboard } from "./features/app/routes/AppDashboard"; import { AppLayout } from "./features/app/components/AppLayout"; @@ -8,6 +8,7 @@ import { ThemeProvider } from "./components/theme/ThemeProvider"; import { ProtectedRoute } from "./features/auth/routes/ProtectedRoute"; import { NewOrganizationRoute } from "./features/organizations/routes/NewOrganizationRoute"; import { OrganizationSelectRoute } from "./features/organizations/routes/OrganizationSelectRoute"; +import { FilesRoute } from "./features/files/FilesRoute"; const TokensRoute = lazy(() => import("./features/tokens/routes/TokensRoute")); const queryClient = new QueryClient(); @@ -19,6 +20,7 @@ function App() { 404
} /> + } /> } /> } /> @@ -31,6 +33,7 @@ function App() { } /> {/* Should probably wrap a suspense around this */} } /> + } /> diff --git a/web/src/components/theme/ModeToggle.tsx b/web/src/components/theme/ModeToggle.tsx index ea8d44f..41b6214 100644 --- a/web/src/components/theme/ModeToggle.tsx +++ b/web/src/components/theme/ModeToggle.tsx @@ -15,9 +15,9 @@ export function ModeToggle() { return ( - diff --git a/web/src/components/ui/Sidebar.tsx b/web/src/components/ui/Sidebar.tsx index 4811f9b..052efc4 100644 --- a/web/src/components/ui/Sidebar.tsx +++ b/web/src/components/ui/Sidebar.tsx @@ -279,7 +279,7 @@ const SidebarTrigger = React.forwardRef< data-sidebar="trigger" variant="ghost" size="icon" - className={cn("h-7 w-7", className)} + className={cn("h-4 w-4", className)} onClick={(event) => { onClick?.(event); toggleSidebar(); diff --git a/web/src/components/ui/button.tsx b/web/src/components/ui/button.tsx index 62b5f54..a7e112a 100644 --- a/web/src/components/ui/button.tsx +++ b/web/src/components/ui/button.tsx @@ -20,7 +20,7 @@ const buttonVariants = cva( }, size: { default: "h-10 px-4 py-2", - sm: "h-9 rounded-md px-3", + sm: "h-8 rounded-md px-3", lg: "h-11 rounded-md px-8", icon: "h-10 w-10", }, diff --git a/web/src/features/app/components/AppLayout.tsx b/web/src/features/app/components/AppLayout.tsx index a24abdd..1d3eb32 100644 --- a/web/src/features/app/components/AppLayout.tsx +++ b/web/src/features/app/components/AppLayout.tsx @@ -3,7 +3,7 @@ import { SidebarProvider, SidebarTrigger, } from "@/components/ui/Sidebar"; -import { Link, Outlet, useLocation } from "react-router"; +import { Link, Outlet, useLocation, useParams } from "react-router"; import { AppSidebar } from "./AppSidebar"; import { Separator } from "@/components/ui/Separator"; import { @@ -15,34 +15,39 @@ import { BreadcrumbSeparator, } from "@/components/ui/Breadcrumb"; import { ModeToggle } from "@/components/theme/ModeToggle"; +import { Params } from "@/typings/router"; +import { useCurrentOrganization } from "@/features/organizations/api/useCurrentOrganization"; export function AppLayout() { const location = useLocation(); + const params = useParams(); + const organization = useCurrentOrganization(params.organizationId); + const paths = location.pathname.split("/").filter((p) => p); return ( -
+
diff --git a/web/src/features/app/components/AppSidebar.tsx b/web/src/features/app/components/AppSidebar.tsx index 8aa89f8..07cba69 100644 --- a/web/src/features/app/components/AppSidebar.tsx +++ b/web/src/features/app/components/AppSidebar.tsx @@ -63,7 +63,7 @@ const items = [ export function AppSidebar() { return ( - + Fivemanage Lite (dev) diff --git a/web/src/features/app/routes/AppDashboard.tsx b/web/src/features/app/routes/AppDashboard.tsx index 4e05587..db4eeba 100644 --- a/web/src/features/app/routes/AppDashboard.tsx +++ b/web/src/features/app/routes/AppDashboard.tsx @@ -1,11 +1,11 @@ import { useSession } from "@/features/auth/api/useSession"; export const AppDashboard: React.FC = () => { - const data = useSession(); + const session = useSession(); return (
-
{JSON.stringify(data)}
+
{JSON.stringify(session.data)}
); }; diff --git a/web/src/features/auth/api/useSession.ts b/web/src/features/auth/api/useSession.ts index c9db776..7057993 100644 --- a/web/src/features/auth/api/useSession.ts +++ b/web/src/features/auth/api/useSession.ts @@ -4,10 +4,10 @@ import { fetchApi } from "@/utils/http-util"; import { useQuery } from "@tanstack/react-query"; export function useSession() { - const { data } = useQuery({ + const { data, isPending } = useQuery({ queryKey: [QueryKeys.Session], queryFn: () => fetchApi("/api/auth/session"), }); - return data; + return { data, isPending }; } diff --git a/web/src/features/auth/routes/ProtectedRoute.tsx b/web/src/features/auth/routes/ProtectedRoute.tsx index 1f432bb..2d689f1 100644 --- a/web/src/features/auth/routes/ProtectedRoute.tsx +++ b/web/src/features/auth/routes/ProtectedRoute.tsx @@ -1,5 +1,6 @@ import { useOrganizations } from "@/features/organizations/api/useOrganizations"; import { Outlet, Navigate, useParams } from "react-router"; +import { useSession } from "../api/useSession"; type AppParams = { organizationId: string; @@ -8,7 +9,13 @@ type AppParams = { export const ProtectedRoute: React.FC = () => { const params = useParams(); + const session = useSession(); const { data: organizations } = useOrganizations(); + + if (!session.data && !session.isPending) { + return ; + } + if (organizations && organizations.length === 0) { return ; } diff --git a/web/src/features/files/FilesRoute.tsx b/web/src/features/files/FilesRoute.tsx new file mode 100644 index 0000000..1cc6302 --- /dev/null +++ b/web/src/features/files/FilesRoute.tsx @@ -0,0 +1,3 @@ +export function FilesRoute() { + return
; +} diff --git a/web/src/features/organizations/api/useCurrentOrganization.ts b/web/src/features/organizations/api/useCurrentOrganization.ts new file mode 100644 index 0000000..3e73a3a --- /dev/null +++ b/web/src/features/organizations/api/useCurrentOrganization.ts @@ -0,0 +1,22 @@ +import { Organization } from "@/typings/organizations"; +import { ApiError, fetchApi } from "@/utils/http-util"; +import { useQuery } from "@tanstack/react-query"; + +export function useCurrentOrganization(organizationId: string | undefined) { + const { data, isLoading } = useQuery({ + queryKey: [organizationId], + queryFn: async () => { + try { + return fetchApi(`/api/organization/${organizationId}`); + } catch (err) { + if (err instanceof ApiError) { + throw new Error(err.message); + } + } + }, + }); + + console.log("useCurrentOrganization", data, isLoading); + + return { data, isLoading }; +} diff --git a/web/src/features/organizations/routes/OrganizationSelectRoute.tsx b/web/src/features/organizations/routes/OrganizationSelectRoute.tsx index 9cbf95f..abb05cf 100644 --- a/web/src/features/organizations/routes/OrganizationSelectRoute.tsx +++ b/web/src/features/organizations/routes/OrganizationSelectRoute.tsx @@ -1,14 +1,26 @@ -import { NavLink } from "react-router"; +import { Navigate, NavLink } from "react-router"; import { useOrganizations } from "../api/useOrganizations"; +import { useSession } from "@/features/auth/api/useSession"; export function OrganizationSelectRoute() { const { data, isLoading } = useOrganizations(); + // todo: move this to a layout route + const session = useSession(); + + if (!session.data && !session.isPending) { + return ; + } + // switch out to use suspense if (isLoading) { return
Loading...
; } + if (!data || data.length === 0) { + return ; + } + return (
diff --git a/web/src/global.css b/web/src/global.css index b1498f1..8376870 100644 --- a/web/src/global.css +++ b/web/src/global.css @@ -2,79 +2,6 @@ @tailwind components; @tailwind utilities; -/*@layer base { - :root { - --background: 0 0% 100%; - --foreground: 222.2 84% 4.9%; - --card: 0 0% 100%; - --card-foreground: 222.2 84% 4.9%; - --popover: 0 0% 100%; - --popover-foreground: 222.2 84% 4.9%; - --primary: 221.2 83.2% 53.3%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 210 40% 96.1%; - --muted-foreground: 215.4 16.3% 44%; - --accent: 210 40% 96.1%; - --accent-foreground: 222.2 47.4% 11.2%; - --destructive: 0 72% 51%; - --destructive-foreground: 210 40% 98%; - --border: 214.3 31.8% 91.4%; - --input: 214.3 31.8% 91.4%; - --ring: 221.2 83.2% 53.3%; - --chart-1: 221.2 83.2% 53.3%; - --chart-2: 212 95% 68%; - --chart-3: 216 92% 60%; - --chart-4: 210 98% 78%; - --chart-5: 212 97% 87%; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - - .dark { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 221.2 83.2% 53.3%; - --primary-foreground: 210 40% 98%; - --secondary: 210 40% 96.1%; - --secondary-foreground: 222.2 47.4% 11.2%; - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 72% 51%; - --destructive-foreground: 210 40% 98%; - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 221.2 83.2% 53.3%; - --chart-1: 221.2 83.2% 53.3%; - --chart-2: 212 95% 68%; - --chart-3: 216 92% 60%; - --chart-4: 210 98% 78%; - --chart-5: 212 97% 87%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; - } -} */ - - @layer base { :root { --background: 210 20% 98%; diff --git a/web/src/typings/organizations.ts b/web/src/typings/organizations.ts index 842488d..f79e6ee 100644 --- a/web/src/typings/organizations.ts +++ b/web/src/typings/organizations.ts @@ -7,6 +7,6 @@ export const createOrganizationSchema = z.object({ export type CreateOrganizationSchema = z.infer; export interface Organization { - id: number; + id: string; name: string; } diff --git a/web/src/typings/router.ts b/web/src/typings/router.ts new file mode 100644 index 0000000..3182af3 --- /dev/null +++ b/web/src/typings/router.ts @@ -0,0 +1,3 @@ +export type Params = { + organizationId: string; +}; From d6cc5cd31d1f43921fab8d3bf07640379d19c520 Mon Sep 17 00:00:00 2001 From: itschip Date: Sat, 10 May 2025 01:49:18 +0200 Subject: [PATCH 13/96] update readme --- README.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/README.md b/README.md index e69de29..e0dca1c 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,36 @@ +# Fievmanage Lite + +## Development + +### Prerequisites + +- Go: 1.23 or later +- Node.js: 20.x or later +- pnpm: 9.11.x or later +- air: hot reload for Go (https://github.com/air-verse/air) + +### Setup + +1. Install node_modules in `/web`. +2. Either run `go mod download` in the root directory, or just let `air` handle it for you, but simply running `air` in the root directory. +3. Set up docker. + 1. Run `docker compose -f deployments/docker-compose.yml -d` +4. Set up environment variables + + ```env + ADMIN_PASSWORD=password + DB_DRIVER=mysql + DSN="root:root@tcp(localhost:3306)/fivemanage-lite-dev" + ``` + + - `DB_DRIVER` + Can either be `mysql` or `pg`. Only mysql is properly tested. + +### Running the application + +First run all migrations. + +1. Init the migration: `go cmd/lite/lite.go db init +2. Run the migrations: `go cmd/lite/lite.go db migrate` + +Start the actual app: 3. Run `air` or `go cmd/lite/lite.go` in the root directory to start the Go application 4. In `web/`, run `pnpm dev` to start the React application. From 37f98e6bdf849aed5362a172de50aff1c9cc60ae Mon Sep 17 00:00:00 2001 From: itschip Date: Sat, 10 May 2025 01:51:07 +0200 Subject: [PATCH 14/96] fix readme --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e0dca1c..582b119 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ First run all migrations. -1. Init the migration: `go cmd/lite/lite.go db init +1. Init the migration: `go cmd/lite/lite.go db init` 2. Run the migrations: `go cmd/lite/lite.go db migrate` Start the actual app: 3. Run `air` or `go cmd/lite/lite.go` in the root directory to start the Go application 4. In `web/`, run `pnpm dev` to start the React application. From 5fdb6c54e0fffadbadbac28d238c516dc58f1a02 Mon Sep 17 00:00:00 2001 From: Christopher Date: Sat, 10 May 2025 12:25:41 +0200 Subject: [PATCH 15/96] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 582b119..79468ce 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Fievmanage Lite +# Fivemanage Lite ## Development From dcdeb6edaf3a77cb7e33592be4d9ef304a78df66 Mon Sep 17 00:00:00 2001 From: itschip Date: Sun, 11 May 2025 01:19:29 +0200 Subject: [PATCH 16/96] feat(app/storage): storage table, storage api --- api/file.go | 12 + cmd/lite/lite.go | 2 +- go.mod | 2 + go.sum | 5 + internal/database/database.go | 2 + internal/database/query/file/file_query.go | 44 ++++ internal/http/httputil/httputil.go | 18 ++ internal/http/internalapi/internal_api.go | 4 +- internal/http/internalapi/storage_group.go | 32 +++ internal/http/middleware/session.go | 3 - internal/http/server.go | 14 +- internal/service/file/error.go | 16 ++ internal/service/file/file_service.go | 48 ++-- internal/service/file/key.go | 27 ++ web/package.json | 3 + web/pnpm-lock.yaml | 237 ++++++++++++++++++ web/src/App.tsx | 4 +- web/src/components/data/DataTable.tsx | 172 +++++++++++++ web/src/components/data/DataTableSkeleton.tsx | 167 ++++++++++++ web/src/components/ui/Checkbox.tsx | 28 +++ .../features/app/components/AppSidebar.tsx | 5 +- web/src/features/files/FilesRoute.tsx | 3 - web/src/features/files/StorageRoute.tsx | 13 + web/src/features/files/api/useStorageFiles.ts | 24 ++ .../features/files/components/AssetList.tsx | 134 ++++++++++ .../files/components/assst-columns.tsx | 128 ++++++++++ web/src/lib/browser/keyboard.ts | 7 + web/src/typings/asset.ts | 6 + web/src/typings/assets.ts | 4 + 29 files changed, 1125 insertions(+), 39 deletions(-) create mode 100644 internal/http/internalapi/storage_group.go create mode 100644 internal/service/file/error.go create mode 100644 internal/service/file/key.go create mode 100644 web/src/components/data/DataTable.tsx create mode 100644 web/src/components/data/DataTableSkeleton.tsx create mode 100644 web/src/components/ui/Checkbox.tsx delete mode 100644 web/src/features/files/FilesRoute.tsx create mode 100644 web/src/features/files/StorageRoute.tsx create mode 100644 web/src/features/files/api/useStorageFiles.ts create mode 100644 web/src/features/files/components/AssetList.tsx create mode 100644 web/src/features/files/components/assst-columns.tsx create mode 100644 web/src/lib/browser/keyboard.ts create mode 100644 web/src/typings/asset.ts create mode 100644 web/src/typings/assets.ts diff --git a/api/file.go b/api/file.go index d19d6cd..dac2f27 100644 --- a/api/file.go +++ b/api/file.go @@ -6,3 +6,15 @@ type UploadFile struct { FileHeader *multipart.FileHeader File multipart.File } + +type Asset struct { + ID string + Key string + Size int64 + Type string +} + +type AssetResponse struct { + StorageFiles []Asset `json:"storageFiles"` + Total int64 `json:"total"` +} diff --git a/cmd/lite/lite.go b/cmd/lite/lite.go index 7592c2a..8683f5e 100644 --- a/cmd/lite/lite.go +++ b/cmd/lite/lite.go @@ -34,7 +34,7 @@ var rootCmd = &cobra.Command{ driver := viper.GetString("driver") dsn := viper.GetString("dsn") - fmt.Println("Starting Fivemanage...") + logrus.Info("Starting Fivemanage...") db := database.New(driver) store := db.Connect(dsn) diff --git a/go.mod b/go.mod index c6f9517..c2a3d65 100644 --- a/go.mod +++ b/go.mod @@ -44,6 +44,8 @@ require ( github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/huandu/go-sqlbuilder v1.35.0 // indirect + github.com/huandu/xstrings v1.4.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/labstack/gommon v0.4.2 // indirect diff --git a/go.sum b/go.sum index 6addc4d..cd857fd 100644 --- a/go.sum +++ b/go.sum @@ -61,6 +61,11 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/huandu/go-assert v1.1.6/go.mod h1:JuIfbmYG9ykwvuxoJ3V8TB5QP+3+ajIA54Y44TmkMxs= +github.com/huandu/go-sqlbuilder v1.35.0 h1:ESvxFHN8vxCTudY1Vq63zYpU5yJBESn19sf6k4v2T5Q= +github.com/huandu/go-sqlbuilder v1.35.0/go.mod h1:mS0GAtrtW+XL6nM2/gXHRJax2RwSW1TraavWDFAc1JA= +github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= +github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= diff --git a/internal/database/database.go b/internal/database/database.go index b01f103..1bb7329 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -62,6 +62,8 @@ type Asset struct { Key string `bun:"key"` Size int64 `bun:"size"` Type string `bun:"type"` + CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` + UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"` OrganizationID int64 `bun:"organization_id"` Organization *Organization `bun:"rel:belongs-to,join:organization_id=id"` } diff --git a/internal/database/query/file/file_query.go b/internal/database/query/file/file_query.go index 32224c8..db008d8 100644 --- a/internal/database/query/file/file_query.go +++ b/internal/database/query/file/file_query.go @@ -2,11 +2,14 @@ package filequery import ( "context" + "database/sql" + "errors" "github.com/fivemanage/lite/internal/database" "github.com/uptrace/bun" ) +// todo: rename to Insert func Create(ctx context.Context, db *bun.DB, file *database.Asset) (bun.Tx, error) { tx, err := db.BeginTx(ctx, nil) _, err = tx.NewInsert().Model(file).Exec(ctx) @@ -16,3 +19,44 @@ func Create(ctx context.Context, db *bun.DB, file *database.Asset) (bun.Tx, erro return tx, nil } + +func FindStorageFiles(ctx context.Context, db *bun.DB, organizationID, search string) ([]*database.Asset, error) { + var files []*database.Asset + + // no, this does not scale at all, but...soonTM + // we are also missing indexes I need to create migrations for + sb := db.NewSelect(). + Model(&files). + Where("organization_id = ?", organizationID). + Order("created_at DESC") + + if search != "" { + // not my proudest moment + sb = sb.Where("name LIKE ?", "%"+search+"%") + } + + err := sb.Scan(ctx) + if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + + return nil, err + } + + return files, nil +} + +func FindTotalStorageCount(ctx context.Context, db *bun.DB, organizationID string) (int64, error) { + var count int64 + + _, err := db.NewSelect(). + Model((*database.Asset)(nil)). + Where("organization_id = ?", organizationID). + ScanAndCount(ctx, count) + if err != nil { + return 0, err + } + + return count, nil +} diff --git a/internal/http/httputil/httputil.go b/internal/http/httputil/httputil.go index 76f2265..03873b9 100644 --- a/internal/http/httputil/httputil.go +++ b/internal/http/httputil/httputil.go @@ -5,9 +5,27 @@ type response struct { Data any `json:"data"` } +type errorResponse struct { + Status string `json:"status"` + Data errorData `json:"data"` +} + +type errorData struct { + Message string `json:"message"` +} + func Response(data any) *response { return &response{ Status: "ok", Data: data, } } + +func ErrorResponse(message string) *errorResponse { + return &errorResponse{ + Status: "error", + Data: errorData{ + Message: message, + }, + } +} diff --git a/internal/http/internalapi/internal_api.go b/internal/http/internalapi/internal_api.go index 3ed02d8..0c4dbfe 100644 --- a/internal/http/internalapi/internal_api.go +++ b/internal/http/internalapi/internal_api.go @@ -3,15 +3,17 @@ package internalapi import ( "github.com/fivemanage/lite/internal/http/middleware" "github.com/fivemanage/lite/internal/service/auth" + "github.com/fivemanage/lite/internal/service/file" "github.com/fivemanage/lite/internal/service/organization" "github.com/fivemanage/lite/internal/service/token" "github.com/labstack/echo/v4" ) -func Add(group *echo.Group, authService *auth.Auth, tokenService *token.Service, organizationService *organization.Service) { +func Add(group *echo.Group, authService *auth.Auth, tokenService *token.Service, organizationService *organization.Service, fileService *file.Service) { group.Use(middleware.Session(authService)) registerAuthApi(group, authService) registerTokensApi(group, tokenService) registerOrganizationApi(group, organizationService) + registerStorageApi(group, fileService) } diff --git a/internal/http/internalapi/storage_group.go b/internal/http/internalapi/storage_group.go new file mode 100644 index 0000000..6480966 --- /dev/null +++ b/internal/http/internalapi/storage_group.go @@ -0,0 +1,32 @@ +package internalapi + +import ( + "errors" + "net/http" + + "github.com/fivemanage/lite/internal/http/httputil" + "github.com/fivemanage/lite/internal/service/file" + "github.com/labstack/echo/v4" +) + +func registerStorageApi(group *echo.Group, fileService *file.Service) { + group.GET("/storage/:organizationId", func(c echo.Context) error { + ctx := c.Request().Context() + + organizationID := c.Param("organizationId") + search := c.QueryParam("search") + + files, err := fileService.ListStorageFiles(ctx, organizationID, search) + if err != nil { + if errors.Is(err, file.ListStorageError{}) { + return echo.NewHTTPError(http.StatusInternalServerError, + httputil.ErrorResponse("Failed to list storage files"), + ) + } + + return echo.NewHTTPError(http.StatusInternalServerError, httputil.ErrorResponse(err.Error())) + } + + return c.JSON(200, httputil.Response(files)) + }) +} diff --git a/internal/http/middleware/session.go b/internal/http/middleware/session.go index a6fa3f4..2b66ad9 100644 --- a/internal/http/middleware/session.go +++ b/internal/http/middleware/session.go @@ -1,7 +1,6 @@ package middleware import ( - "fmt" "net/http" "strings" @@ -14,8 +13,6 @@ func Session(authService *auth.Auth) echo.MiddlewareFunc { return func(c echo.Context) error { ctx := c.Request().Context() - fmt.Println(c.Request().URL.Path) - // ignore /auth endpoints if strings.HasPrefix(c.Request().URL.Path, "/api/auth") { return next(c) } diff --git a/internal/http/server.go b/internal/http/server.go index 367448b..79789d5 100644 --- a/internal/http/server.go +++ b/internal/http/server.go @@ -29,9 +29,9 @@ type Server struct { // TODO: Add sentry for monitoring. There should be an opt-out option. func NewServer( - authservice *auth.Auth, - tokenservice *token.Service, - fileservice *file.Service, + authService *auth.Auth, + tokenService *token.Service, + fileService *file.Service, organizationService *organization.Service, ) *echo.Echo { app := echo.New() @@ -40,7 +40,6 @@ func NewServer( // not good, not bad app.Validator = &_validator.CustomValidator{Validator: validator.New()} app.Use(middleware.Recover()) - app.Use(middleware.Logger()) app.Use(middleware.StaticWithConfig(middleware.StaticConfig{ Filesystem: getFileSystem("dist"), @@ -54,11 +53,12 @@ func NewServer( internalapi.Add( apiGroup, - authservice, - tokenservice, + authService, + tokenService, organizationService, + fileService, ) - publicapi.Add(apiGroup, fileservice) + publicapi.Add(apiGroup, fileService) return app } diff --git a/internal/service/file/error.go b/internal/service/file/error.go new file mode 100644 index 0000000..c86f0dd --- /dev/null +++ b/internal/service/file/error.go @@ -0,0 +1,16 @@ +package file + +import "fmt" + +type ListStorageError struct { + ErrorMsg string +} + +func (e ListStorageError) Error() string { + return fmt.Sprintf("failed to list storage files: %s", e.ErrorMsg) +} + +func (ListStorageError) Is(target error) bool { + _, ok := target.(ListStorageError) + return ok +} diff --git a/internal/service/file/file_service.go b/internal/service/file/file_service.go index d44bef3..07ac5c7 100644 --- a/internal/service/file/file_service.go +++ b/internal/service/file/file_service.go @@ -2,14 +2,13 @@ package file import ( "context" - "fmt" "mime/multipart" - "github.com/fivemanage/lite/internal/crypt" + "github.com/fivemanage/lite/api" "github.com/fivemanage/lite/internal/database" - filequery "github.com/fivemanage/lite/internal/database/query/file" + "github.com/fivemanage/lite/internal/database/query/file" "github.com/fivemanage/lite/internal/storage" - "github.com/gabriel-vasile/mimetype" + "github.com/sirupsen/logrus" "github.com/uptrace/bun" ) @@ -33,18 +32,18 @@ func (s *Service) CreateFile( fileHeader *multipart.FileHeader, ) error { var err error - key, contentType, err := generateFileKey(organizationID, fileType, file, fileHeader) + key, contentType, err := generateFileKey(organizationID, file, fileHeader) if err != nil { return err } - dbFile := &database.Asset{ + asset := &database.Asset{ Type: fileType, Size: fileHeader.Size, Key: key, } - tx, err := filequery.Create(ctx, s.db, dbFile) + tx, err := filequery.Create(ctx, s.db, asset) if err != nil { return err } @@ -61,20 +60,31 @@ func (s *Service) CreateFile( return nil } -func generateFileKey(organizationID, fileType string, file multipart.File, fileHeader *multipart.FileHeader) (string, string, error) { - filename, err := crypt.GenerateFilename() - if err != nil { - return "", "", err - } +func (s *Service) ListStorageFiles( + ctx context.Context, + organizationID string, + search string, +) ([]*api.Asset, error) { + var err error - buf := make([]byte, fileHeader.Size) - _, err = file.Read(buf) + files, err := filequery.FindStorageFiles(ctx, s.db, organizationID, search) if err != nil { - return "", "", err + storageError := &ListStorageError{ + ErrorMsg: err.Error(), + } + logrus.WithError(storageError). + WithField("organization_id", organizationID). + Error("FileService.ListStorageFiles") + return nil, storageError } - mime := mimetype.Detect(buf) - key := fmt.Sprintf("%s/%s.%s", organizationID, filename, mime.Extension()) + assets := make([]*api.Asset, len(files)) + for i, file := range files { + assets[i] = &api.Asset{ + ID: file.ID, + Type: file.Type, + Key: file.Key, + Size: file.Size, + } + } - return key, mime.String(), nil -} diff --git a/internal/service/file/key.go b/internal/service/file/key.go new file mode 100644 index 0000000..07823a2 --- /dev/null +++ b/internal/service/file/key.go @@ -0,0 +1,27 @@ +package file + +import ( + "fmt" + "mime/multipart" + + "github.com/fivemanage/lite/internal/crypt" + "github.com/gabriel-vasile/mimetype" +) + +func generateFileKey(organizationID string, file multipart.File, fileHeader *multipart.FileHeader) (string, string, error) { + filename, err := crypt.GenerateFilename() + if err != nil { + return "", "", err + } + + buf := make([]byte, fileHeader.Size) + _, err = file.Read(buf) + if err != nil { + return "", "", err + } + + mime := mimetype.Detect(buf) + key := fmt.Sprintf("%s/%s.%s", organizationID, filename, mime.Extension()) + + return key, mime.String(), nil +} diff --git a/web/package.json b/web/package.json index 1f61c8e..d6a902f 100644 --- a/web/package.json +++ b/web/package.json @@ -12,6 +12,7 @@ }, "dependencies": { "@hookform/resolvers": "^3.10.0", + "@radix-ui/react-checkbox": "^1.3.1", "@radix-ui/react-dialog": "^1.1.5", "@radix-ui/react-dropdown-menu": "^2.1.6", "@radix-ui/react-label": "^2.1.1", @@ -19,8 +20,10 @@ "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.7", "@tanstack/react-query": "^5.59.15", + "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", + "date-fns": "^4.1.0", "geist": "^1.3.1", "lucide-react": "^0.428.0", "react": "^19.0.0", diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 76f5f6a..0fa3b72 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -11,6 +11,9 @@ importers: '@hookform/resolvers': specifier: ^3.10.0 version: 3.10.0(react-hook-form@7.54.2(react@19.0.0)) + '@radix-ui/react-checkbox': + specifier: ^1.3.1 + version: 1.3.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) '@radix-ui/react-dialog': specifier: ^1.1.5 version: 1.1.5(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -32,12 +35,18 @@ importers: '@tanstack/react-query': specifier: ^5.59.15 version: 5.59.15(react@19.0.0) + '@tanstack/react-table': + specifier: ^8.21.3 + version: 8.21.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0) class-variance-authority: specifier: ^0.7.0 version: 0.7.0 clsx: specifier: ^2.1.1 version: 2.1.1 + date-fns: + specifier: ^4.1.0 + version: 4.1.0 geist: specifier: ^1.3.1 version: 1.3.1(next@15.2.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)) @@ -521,6 +530,9 @@ packages: '@radix-ui/primitive@1.1.1': resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} + '@radix-ui/primitive@1.1.2': + resolution: {integrity: sha512-XnbHrrprsNqZKQhStrSwgRUQzoCI1glLzdw79xiZPoofhGICeZRSQ3dIxAKH1gb3OHfNf4d6f+vAv3kil2eggA==} + '@radix-ui/react-arrow@1.1.1': resolution: {integrity: sha512-NaVpZfmv8SKeZbn4ijN2V3jlHA9ngBG16VnIIm22nUR0Yk8KUALyBxT3KYEUnNuch9sTE8UTsS3whzBgKOL30w==} peerDependencies: @@ -547,6 +559,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-checkbox@1.3.1': + resolution: {integrity: sha512-xTaLKAO+XXMPK/BpVTSaAAhlefmvMSACjIhK9mGsImvX2ljcTDm8VGR1CuS1uYcNdR5J+oiOhoJZc5un6bh3VQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-collection@1.1.2': resolution: {integrity: sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==} peerDependencies: @@ -578,6 +603,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-context@1.1.1': resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==} peerDependencies: @@ -587,6 +621,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-dialog@1.1.5': resolution: {integrity: sha512-LaO3e5h/NOEL4OfXjxD43k9Dx+vn+8n+PCFt6uhX/BADFflllyv3WJG6rgvvSVBxpTch938Qq/LGc2MMxipXPw==} peerDependencies: @@ -783,6 +826,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-presence@1.1.4': + resolution: {integrity: sha512-ueDqRbdc4/bkaQT3GIpLQssRlFgWaL/U2z/S31qRwwLWoxHLgry3SIfCwhxeQNbirEUXFa+lq3RL3oBYXtcmIA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-primitive@2.0.1': resolution: {integrity: sha512-sHCWTtxwNn3L3fH8qAfnF3WbUZycW93SM1j3NFDzXBiz8D6F5UTTy8G1+WFEaiCdvCVRJWj6N2R4Xq6HdiHmDg==} peerDependencies: @@ -809,6 +865,19 @@ packages: '@types/react-dom': optional: true + '@radix-ui/react-primitive@2.1.2': + resolution: {integrity: sha512-uHa+l/lKfxuDD2zjN/0peM/RhhSmRjr5YWdk/37EnSv1nJ88uvG85DPexSm8HdFQROd2VdERJ6ynXbkCFi+APw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + '@radix-ui/react-roving-focus@1.1.2': resolution: {integrity: sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==} peerDependencies: @@ -862,6 +931,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-slot@1.2.2': + resolution: {integrity: sha512-y7TBO4xN4Y94FvcWIOIh18fM4R1A8S4q1jhoz4PNzOoHsFcN8pogcFmZrTYAm4F9VRUrWP/Mw7xSKybIeRI+CQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-tooltip@1.1.7': resolution: {integrity: sha512-ss0s80BC0+g0+Zc53MvilcnTYSOi4mSuFWBPYPuTOFGjx+pUU+ZrmamMNwS56t8MTFlniA5ocjd4jYm/CdhbOg==} peerDependencies: @@ -893,6 +971,24 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-escape-keydown@1.1.0': resolution: {integrity: sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==} peerDependencies: @@ -911,6 +1007,24 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-use-rect@1.1.0': resolution: {integrity: sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==} peerDependencies: @@ -929,6 +1043,15 @@ packages: '@types/react': optional: true + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@radix-ui/react-visually-hidden@1.1.1': resolution: {integrity: sha512-vVfA2IZ9q/J+gEamvj761Oq1FpWgCDaNOOIfbPVp2MVPLEomUr5+Vf7kJGwQ24YxZSlQVar7Bes8kyTo5Dshpg==} peerDependencies: @@ -1131,6 +1254,17 @@ packages: peerDependencies: react: ^18 || ^19 + '@tanstack/react-table@8.21.3': + resolution: {integrity: sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==} + engines: {node: '>=12'} + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + '@tanstack/table-core@8.21.3': + resolution: {integrity: sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==} + engines: {node: '>=12'} + '@types/cookie@0.6.0': resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==} @@ -1373,6 +1507,9 @@ packages: csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + date-fns@4.1.0: + resolution: {integrity: sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==} + debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -2520,6 +2657,8 @@ snapshots: '@radix-ui/primitive@1.1.1': {} + '@radix-ui/primitive@1.1.2': {} + '@radix-ui/react-arrow@1.1.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -2538,6 +2677,22 @@ snapshots: '@types/react': 19.0.8 '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-checkbox@1.3.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/primitive': 1.1.2 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.8)(react@19.0.0) + '@radix-ui/react-context': 1.1.2(@types/react@19.0.8)(react@19.0.0) + '@radix-ui/react-presence': 1.1.4(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-primitive': 2.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.0.8)(react@19.0.0) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.0.8)(react@19.0.0) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.0.8)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.8 + '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-collection@1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-compose-refs': 1.1.1(@types/react@19.0.8)(react@19.0.0) @@ -2562,12 +2717,24 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.0.8)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.8 + '@radix-ui/react-context@1.1.1(@types/react@19.0.8)(react@19.0.0)': dependencies: react: 19.0.0 optionalDependencies: '@types/react': 19.0.8 + '@radix-ui/react-context@1.1.2(@types/react@19.0.8)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.8 + '@radix-ui/react-dialog@1.1.5(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -2773,6 +2940,16 @@ snapshots: '@types/react': 19.0.8 '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-presence@1.1.4(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.8)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.8)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.8 + '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-primitive@2.0.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-slot': 1.1.1(@types/react@19.0.8)(react@19.0.0) @@ -2791,6 +2968,15 @@ snapshots: '@types/react': 19.0.8 '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-primitive@2.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@radix-ui/react-slot': 1.2.2(@types/react@19.0.8)(react@19.0.0) + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + optionalDependencies: + '@types/react': 19.0.8 + '@types/react-dom': 19.0.3(@types/react@19.0.8) + '@radix-ui/react-roving-focus@1.1.2(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -2838,6 +3024,13 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 + '@radix-ui/react-slot@1.2.2(@types/react@19.0.8)(react@19.0.0)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.0.8)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.8 + '@radix-ui/react-tooltip@1.1.7(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/primitive': 1.1.1 @@ -2871,6 +3064,21 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.0.8)(react@19.0.0)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.0.8)(react@19.0.0) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.8)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.8 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.0.8)(react@19.0.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.8)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.8 + '@radix-ui/react-use-escape-keydown@1.1.0(@types/react@19.0.8)(react@19.0.0)': dependencies: '@radix-ui/react-use-callback-ref': 1.1.0(@types/react@19.0.8)(react@19.0.0) @@ -2884,6 +3092,18 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.0.8)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.8 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.0.8)(react@19.0.0)': + dependencies: + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.8 + '@radix-ui/react-use-rect@1.1.0(@types/react@19.0.8)(react@19.0.0)': dependencies: '@radix-ui/rect': 1.1.0 @@ -2898,6 +3118,13 @@ snapshots: optionalDependencies: '@types/react': 19.0.8 + '@radix-ui/react-use-size@1.1.1(@types/react@19.0.8)(react@19.0.0)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.0.8)(react@19.0.0) + react: 19.0.0 + optionalDependencies: + '@types/react': 19.0.8 + '@radix-ui/react-visually-hidden@1.1.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': dependencies: '@radix-ui/react-primitive': 2.0.1(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -3038,6 +3265,14 @@ snapshots: '@tanstack/query-core': 5.59.13 react: 19.0.0 + '@tanstack/react-table@8.21.3(react-dom@19.0.0(react@19.0.0))(react@19.0.0)': + dependencies: + '@tanstack/table-core': 8.21.3 + react: 19.0.0 + react-dom: 19.0.0(react@19.0.0) + + '@tanstack/table-core@8.21.3': {} + '@types/cookie@0.6.0': {} '@types/estree@1.0.6': {} @@ -3298,6 +3533,8 @@ snapshots: csstype@3.1.3: {} + date-fns@4.1.0: {} + debug@4.3.4: dependencies: ms: 2.1.2 diff --git a/web/src/App.tsx b/web/src/App.tsx index c5e8ad3..3c83945 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -8,7 +8,7 @@ import { ThemeProvider } from "./components/theme/ThemeProvider"; import { ProtectedRoute } from "./features/auth/routes/ProtectedRoute"; import { NewOrganizationRoute } from "./features/organizations/routes/NewOrganizationRoute"; import { OrganizationSelectRoute } from "./features/organizations/routes/OrganizationSelectRoute"; -import { FilesRoute } from "./features/files/FilesRoute"; +const StorageRoute = lazy(() => import("./features/files/StorageRoute")); const TokensRoute = lazy(() => import("./features/tokens/routes/TokensRoute")); const queryClient = new QueryClient(); @@ -33,7 +33,7 @@ function App() { } /> {/* Should probably wrap a suspense around this */} } /> - } /> + } /> diff --git a/web/src/components/data/DataTable.tsx b/web/src/components/data/DataTable.tsx new file mode 100644 index 0000000..3b8a671 --- /dev/null +++ b/web/src/components/data/DataTable.tsx @@ -0,0 +1,172 @@ +import { + type ColumnDef, + type PaginationState, + type Row, + type RowSelectionState, + flexRender, + getCoreRowModel, + getPaginationRowModel, + useReactTable, +} from "@tanstack/react-table"; +import { useEffect, useRef, useState } from "react"; + +import { Button } from "@/components/ui/Button"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/Table"; +import { useLocation, useSearchParams, useNavigate } from "react-router"; +import { Asset } from "@/typings/asset"; +//import { DataDeleteDialog } from "./DataDeleteDialog"; + +interface DataTableProps { + data: T[]; + columns: ColumnDef[]; + totalCount: number; + isLoading: boolean; + onDelete: (rows: Row[]) => Promise; + rowSelection: RowSelectionState; + setRowSelection: React.Dispatch>; + showDeleteModal: boolean; + setShowDeleteModal: React.Dispatch>; +} + +export function DataTable({ + data, + totalCount, + columns, + rowSelection, + setRowSelection, +}: DataTableProps) { + const router = useNavigate(); + const pathname = useLocation(); + const [searchParams] = useSearchParams(); + + const [pagination, setPagination] = useState({ + pageSize: 20, + pageIndex: Number(searchParams.get("page")) || 0, + }); + + const tableContainerRef = useRef(null); + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onPaginationChange: setPagination, + onRowSelectionChange: setRowSelection, + pageCount: Math.ceil(totalCount / 20), + manualPagination: true, + state: { + pagination, + rowSelection: rowSelection, + }, + }); + + /*const handleDeleteSelection = async () => { + const rows = table.getFilteredSelectedRowModel().rows; + await onDelete(rows); + }; */ + + useEffect(() => { + if (pagination) { + const params = new URLSearchParams(searchParams); + params.set("page", `${pagination.pageIndex}`); + //router.replace(`${pathname}?${params.toString()}`); + } + }, [pagination, router, searchParams, pathname]); + + return ( +
+ {/*table.getFilteredSelectedRowModel().rows.length ? ( + + selectedRows={table.getFilteredSelectedRowModel().rows} + showModal={showDeleteModal} + setShowDeleteModal={setShowDeleteModal} + onDelete={handleDeleteSelection} + isLoading={isLoading} + /> + ) : null */} +
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+
+ + + {table.getHeaderGroups()?.map((headerGroup) => ( + + {headerGroup.headers?.map((header) => { + return ( + + {header.isPlaceholder ? null : ( +
+ {flexRender( + header.column.columnDef.header, + header.getContext(), + )} +
+ )} +
+ ); + })} +
+ ))} +
+ + {table.getRowModel().rows.length ? ( + table.getRowModel().rows.map((row) => { + return ( + + {row.getVisibleCells().map((cell) => { + return ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} + + ); + })} + + ); + }) + ) : ( + + + No results. + + + )} + +
+
+
+ + +
+
+ ); +} diff --git a/web/src/components/data/DataTableSkeleton.tsx b/web/src/components/data/DataTableSkeleton.tsx new file mode 100644 index 0000000..abf36ce --- /dev/null +++ b/web/src/components/data/DataTableSkeleton.tsx @@ -0,0 +1,167 @@ +import { cn } from "@/lib/utils"; +import { Skeleton } from "@/components/ui/Skeleton"; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/Table"; + +interface DataTableSkeletonProps extends React.HTMLAttributes { + /** + * The number of columns in the table. + * @type number + */ + columnCount: number; + + /** + * The number of rows in the table. + * @default 10 + * @type number | undefined + */ + rowCount?: number; + + /** + * The number of searchable columns in the table. + * @default 0 + * @type number | undefined + */ + searchableColumnCount?: number; + + /** + * The number of filterable columns in the table. + * @default 0 + * @type number | undefined + */ + filterableColumnCount?: number; + + /** + * Flag to show the table view options. + * @default undefined + * @type boolean | undefined + */ + showViewOptions?: boolean; + + /** + * The width of each cell in the table. + * The length of the array should be equal to the columnCount. + * Any valid CSS width value is accepted. + * @default ["auto"] + * @type string[] | undefined + */ + cellWidths?: string[]; + + /** + * Flag to show the pagination bar. + * @default true + * @type boolean | undefined + */ + withPagination?: boolean; + + /** + * Flag to prevent the table cells from shrinking. + * @default false + * @type boolean | undefined + */ + shrinkZero?: boolean; +} + +export function DataTableSkeleton(props: DataTableSkeletonProps) { + const { + columnCount, + rowCount = 10, + searchableColumnCount = 0, + filterableColumnCount = 0, + showViewOptions = true, + cellWidths = ["auto"], + withPagination = true, + shrinkZero = false, + className, + ...skeletonProps + } = props; + + return ( +
+
+
+ {searchableColumnCount > 0 + ? Array.from({ length: searchableColumnCount }).map((_, i) => ( + + )) + : null} + {filterableColumnCount > 0 + ? Array.from({ length: filterableColumnCount }).map((_, i) => ( + + )) + : null} +
+ {showViewOptions ? ( + + ) : null} +
+
+ + + {Array.from({ length: 1 }).map((_, i) => ( + + {Array.from({ length: columnCount }).map((_, j) => ( + + + + ))} + + ))} + + + {Array.from({ length: rowCount }).map((_, i) => ( + + {Array.from({ length: columnCount }).map((_, j) => ( + + + + ))} + + ))} + +
+
+ {withPagination ? ( +
+ +
+
+ + +
+
+ +
+
+ + + + +
+
+
+ ) : null} +
+ ); +} diff --git a/web/src/components/ui/Checkbox.tsx b/web/src/components/ui/Checkbox.tsx new file mode 100644 index 0000000..3f61f28 --- /dev/null +++ b/web/src/components/ui/Checkbox.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; +import * as CheckboxPrimitive from "@radix-ui/react-checkbox"; +import { Check } from "lucide-react"; + +import { cn } from "@/lib/utils"; + +const Checkbox = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + + +)); +Checkbox.displayName = CheckboxPrimitive.Root.displayName; + +export { Checkbox }; diff --git a/web/src/features/app/components/AppSidebar.tsx b/web/src/features/app/components/AppSidebar.tsx index 07cba69..d0d68fb 100644 --- a/web/src/features/app/components/AppSidebar.tsx +++ b/web/src/features/app/components/AppSidebar.tsx @@ -20,11 +20,10 @@ import { } from "@/components/ui/Sidebar"; import { NavLink } from "react-router"; -// Menu items. const items = [ { - title: "Files", - url: "files", + title: "Storage", + url: "storage", icon: Folders, comingSoon: false, }, diff --git a/web/src/features/files/FilesRoute.tsx b/web/src/features/files/FilesRoute.tsx deleted file mode 100644 index 1cc6302..0000000 --- a/web/src/features/files/FilesRoute.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export function FilesRoute() { - return
; -} diff --git a/web/src/features/files/StorageRoute.tsx b/web/src/features/files/StorageRoute.tsx new file mode 100644 index 0000000..bc27f0a --- /dev/null +++ b/web/src/features/files/StorageRoute.tsx @@ -0,0 +1,13 @@ +import { Suspense } from "react"; +import { AssetList } from "./components/AssetList"; +import { DataTableSkeleton } from "@/components/data/DataTableSkeleton"; + +export default function StorageRoute() { + return ( +
+ }> + + +
+ ); +} diff --git a/web/src/features/files/api/useStorageFiles.ts b/web/src/features/files/api/useStorageFiles.ts new file mode 100644 index 0000000..fd15bbe --- /dev/null +++ b/web/src/features/files/api/useStorageFiles.ts @@ -0,0 +1,24 @@ +import { Asset } from "@/typings/asset"; +import { AssetParams } from "@/typings/assets"; +import { ApiError, fetchApi } from "@/utils/http-util"; +import { useSuspenseQuery } from "@tanstack/react-query"; + +export function useStorageFiles(params: AssetParams) { + const { data, isLoading } = useSuspenseQuery({ + queryKey: ["storage", params.organizationId], + queryFn: async () => { + try { + return await fetchApi(`/api/storage/${params.organizationId}`); + } catch (error) { + if (error instanceof ApiError) { + throw new Error(error.message); + } + } + }, + }); + + return { + data, + isLoading, + }; +} diff --git a/web/src/features/files/components/AssetList.tsx b/web/src/features/files/components/AssetList.tsx new file mode 100644 index 0000000..b18bd62 --- /dev/null +++ b/web/src/features/files/components/AssetList.tsx @@ -0,0 +1,134 @@ +import type { ColumnDef, Row, RowSelectionState } from "@tanstack/react-table"; +import { useMemo, useState } from "react"; +//import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"; +import { DataTable } from "@/components/data/DataTable"; +import { Asset } from "@/typings/asset"; +import { useParams, useSearchParams } from "react-router"; +import { Params } from "@/typings/router"; +import { assetColumns } from "./assst-columns"; +import { useStorageFiles } from "../api/useStorageFiles"; +//import { useToast } from "@fivemanage/ui/hooks/use-toast"; +//import { AssetGridSkeleton } from "./asset-grid-skeleton"; +//import { AssetGridView } from "./asset-grid-view"; + +export function AssetList() { + const [rowSelection, setRowSelection] = useState({}); + const [showDeleteModal, setShowDeleteModal] = useState(false); + + const columns = useMemo[]>(() => assetColumns(), []); + + //const { toast } = useToast(); + //const trpcUtils = api.useUtils(); + + const [searchParams] = useSearchParams(); + const params = useParams(); + + const view = searchParams?.get("view") ?? "list"; + //const assetType = searchParams?.get("type") ?? "all"; + //const fromDate = searchParams.get("from") ?? new Date().toString(); + //const toDate = searchParams.get("to") ?? new Date().toString(); + //const page = searchParams.get("page"); + const search = searchParams.get("search"); + + /*const { data, isLoading } = api.assets.list.useQuery({ + //toDate, + //fromDate, + search: search, + organizationId: params.organizationId as string, + //currentPage: Number(page), + //type: assetType !== "all" ? assetType : undefined, + }); */ + + const { data } = useStorageFiles({ + organizationId: params.organizationId as string, + search: search ?? undefined, + }); + + /*const { mutateAsync, isPending: isDeleteLoading } = + api.assets.deleteMany.useMutation({ + onSuccess() { + setShowDeleteModal(false); + setRowSelection({}); + + //void trpcUtils.assets.list.invalidate(); + //router.refresh(); + + toast({ + title: "Success", + description: "Assets successfully deleted.", + }); + }, + onError: () => { + setShowDeleteModal(false); + setRowSelection({}); + + void trpcUtils.assets.list.invalidate(); + router.refresh(); + + toast({ + title: "Error", + description: "Failed to delete assets. Please try again later.", + }); + }, + }); */ + + /*const handleDelete = async (rows: Row[]) => { + try { + await mutateAsync({ + teamId: queryParams.teamId as string, + files: rows.map((r) => { + return { + id: r.original.id, + key: r.original.key ?? "", + type: r.original.type, + }; + }), + }); + } catch (err) { + console.error(err); + } + }; */ + + /*if (isLoading) { + return view === "list" ? ( + + ) : ( + { + + } + ); + } */ + + return ( +
+
+ {/*data && view === "grid" && ( + + )*/} +
+
+ {data && view === "list" && ( + {}} + /> + )} +
+
+ ); +} diff --git a/web/src/features/files/components/assst-columns.tsx b/web/src/features/files/components/assst-columns.tsx new file mode 100644 index 0000000..83aeb36 --- /dev/null +++ b/web/src/features/files/components/assst-columns.tsx @@ -0,0 +1,128 @@ +import { Checkbox } from "@/components/ui/Checkbox"; +import { + Tooltip, + TooltipProvider, + TooltipTrigger, + TooltipContent, +} from "@/components/ui/Tooltip"; +import { copyToClipboard } from "@/lib/browser/keyboard"; +import { Asset } from "@/typings/asset"; +import { ColumnDef } from "@tanstack/react-table"; +import { format } from "date-fns"; +import { + ClipboardCopy, + FileAudioIcon, + ImageIcon, + VideoIcon, +} from "lucide-react"; +import { Link } from "react-router"; + +export function assetColumns(): ColumnDef[] { + return [ + /*{ + id: "select", + header: ({ table }) => ( +
+ + table.toggleAllPageRowsSelected(!!value) + } + aria-label="Select all" + /> +
+ ), + cell: ({ row }) => ( +
e.stopPropagation()}> + { + row.toggleSelected(!!value); + }} + aria-label="Select row" + /> +
+ ), + }, */ + { + accessorKey: "id", + header: "Asset", + cell: (info) => { + const asset = info.row.original; + const displayValue = asset.key?.replace(/^(image|video|audio)\//, ""); + const type = asset.type; + + let Icon = ImageIcon; + const href = `storage/asset/${asset.id}`; + + if (type === "video") { + Icon = VideoIcon; + } else if (type === "audio") { + Icon = FileAudioIcon; + } + + return ( + + + {displayValue} + + ); + }, + }, + { + accessorKey: "type", + header: "Type", + cell: (info) => { + const type = info.getValue() as string; + return type.charAt(0).toUpperCase() + type.slice(1); + }, + }, + { + accessorKey: "createdAt", + header: "Created At", + cell: (info) => + format(new Date(info.getValue() as Date), "yyyy-MM-dd HH:mm:ss"), + }, + { + accessorKey: "url", + header: "URL", + cell: (info) => ( + + + + + + +

Copy URL

+
+
+
+ ), + }, + /*{ + id: "delete", + header: ({ table }) => ( +
+ +
+ ), + }, */ + ]; +} diff --git a/web/src/lib/browser/keyboard.ts b/web/src/lib/browser/keyboard.ts new file mode 100644 index 0000000..5605c1d --- /dev/null +++ b/web/src/lib/browser/keyboard.ts @@ -0,0 +1,7 @@ +export function copyToClipboard( + e: React.MouseEvent, + value: string, +) { + e.stopPropagation(); + void navigator.clipboard.writeText(value); +} diff --git a/web/src/typings/asset.ts b/web/src/typings/asset.ts new file mode 100644 index 0000000..0a64d0c --- /dev/null +++ b/web/src/typings/asset.ts @@ -0,0 +1,6 @@ +export interface Asset { + id: string; + key: string; + size: number; + type: string; +} diff --git a/web/src/typings/assets.ts b/web/src/typings/assets.ts new file mode 100644 index 0000000..5313fbd --- /dev/null +++ b/web/src/typings/assets.ts @@ -0,0 +1,4 @@ +export type AssetParams = { + search?: string; + organizationId: string | undefined; +}; From 65f9f3f0e60f67aebc08c9266cad3eb058ab80c1 Mon Sep 17 00:00:00 2001 From: itschip Date: Sun, 18 May 2025 22:16:47 +0200 Subject: [PATCH 17/96] hey, you can upload files now --- api/file.go | 18 +- internal/database/database.go | 2 +- internal/database/query/file/file_query.go | 8 +- internal/http/httputil/formdata.go | 3 + internal/http/httputil/mime.go | 29 + internal/http/internalapi/storage_group.go | 40 +- internal/http/publicapi/media_group.go | 6 +- internal/service/file/error.go | 13 + internal/service/file/file_service.go | 123 +- internal/service/file/key.go | 21 +- internal/storage/s3/s3.go | 10 +- web/components.json | 36 +- web/package.json | 7 +- web/pnpm-lock.yaml | 931 +++++++-------- web/postcss.config.js | 6 - web/src/components/theme/ModeToggle.tsx | 2 +- web/src/components/ui/Breadcrumb.tsx | 170 ++- web/src/components/ui/Card.tsx | 148 +-- web/src/components/ui/Checkbox.tsx | 50 +- web/src/components/ui/Dialog.tsx | 207 ++-- web/src/components/ui/DropdownMenu.tsx | 199 ---- web/src/components/ui/Form.tsx | 152 ++- web/src/components/ui/Input.tsx | 40 +- web/src/components/ui/Label.tsx | 42 +- web/src/components/ui/Separator.tsx | 37 +- web/src/components/ui/Sheet.tsx | 225 ++-- web/src/components/ui/Sidebar.tsx | 1051 ++++++++--------- web/src/components/ui/Skeleton.tsx | 14 +- web/src/components/ui/Table.tsx | 195 ++- web/src/components/ui/Tooltip.tsx | 77 +- web/src/components/ui/button.tsx | 74 +- web/src/components/ui/dropdown-menu.tsx | 255 ++++ web/src/features/app/components/AppLayout.tsx | 6 +- web/src/features/auth/components/AuthForm.tsx | 8 +- web/src/features/files/StorageRoute.tsx | 2 + web/src/features/files/api/useStorageFiles.ts | 6 +- web/src/features/files/api/useUploadFile.tsx | 28 + .../features/files/components/AssetList.tsx | 60 +- .../files/components/UploadDialog.tsx | 174 +++ .../files/components/assst-columns.tsx | 44 +- web/src/global.css | 219 ++-- web/src/hooks/use-mobile.ts | 19 + web/src/lib/utils.ts | 2 +- web/src/main.tsx | 6 +- web/src/typings/asset.ts | 5 + web/tailwind.config.js | 155 --- web/tsconfig.json | 9 +- web/vite.config.ts | 3 +- 48 files changed, 2571 insertions(+), 2366 deletions(-) create mode 100644 internal/http/httputil/mime.go delete mode 100644 web/postcss.config.js delete mode 100644 web/src/components/ui/DropdownMenu.tsx create mode 100644 web/src/components/ui/dropdown-menu.tsx create mode 100644 web/src/features/files/api/useUploadFile.tsx create mode 100644 web/src/features/files/components/UploadDialog.tsx create mode 100644 web/src/hooks/use-mobile.ts delete mode 100644 web/tailwind.config.js diff --git a/api/file.go b/api/file.go index dac2f27..8db306e 100644 --- a/api/file.go +++ b/api/file.go @@ -1,6 +1,9 @@ package api -import "mime/multipart" +import ( + "mime/multipart" + "time" +) type UploadFile struct { FileHeader *multipart.FileHeader @@ -8,13 +11,14 @@ type UploadFile struct { } type Asset struct { - ID string - Key string - Size int64 - Type string + ID string `json:"id"` + Key string `json:"key"` + Size int64 `json:"size"` + Type string `json:"type"` + CreatedAt time.Time `json:"createdAt"` } type AssetResponse struct { - StorageFiles []Asset `json:"storageFiles"` - Total int64 `json:"total"` + StorageFiles []*Asset `json:"files"` + TotalCount int `json:"totalCount"` } diff --git a/internal/database/database.go b/internal/database/database.go index 1bb7329..1414989 100644 --- a/internal/database/database.go +++ b/internal/database/database.go @@ -64,7 +64,7 @@ type Asset struct { Type string `bun:"type"` CreatedAt time.Time `bun:"created_at,nullzero,notnull,default:current_timestamp"` UpdatedAt time.Time `bun:"updated_at,nullzero,notnull,default:current_timestamp"` - OrganizationID int64 `bun:"organization_id"` + OrganizationID string `bun:"organization_id"` Organization *Organization `bun:"rel:belongs-to,join:organization_id=id"` } diff --git a/internal/database/query/file/file_query.go b/internal/database/query/file/file_query.go index db008d8..469d704 100644 --- a/internal/database/query/file/file_query.go +++ b/internal/database/query/file/file_query.go @@ -47,13 +47,11 @@ func FindStorageFiles(ctx context.Context, db *bun.DB, organizationID, search st return files, nil } -func FindTotalStorageCount(ctx context.Context, db *bun.DB, organizationID string) (int64, error) { - var count int64 - - _, err := db.NewSelect(). +func FindTotalStorageCount(ctx context.Context, db *bun.DB, organizationID string) (int, error) { + count, err := db.NewSelect(). Model((*database.Asset)(nil)). Where("organization_id = ?", organizationID). - ScanAndCount(ctx, count) + Count(ctx) if err != nil { return 0, err } diff --git a/internal/http/httputil/formdata.go b/internal/http/httputil/formdata.go index 4d55f5e..092c498 100644 --- a/internal/http/httputil/formdata.go +++ b/internal/http/httputil/formdata.go @@ -3,11 +3,14 @@ package httputil import ( "mime/multipart" "net/http" + + "github.com/sirupsen/logrus" ) func File(r *http.Request, key string) (file multipart.File, fileHeader *multipart.FileHeader, err error) { err = r.ParseMultipartForm(32 << 20) if err != nil { + logrus.WithError(err).Error("failed to parse multipart form") return nil, nil, err } diff --git a/internal/http/httputil/mime.go b/internal/http/httputil/mime.go new file mode 100644 index 0000000..1c8fcc6 --- /dev/null +++ b/internal/http/httputil/mime.go @@ -0,0 +1,29 @@ +package httputil + +import ( + "fmt" + "mime/multipart" + "strings" + + "github.com/gabriel-vasile/mimetype" +) + +func GetMimeDetails(fileHeader *multipart.FileHeader, file multipart.File) (string, string, string, error) { + var err error + var fileType string + + // Get the file type from the header + buf := make([]byte, fileHeader.Size) + _, err = file.Read(buf) + if err != nil { + return "", "", "", fmt.Errorf("error reading file: %w\n", err) + } + + mime := mimetype.Detect(buf) + mimeType := mime.String() + extension := mime.Extension() + + fileType = strings.Split(mime.String(), "/")[0] + + return mimeType, extension, fileType, nil +} diff --git a/internal/http/internalapi/storage_group.go b/internal/http/internalapi/storage_group.go index 6480966..96cad29 100644 --- a/internal/http/internalapi/storage_group.go +++ b/internal/http/internalapi/storage_group.go @@ -7,6 +7,7 @@ import ( "github.com/fivemanage/lite/internal/http/httputil" "github.com/fivemanage/lite/internal/service/file" "github.com/labstack/echo/v4" + "github.com/sirupsen/logrus" ) func registerStorageApi(group *echo.Group, fileService *file.Service) { @@ -16,7 +17,7 @@ func registerStorageApi(group *echo.Group, fileService *file.Service) { organizationID := c.Param("organizationId") search := c.QueryParam("search") - files, err := fileService.ListStorageFiles(ctx, organizationID, search) + assetData, err := fileService.ListStorageFiles(ctx, organizationID, search) if err != nil { if errors.Is(err, file.ListStorageError{}) { return echo.NewHTTPError(http.StatusInternalServerError, @@ -27,6 +28,41 @@ func registerStorageApi(group *echo.Group, fileService *file.Service) { return echo.NewHTTPError(http.StatusInternalServerError, httputil.ErrorResponse(err.Error())) } - return c.JSON(200, httputil.Response(files)) + return c.JSON(200, httputil.Response(assetData)) + }) + + group.POST("/storage/:organizationId/upload", func(c echo.Context) error { + ctx := c.Request().Context() + + organizationID := c.Param("organizationId") + + formFile, header, err := httputil.File(c.Request(), "file") + if err != nil { + logrus.WithField("organizationId", organizationID).Error("failed to get file from request") + + return echo.NewHTTPError(http.StatusInternalServerError, + httputil.ErrorResponse("Failed to get file from request"), + ) + } + + err = fileService.CreateStorageFile( + ctx, + organizationID, + formFile, + header, + ) + if err != nil { + logrus.WithError(err).WithField("organizationId", organizationID).Error("failed to create file") + + if errors.Is(err, file.UploadStorageError{}) { + return echo.NewHTTPError(http.StatusInternalServerError, + httputil.ErrorResponse("Failed to upload storage file"), + ) + } + + return echo.NewHTTPError(http.StatusInternalServerError, httputil.ErrorResponse(err.Error())) + } + + return c.JSON(http.StatusOK, httputil.Response("File uploaded successfully")) }) } diff --git a/internal/http/publicapi/media_group.go b/internal/http/publicapi/media_group.go index 4200523..18ce0ea 100644 --- a/internal/http/publicapi/media_group.go +++ b/internal/http/publicapi/media_group.go @@ -21,7 +21,7 @@ func registerMediaApi(group *echo.Group, fileService *file.Service) { }) } - err = fileService.CreateFile(ctx, "", "image", file, header) + err = fileService.CreateFile(ctx, "", file, header) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err) } @@ -40,7 +40,7 @@ func registerMediaApi(group *echo.Group, fileService *file.Service) { }) } - err = fileService.CreateFile(ctx, "", "video", file, header) + err = fileService.CreateFile(ctx, "", file, header) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err) } @@ -59,7 +59,7 @@ func registerMediaApi(group *echo.Group, fileService *file.Service) { }) } - err = fileService.CreateFile(ctx, "", "audio", file, header) + err = fileService.CreateFile(ctx, "", file, header) if err != nil { return echo.NewHTTPError(http.StatusInternalServerError, err) } diff --git a/internal/service/file/error.go b/internal/service/file/error.go index c86f0dd..ccec952 100644 --- a/internal/service/file/error.go +++ b/internal/service/file/error.go @@ -14,3 +14,16 @@ func (ListStorageError) Is(target error) bool { _, ok := target.(ListStorageError) return ok } + +type UploadStorageError struct { + ErrorMsg string +} + +func (e UploadStorageError) Error() string { + return fmt.Sprintf("failed to upload storage file: %s", e.ErrorMsg) +} + +func (UploadStorageError) Is(target error) bool { + _, ok := target.(UploadStorageError) + return ok +} diff --git a/internal/service/file/file_service.go b/internal/service/file/file_service.go index 07ac5c7..f1298bc 100644 --- a/internal/service/file/file_service.go +++ b/internal/service/file/file_service.go @@ -2,11 +2,14 @@ package file import ( "context" + "errors" "mime/multipart" "github.com/fivemanage/lite/api" + "github.com/fivemanage/lite/internal/crypt" "github.com/fivemanage/lite/internal/database" "github.com/fivemanage/lite/internal/database/query/file" + "github.com/fivemanage/lite/internal/http/httputil" "github.com/fivemanage/lite/internal/storage" "github.com/sirupsen/logrus" "github.com/uptrace/bun" @@ -27,17 +30,36 @@ func NewService(db *bun.DB, storageLayer storage.StorageLayer) *Service { func (s *Service) CreateFile( ctx context.Context, organizationID string, - fileType string, file multipart.File, fileHeader *multipart.FileHeader, ) error { var err error - key, contentType, err := generateFileKey(organizationID, file, fileHeader) + var key string + + primaryKey, err := crypt.GeneratePrimaryKey() if err != nil { - return err + return UploadStorageError{ + ErrorMsg: err.Error(), + } + } + + mimeType, ext, fileType, err := httputil.GetMimeDetails(fileHeader, file) + if err != nil { + return UploadStorageError{ + ErrorMsg: errors.New("failed to get mime type").Error(), + } + } + + key, err = generateFileKey(organizationID, ext) + if err != nil { + return UploadStorageError{ + ErrorMsg: errors.New("failed to generate file key").Error(), + } } + // add organizationID to the asset asset := &database.Asset{ + ID: primaryKey, Type: fileType, Size: fileHeader.Size, Key: key, @@ -48,7 +70,7 @@ func (s *Service) CreateFile( return err } - err = s.storage.UploadFile(ctx, file, key, contentType) + err = s.storage.UploadFile(ctx, file, key, mimeType) if err != nil { if err := tx.Rollback(); err != nil { return err @@ -57,6 +79,69 @@ func (s *Service) CreateFile( return err } + // this is a bit tricky, but if this fails....then...oh well + err = tx.Commit() + if err != nil { + return err + } + + return nil +} + +func (s *Service) CreateStorageFile( + ctx context.Context, + organizationID string, + file multipart.File, + fileHeader *multipart.FileHeader, +) error { + var err error + + primaryKey, err := crypt.GeneratePrimaryKey() + if err != nil { + return UploadStorageError{ + ErrorMsg: err.Error(), + } + } + + mimeType, _, fileType, err := httputil.GetMimeDetails(fileHeader, file) + if err != nil { + return UploadStorageError{ + ErrorMsg: errors.New("failed to get mime type").Error(), + } + } + + // fileHeader.Filename has the extension most of the time + // should it become an issue, we can look for it and check if its empty; + key := generateWebKey(organizationID, fileHeader.Filename) + + // add organizationID to the asset + asset := &database.Asset{ + ID: primaryKey, + OrganizationID: organizationID, + Type: fileType, + Size: fileHeader.Size, + Key: key, + } + + tx, err := filequery.Create(ctx, s.db, asset) + if err != nil { + return err + } + + err = s.storage.UploadFile(ctx, file, key, mimeType) + if err != nil { + if err := tx.Rollback(); err != nil { + return err + } + + return err + } + + err = tx.Commit() + if err != nil { + return err + } + return nil } @@ -64,7 +149,7 @@ func (s *Service) ListStorageFiles( ctx context.Context, organizationID string, search string, -) ([]*api.Asset, error) { +) (*api.AssetResponse, error) { var err error files, err := filequery.FindStorageFiles(ctx, s.db, organizationID, search) @@ -81,10 +166,30 @@ func (s *Service) ListStorageFiles( assets := make([]*api.Asset, len(files)) for i, file := range files { assets[i] = &api.Asset{ - ID: file.ID, - Type: file.Type, - Key: file.Key, - Size: file.Size, + ID: file.ID, + Type: file.Type, + Key: file.Key, + Size: file.Size, + CreatedAt: file.CreatedAt, } } + totalCount, err := filequery.FindTotalStorageCount(ctx, s.db, organizationID) + if err != nil { + storageError := &ListStorageError{ + ErrorMsg: err.Error(), + } + + logrus.WithError(storageError). + WithField("organization_id", organizationID).Error("FileService.ListStorageFiles") + + return nil, storageError + } + + response := &api.AssetResponse{ + StorageFiles: assets, + TotalCount: totalCount, + } + + return response, nil +} diff --git a/internal/service/file/key.go b/internal/service/file/key.go index 07823a2..97fbf65 100644 --- a/internal/service/file/key.go +++ b/internal/service/file/key.go @@ -2,26 +2,21 @@ package file import ( "fmt" - "mime/multipart" "github.com/fivemanage/lite/internal/crypt" - "github.com/gabriel-vasile/mimetype" ) -func generateFileKey(organizationID string, file multipart.File, fileHeader *multipart.FileHeader) (string, string, error) { +func generateFileKey(organizationID string, ext string) (string, error) { filename, err := crypt.GenerateFilename() if err != nil { - return "", "", err + return "", err } - buf := make([]byte, fileHeader.Size) - _, err = file.Read(buf) - if err != nil { - return "", "", err - } - - mime := mimetype.Detect(buf) - key := fmt.Sprintf("%s/%s.%s", organizationID, filename, mime.Extension()) + key := fmt.Sprintf("%s/%s.%s", organizationID, filename, ext) + return key, nil +} - return key, mime.String(), nil +func generateWebKey(organizationID, filename string) string { + key := fmt.Sprintf("%s/%s", organizationID, filename) + return key } diff --git a/internal/storage/s3/s3.go b/internal/storage/s3/s3.go index d4db050..0ddd92d 100644 --- a/internal/storage/s3/s3.go +++ b/internal/storage/s3/s3.go @@ -8,10 +8,12 @@ import ( "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/sirupsen/logrus" ) type Storage struct { client *s3.Client + bucket string } func New() *Storage { @@ -33,15 +35,21 @@ func New() *Storage { o.BaseEndpoint = aws.String(os.Getenv("AWS_ENDPOINT")) }) + bucket := os.Getenv("AWS_BUCKET") + if bucket == "" { + logrus.Fatalln("environment variable: AWS_BUCKET is not set") + } + return &Storage{ client: client, + bucket: bucket, } } // UploadFile will both upload and replace as long as the key is the same func (s *Storage) UploadFile(ctx context.Context, file io.Reader, key, contenType string) error { _, err := s.client.PutObject(ctx, &s3.PutObjectInput{ - Bucket: aws.String(os.Getenv("AWS_BUCKET")), + Bucket: aws.String(s.bucket), Key: aws.String(key), Body: file, ContentType: aws.String(contenType), diff --git a/web/components.json b/web/components.json index 8121514..189a6a4 100644 --- a/web/components.json +++ b/web/components.json @@ -1,17 +1,21 @@ { - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.js", - "css": "src/global.css", - "baseColor": "neutral", - "cssVariables": true, - "prefix": "" - }, - "aliases": { - "components": "src/components", - "utils": "src/lib/utils" - } -} + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/global.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/web/package.json b/web/package.json index d6a902f..01bdf07 100644 --- a/web/package.json +++ b/web/package.json @@ -19,6 +19,7 @@ "@radix-ui/react-separator": "^1.1.1", "@radix-ui/react-slot": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.7", + "@tailwindcss/vite": "^4.1.6", "@tanstack/react-query": "^5.59.15", "@tanstack/react-table": "^8.21.3", "class-variance-authority": "^0.7.0", @@ -28,9 +29,10 @@ "lucide-react": "^0.428.0", "react": "^19.0.0", "react-dom": "^19.0.0", + "react-dropzone": "^14.3.8", "react-hook-form": "^7.54.2", "react-router": "^7.1.5", - "tailwind-merge": "^2.5.2", + "tailwind-merge": "^3.3.0", "tailwindcss-animate": "^1.0.7", "zod": "^3.24.1" }, @@ -47,7 +49,8 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.5", "postcss": "^8.4.38", - "tailwindcss": "^3.4.1", + "tailwindcss": "^4.1.6", + "tw-animate-css": "^1.3.0", "typescript": "^5.5.3", "vite": "^6.1.0" } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 0fa3b72..2101af5 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.1.7 version: 1.1.7(@types/react-dom@19.0.3(@types/react@19.0.8))(@types/react@19.0.8)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@tailwindcss/vite': + specifier: ^4.1.6 + version: 4.1.6(vite@6.1.0(@types/node@22.4.0)(jiti@2.4.2)(lightningcss@1.29.2)) '@tanstack/react-query': specifier: ^5.59.15 version: 5.59.15(react@19.0.0) @@ -59,6 +62,9 @@ importers: react-dom: specifier: ^19.0.0 version: 19.0.0(react@19.0.0) + react-dropzone: + specifier: ^14.3.8 + version: 14.3.8(react@19.0.0) react-hook-form: specifier: ^7.54.2 version: 7.54.2(react@19.0.0) @@ -66,18 +72,18 @@ importers: specifier: ^7.1.5 version: 7.1.5(react-dom@19.0.0(react@19.0.0))(react@19.0.0) tailwind-merge: - specifier: ^2.5.2 - version: 2.5.2 + specifier: ^3.3.0 + version: 3.3.0 tailwindcss-animate: specifier: ^1.0.7 - version: 1.0.7(tailwindcss@3.4.1) + version: 1.0.7(tailwindcss@4.1.6) zod: specifier: ^3.24.1 version: 3.24.1 devDependencies: '@tailwindcss/typography': specifier: ^0.5.16 - version: 0.5.16(tailwindcss@3.4.1) + version: 0.5.16(tailwindcss@4.1.6) '@types/node': specifier: ^22.4.0 version: 22.4.0 @@ -95,7 +101,7 @@ importers: version: 7.1.1(eslint@8.57.0)(typescript@5.5.4) '@vitejs/plugin-react-swc': specifier: ^3.8.0 - version: 3.8.0(@swc/helpers@0.5.15)(vite@6.1.0(@types/node@22.4.0)(jiti@1.21.0)) + version: 3.8.0(@swc/helpers@0.5.15)(vite@6.1.0(@types/node@22.4.0)(jiti@2.4.2)(lightningcss@1.29.2)) autoprefixer: specifier: ^10.4.19 version: 10.4.19(postcss@8.4.38) @@ -112,14 +118,17 @@ importers: specifier: ^8.4.38 version: 8.4.38 tailwindcss: - specifier: ^3.4.1 - version: 3.4.1 + specifier: ^4.1.6 + version: 4.1.6 + tw-animate-css: + specifier: ^1.3.0 + version: 1.3.0 typescript: specifier: ^5.5.3 version: 5.5.4 vite: specifier: ^6.1.0 - version: 6.1.0(@types/node@22.4.0)(jiti@1.21.0) + version: 6.1.0(@types/node@22.4.0)(jiti@2.4.2)(lightningcss@1.29.2) packages: @@ -127,9 +136,9 @@ packages: resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} engines: {node: '>=0.10.0'} - '@alloc/quick-lru@5.2.0': - resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} - engines: {node: '>=10'} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} '@emnapi/runtime@1.3.1': resolution: {integrity: sha512-kEBmG8KyqtxJZv+ygbEim+KCGtIq1fC22Ms3S4ziXmYKm8uyoLX0MHONVKwp+9opg390VaKRNt4a7A9NwmpNhw==} @@ -438,12 +447,12 @@ packages: cpu: [x64] os: [win32] - '@isaacs/cliui@8.0.2': - resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} - engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} + '@jridgewell/gen-mapping@0.3.8': + resolution: {integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==} engines: {node: '>=6.0.0'} '@jridgewell/resolve-uri@3.1.2': @@ -454,8 +463,8 @@ packages: resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} engines: {node: '>=6.0.0'} - '@jridgewell/sourcemap-codec@1.4.15': - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} + '@jridgewell/sourcemap-codec@1.5.0': + resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} @@ -523,10 +532,6 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@pkgjs/parseargs@0.11.0': - resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} - engines: {node: '>=14'} - '@radix-ui/primitive@1.1.1': resolution: {integrity: sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==} @@ -1241,11 +1246,101 @@ packages: '@swc/types@0.1.17': resolution: {integrity: sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==} + '@tailwindcss/node@4.1.6': + resolution: {integrity: sha512-ed6zQbgmKsjsVvodAS1q1Ld2BolEuxJOSyyNc+vhkjdmfNUDCmQnlXBfQkHrlzNmslxHsQU/bFmzcEbv4xXsLg==} + + '@tailwindcss/oxide-android-arm64@4.1.6': + resolution: {integrity: sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.1.6': + resolution: {integrity: sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.1.6': + resolution: {integrity: sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.1.6': + resolution: {integrity: sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.6': + resolution: {integrity: sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.6': + resolution: {integrity: sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.1.6': + resolution: {integrity: sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.1.6': + resolution: {integrity: sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.1.6': + resolution: {integrity: sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.1.6': + resolution: {integrity: sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.6': + resolution: {integrity: sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.1.6': + resolution: {integrity: sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.1.6': + resolution: {integrity: sha512-0bpEBQiGx+227fW4G0fLQ8vuvyy5rsB1YIYNapTq3aRsJ9taF3f5cCaovDjN5pUGKKzcpMrZst/mhNaKAPOHOA==} + engines: {node: '>= 10'} + '@tailwindcss/typography@0.5.16': resolution: {integrity: sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==} peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + '@tailwindcss/vite@4.1.6': + resolution: {integrity: sha512-zjtqjDeY1w3g2beYQtrMAf51n5G7o+UwmyOjtsDMP7t6XyoRMOidcoKP32ps7AkNOHIXEOK0bhIC05dj8oJp4w==} + peerDependencies: + vite: ^5.2.0 || ^6 + '@tanstack/query-core@5.59.13': resolution: {integrity: sha512-Oou0bBu/P8+oYjXsJQ11j+gcpLAMpqW42UlokQYEz4dE7+hOtVO9rVuolJKgEccqzvyFzqX4/zZWY+R/v1wVsQ==} @@ -1371,28 +1466,10 @@ packages: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} - ansi-regex@6.0.1: - resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} - engines: {node: '>=12'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - ansi-styles@6.2.1: - resolution: {integrity: sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==} - engines: {node: '>=12'} - - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - arg@5.0.2: - resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -1404,6 +1481,10 @@ packages: resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} engines: {node: '>=8'} + attr-accept@2.2.5: + resolution: {integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==} + engines: {node: '>=4'} + autoprefixer@10.4.19: resolution: {integrity: sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==} engines: {node: ^10 || ^12 || >=14} @@ -1414,10 +1495,6 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} - brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} @@ -1441,10 +1518,6 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - camelcase-css@2.0.1: - resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} - engines: {node: '>= 6'} - caniuse-lite@1.0.30001600: resolution: {integrity: sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==} @@ -1452,9 +1525,9 @@ packages: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} class-variance-authority@0.7.0: resolution: {integrity: sha512-jFI8IQw4hczaL4ALINxqLEXQbWcNjoSkloa4IaufXCJr6QawJyw7tuRysRsrE8w2p/4gGaxKIt/hX3qz/IbD1A==} @@ -1484,10 +1557,6 @@ packages: resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==} engines: {node: '>=12.5.0'} - commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1526,34 +1595,27 @@ packages: resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} engines: {node: '>=8'} + detect-libc@2.0.4: + resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==} + engines: {node: '>=8'} + detect-node-es@1.1.0: resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} - didyoumean@1.2.2: - resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} - dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} - dlv@1.1.3: - resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==} - doctrine@3.0.0: resolution: {integrity: sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==} engines: {node: '>=6.0.0'} - eastasianwidth@0.2.0: - resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} - electron-to-chromium@1.4.717: resolution: {integrity: sha512-6Fmg8QkkumNOwuZ/5mIbMU9WI3H2fmn5ajcVya64I5Yr5CcNmO7vcLt0Y7c96DCiMO5/9G+4sI2r6eEvdg1F7A==} - emoji-regex@8.0.0: - resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - - emoji-regex@9.2.2: - resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + enhanced-resolve@5.18.1: + resolution: {integrity: sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==} + engines: {node: '>=10.13.0'} esbuild@0.24.2: resolution: {integrity: sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==} @@ -1632,6 +1694,10 @@ packages: resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} engines: {node: ^10.12.0 || >=12.0.0} + file-selector@2.1.2: + resolution: {integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==} + engines: {node: '>= 12'} + fill-range@7.0.1: resolution: {integrity: sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==} engines: {node: '>=8'} @@ -1647,10 +1713,6 @@ packages: flatted@3.3.1: resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} - foreground-child@3.1.1: - resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==} - engines: {node: '>=14'} - fraction.js@4.3.7: resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} @@ -1662,9 +1724,6 @@ packages: engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] - function-bind@1.1.2: - resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - geist@1.3.1: resolution: {integrity: sha512-Q4gC1pBVPN+D579pBaz0TRRnGA4p9UK6elDY/xizXdFk/g4EKR5g0I+4p/Kj6gM0SajDBZ/0FvDV9ey9ud7BWw==} peerDependencies: @@ -1682,11 +1741,6 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} - glob@10.3.10: - resolution: {integrity: sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} @@ -1698,6 +1752,9 @@ packages: resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} engines: {node: '>=10'} + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + graphemer@1.4.0: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} @@ -1705,10 +1762,6 @@ packages: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - hasown@2.0.2: - resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} - engines: {node: '>= 0.4'} - ignore@5.3.1: resolution: {integrity: sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==} engines: {node: '>= 4'} @@ -1730,21 +1783,10 @@ packages: is-arrayish@0.3.2: resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==} - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} - - is-core-module@2.13.1: - resolution: {integrity: sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} - is-fullwidth-code-point@3.0.0: - resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} - engines: {node: '>=8'} - is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1760,14 +1802,13 @@ packages: isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} - jackspeak@2.3.6: - resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==} - engines: {node: '>=14'} - - jiti@1.21.0: - resolution: {integrity: sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==} + jiti@2.4.2: + resolution: {integrity: sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==} hasBin: true + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true @@ -1788,16 +1829,69 @@ packages: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - lilconfig@2.1.0: - resolution: {integrity: sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==} - engines: {node: '>=10'} + lightningcss-darwin-arm64@1.29.2: + resolution: {integrity: sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.29.2: + resolution: {integrity: sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.29.2: + resolution: {integrity: sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.29.2: + resolution: {integrity: sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.29.2: + resolution: {integrity: sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.29.2: + resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.29.2: + resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.29.2: + resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.29.2: + resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] - lilconfig@3.1.1: - resolution: {integrity: sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==} - engines: {node: '>=14'} + lightningcss-win32-x64-msvc@1.29.2: + resolution: {integrity: sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] - lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} + lightningcss@1.29.2: + resolution: {integrity: sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==} + engines: {node: '>= 12.0.0'} locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} @@ -1812,9 +1906,9 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lru-cache@10.2.0: - resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==} - engines: {node: 14 || >=16.14} + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} @@ -1825,6 +1919,9 @@ packages: peerDependencies: react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc + magic-string@0.30.17: + resolution: {integrity: sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1840,16 +1937,22 @@ packages: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} - minipass@7.0.4: - resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==} + minipass@7.1.2: + resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minizlib@3.0.2: + resolution: {integrity: sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==} + engines: {node: '>= 18'} + + mkdirp@3.0.1: + resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==} + engines: {node: '>=10'} + hasBin: true + ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1887,10 +1990,6 @@ packages: node-releases@2.0.14: resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - normalize-range@0.1.2: resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} engines: {node: '>=0.10.0'} @@ -1899,10 +1998,6 @@ packages: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} - object-hash@3.0.0: - resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==} - engines: {node: '>= 6'} - once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1934,13 +2029,6 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-parse@1.0.7: - resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} - - path-scurry@1.10.1: - resolution: {integrity: sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==} - engines: {node: '>=16 || 14 >=14.17'} - path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -1955,52 +2043,10 @@ packages: resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} engines: {node: '>=8.6'} - pify@2.3.0: - resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} - engines: {node: '>=0.10.0'} - - pirates@4.0.6: - resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} - engines: {node: '>= 6'} - - postcss-import@15.1.0: - resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} - engines: {node: '>=14.0.0'} - peerDependencies: - postcss: ^8.0.0 - - postcss-js@4.0.1: - resolution: {integrity: sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==} - engines: {node: ^12 || ^14 || >= 16} - peerDependencies: - postcss: ^8.4.21 - - postcss-load-config@4.0.2: - resolution: {integrity: sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==} - engines: {node: '>= 14'} - peerDependencies: - postcss: '>=8.0.9' - ts-node: '>=9.0.0' - peerDependenciesMeta: - postcss: - optional: true - ts-node: - optional: true - - postcss-nested@6.0.1: - resolution: {integrity: sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==} - engines: {node: '>=12.0'} - peerDependencies: - postcss: ^8.2.14 - postcss-selector-parser@6.0.10: resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} engines: {node: '>=4'} - postcss-selector-parser@6.0.16: - resolution: {integrity: sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==} - engines: {node: '>=4'} - postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} @@ -2020,6 +2066,9 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2032,12 +2081,21 @@ packages: peerDependencies: react: ^19.0.0 + react-dropzone@14.3.8: + resolution: {integrity: sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==} + engines: {node: '>= 10.13'} + peerDependencies: + react: '>= 16.8 || 18.0.0' + react-hook-form@7.54.2: resolution: {integrity: sha512-eHpAUgUjWbZocoQYUHposymRb4ZP6d0uwUnooL2uOybA9/3tPUvoAKqEWK1WaSiTxxOfTpffNZP7QwlnM3/gEg==} engines: {node: '>=18.0.0'} peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-remove-scroll-bar@2.3.8: resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} engines: {node: '>=10'} @@ -2082,21 +2140,10 @@ packages: resolution: {integrity: sha512-V8AVnmPIICiWpGfm6GLzCR/W5FXLchHop40W4nXBmdlEceh16rCN8O8LNWm5bh5XUX91fh7KpA+W0TgMKmgTpQ==} engines: {node: '>=0.10.0'} - read-cache@1.0.0: - resolution: {integrity: sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==} - - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve@1.22.8: - resolution: {integrity: sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==} - hasBin: true - reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -2141,10 +2188,6 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - signal-exit@4.1.0: - resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} - engines: {node: '>=14'} - simple-swizzle@0.2.2: resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==} @@ -2164,22 +2207,10 @@ packages: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} - string-width@4.2.3: - resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} - engines: {node: '>=8'} - - string-width@5.1.2: - resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} - engines: {node: '>=12'} - strip-ansi@6.0.1: resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} engines: {node: '>=8'} - strip-ansi@7.1.0: - resolution: {integrity: sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==} - engines: {node: '>=12'} - strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -2197,41 +2228,31 @@ packages: babel-plugin-macros: optional: true - sucrase@3.35.0: - resolution: {integrity: sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==} - engines: {node: '>=16 || 14 >=14.17'} - hasBin: true - supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - supports-preserve-symlinks-flag@1.0.0: - resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} - engines: {node: '>= 0.4'} - - tailwind-merge@2.5.2: - resolution: {integrity: sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==} + tailwind-merge@3.3.0: + resolution: {integrity: sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==} tailwindcss-animate@1.0.7: resolution: {integrity: sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==} peerDependencies: tailwindcss: '>=3.0.0 || insiders' - tailwindcss@3.4.1: - resolution: {integrity: sha512-qAYmXRfk3ENzuPBakNK0SRrUDipP8NQnEY6772uDhflcQz5EhRdD7JNZxyrFHVQNCwULPBn6FNPp9brpO7ctcA==} - engines: {node: '>=14.0.0'} - hasBin: true + tailwindcss@4.1.6: + resolution: {integrity: sha512-j0cGLTreM6u4OWzBeLBpycK0WIh8w7kSwcUsQZoGLHZ7xDTdM69lN64AgoIEEwFi0tnhs4wSykUa5YWxAzgFYg==} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + tapable@2.2.1: + resolution: {integrity: sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==} + engines: {node: '>=6'} - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} + tar@7.4.3: + resolution: {integrity: sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==} + engines: {node: '>=18'} - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + text-table@0.2.0: + resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} @@ -2243,15 +2264,15 @@ packages: peerDependencies: typescript: '>=4.2.0' - ts-interface-checker@0.1.13: - resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} - tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} turbo-stream@2.4.0: resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==} + tw-animate-css@1.3.0: + resolution: {integrity: sha512-jrJ0XenzS9KVuDThJDvnhalbl4IYiMQ/XvpA0a2FL8KmlK+6CSMviO7ROY/I7z1NnUs5NnDhlM6fXmF40xPxzw==} + type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} @@ -2345,24 +2366,15 @@ packages: engines: {node: '>= 8'} hasBin: true - wrap-ansi@7.0.0: - resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} - engines: {node: '>=10'} - - wrap-ansi@8.1.0: - resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} - engines: {node: '>=12'} - wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - yaml@2.4.1: - resolution: {integrity: sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==} - engines: {node: '>= 14'} - hasBin: true + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} @@ -2375,7 +2387,10 @@ snapshots: '@aashutoshrathi/word-wrap@1.2.6': {} - '@alloc/quick-lru@5.2.0': {} + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 '@emnapi/runtime@1.3.1': dependencies: @@ -2588,31 +2603,26 @@ snapshots: '@img/sharp-win32-x64@0.33.5': optional: true - '@isaacs/cliui@8.0.2': + '@isaacs/fs-minipass@4.0.1': dependencies: - string-width: 5.1.2 - string-width-cjs: string-width@4.2.3 - strip-ansi: 7.1.0 - strip-ansi-cjs: strip-ansi@6.0.1 - wrap-ansi: 8.1.0 - wrap-ansi-cjs: wrap-ansi@7.0.0 + minipass: 7.1.2 - '@jridgewell/gen-mapping@0.3.5': + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@jridgewell/trace-mapping': 0.3.25 '@jridgewell/resolve-uri@3.1.2': {} '@jridgewell/set-array@1.2.1': {} - '@jridgewell/sourcemap-codec@1.4.15': {} + '@jridgewell/sourcemap-codec@1.5.0': {} '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/sourcemap-codec': 1.5.0 '@next/env@15.2.2': {} @@ -2652,9 +2662,6 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 - '@pkgjs/parseargs@0.11.0': - optional: true - '@radix-ui/primitive@1.1.1': {} '@radix-ui/primitive@1.1.2': {} @@ -3250,13 +3257,84 @@ snapshots: dependencies: '@swc/counter': 0.1.3 - '@tailwindcss/typography@0.5.16(tailwindcss@3.4.1)': + '@tailwindcss/node@4.1.6': + dependencies: + '@ampproject/remapping': 2.3.0 + enhanced-resolve: 5.18.1 + jiti: 2.4.2 + lightningcss: 1.29.2 + magic-string: 0.30.17 + source-map-js: 1.2.1 + tailwindcss: 4.1.6 + + '@tailwindcss/oxide-android-arm64@4.1.6': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.1.6': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.1.6': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.1.6': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.1.6': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.1.6': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.1.6': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.1.6': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.1.6': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.1.6': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.1.6': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.1.6': + optional: true + + '@tailwindcss/oxide@4.1.6': + dependencies: + detect-libc: 2.0.4 + tar: 7.4.3 + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.1.6 + '@tailwindcss/oxide-darwin-arm64': 4.1.6 + '@tailwindcss/oxide-darwin-x64': 4.1.6 + '@tailwindcss/oxide-freebsd-x64': 4.1.6 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.6 + '@tailwindcss/oxide-linux-arm64-gnu': 4.1.6 + '@tailwindcss/oxide-linux-arm64-musl': 4.1.6 + '@tailwindcss/oxide-linux-x64-gnu': 4.1.6 + '@tailwindcss/oxide-linux-x64-musl': 4.1.6 + '@tailwindcss/oxide-wasm32-wasi': 4.1.6 + '@tailwindcss/oxide-win32-arm64-msvc': 4.1.6 + '@tailwindcss/oxide-win32-x64-msvc': 4.1.6 + + '@tailwindcss/typography@0.5.16(tailwindcss@4.1.6)': dependencies: lodash.castarray: 4.4.0 lodash.isplainobject: 4.0.6 lodash.merge: 4.6.2 postcss-selector-parser: 6.0.10 - tailwindcss: 3.4.1 + tailwindcss: 4.1.6 + + '@tailwindcss/vite@4.1.6(vite@6.1.0(@types/node@22.4.0)(jiti@2.4.2)(lightningcss@1.29.2))': + dependencies: + '@tailwindcss/node': 4.1.6 + '@tailwindcss/oxide': 4.1.6 + tailwindcss: 4.1.6 + vite: 6.1.0(@types/node@22.4.0)(jiti@2.4.2)(lightningcss@1.29.2) '@tanstack/query-core@5.59.13': {} @@ -3381,10 +3459,10 @@ snapshots: '@ungap/structured-clone@1.2.0': {} - '@vitejs/plugin-react-swc@3.8.0(@swc/helpers@0.5.15)(vite@6.1.0(@types/node@22.4.0)(jiti@1.21.0))': + '@vitejs/plugin-react-swc@3.8.0(@swc/helpers@0.5.15)(vite@6.1.0(@types/node@22.4.0)(jiti@2.4.2)(lightningcss@1.29.2))': dependencies: '@swc/core': 1.10.15(@swc/helpers@0.5.15) - vite: 6.1.0(@types/node@22.4.0)(jiti@1.21.0) + vite: 6.1.0(@types/node@22.4.0)(jiti@2.4.2)(lightningcss@1.29.2) transitivePeerDependencies: - '@swc/helpers' @@ -3403,23 +3481,10 @@ snapshots: ansi-regex@5.0.1: {} - ansi-regex@6.0.1: {} - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - ansi-styles@6.2.1: {} - - any-promise@1.3.0: {} - - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - arg@5.0.2: {} - argparse@2.0.1: {} aria-hidden@1.2.4: @@ -3428,6 +3493,8 @@ snapshots: array-union@2.1.0: {} + attr-accept@2.2.5: {} + autoprefixer@10.4.19(postcss@8.4.38): dependencies: browserslist: 4.23.0 @@ -3440,8 +3507,6 @@ snapshots: balanced-match@1.0.2: {} - binary-extensions@2.3.0: {} - brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 @@ -3468,8 +3533,6 @@ snapshots: callsites@3.1.0: {} - camelcase-css@2.0.1: {} - caniuse-lite@1.0.30001600: {} chalk@4.1.2: @@ -3477,17 +3540,7 @@ snapshots: ansi-styles: 4.3.0 supports-color: 7.2.0 - chokidar@3.6.0: - dependencies: - anymatch: 3.1.3 - braces: 3.0.2 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 + chownr@3.0.0: {} class-variance-authority@0.7.0: dependencies: @@ -3517,8 +3570,6 @@ snapshots: color-string: 1.9.1 optional: true - commander@4.1.1: {} - concat-map@0.0.1: {} cookie@1.0.2: {} @@ -3541,30 +3592,26 @@ snapshots: deep-is@0.1.4: {} - detect-libc@2.0.3: - optional: true + detect-libc@2.0.3: {} - detect-node-es@1.1.0: {} + detect-libc@2.0.4: {} - didyoumean@1.2.2: {} + detect-node-es@1.1.0: {} dir-glob@3.0.1: dependencies: path-type: 4.0.0 - dlv@1.1.3: {} - doctrine@3.0.0: dependencies: esutils: 2.0.3 - eastasianwidth@0.2.0: {} - electron-to-chromium@1.4.717: {} - emoji-regex@8.0.0: {} - - emoji-regex@9.2.2: {} + enhanced-resolve@5.18.1: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.2.1 esbuild@0.24.2: optionalDependencies: @@ -3696,6 +3743,10 @@ snapshots: dependencies: flat-cache: 3.2.0 + file-selector@2.1.2: + dependencies: + tslib: 2.8.1 + fill-range@7.0.1: dependencies: to-regex-range: 5.0.1 @@ -3713,11 +3764,6 @@ snapshots: flatted@3.3.1: {} - foreground-child@3.1.1: - dependencies: - cross-spawn: 7.0.3 - signal-exit: 4.1.0 - fraction.js@4.3.7: {} fs.realpath@1.0.0: {} @@ -3725,8 +3771,6 @@ snapshots: fsevents@2.3.3: optional: true - function-bind@1.1.2: {} - geist@1.3.1(next@15.2.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0)): dependencies: next: 15.2.2(react-dom@19.0.0(react@19.0.0))(react@19.0.0) @@ -3741,14 +3785,6 @@ snapshots: dependencies: is-glob: 4.0.3 - glob@10.3.10: - dependencies: - foreground-child: 3.1.1 - jackspeak: 2.3.6 - minimatch: 9.0.3 - minipass: 7.0.4 - path-scurry: 1.10.1 - glob@7.2.3: dependencies: fs.realpath: 1.0.0 @@ -3771,14 +3807,12 @@ snapshots: merge2: 1.4.1 slash: 3.0.0 + graceful-fs@4.2.11: {} + graphemer@1.4.0: {} has-flag@4.0.0: {} - hasown@2.0.2: - dependencies: - function-bind: 1.1.2 - ignore@5.3.1: {} import-fresh@3.3.0: @@ -3798,18 +3832,8 @@ snapshots: is-arrayish@0.3.2: optional: true - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 - - is-core-module@2.13.1: - dependencies: - hasown: 2.0.2 - is-extglob@2.1.1: {} - is-fullwidth-code-point@3.0.0: {} - is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -3820,13 +3844,9 @@ snapshots: isexe@2.0.0: {} - jackspeak@2.3.6: - dependencies: - '@isaacs/cliui': 8.0.2 - optionalDependencies: - '@pkgjs/parseargs': 0.11.0 + jiti@2.4.2: {} - jiti@1.21.0: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: dependencies: @@ -3847,11 +3867,50 @@ snapshots: prelude-ls: 1.2.1 type-check: 0.4.0 - lilconfig@2.1.0: {} + lightningcss-darwin-arm64@1.29.2: + optional: true + + lightningcss-darwin-x64@1.29.2: + optional: true - lilconfig@3.1.1: {} + lightningcss-freebsd-x64@1.29.2: + optional: true - lines-and-columns@1.2.4: {} + lightningcss-linux-arm-gnueabihf@1.29.2: + optional: true + + lightningcss-linux-arm64-gnu@1.29.2: + optional: true + + lightningcss-linux-arm64-musl@1.29.2: + optional: true + + lightningcss-linux-x64-gnu@1.29.2: + optional: true + + lightningcss-linux-x64-musl@1.29.2: + optional: true + + lightningcss-win32-arm64-msvc@1.29.2: + optional: true + + lightningcss-win32-x64-msvc@1.29.2: + optional: true + + lightningcss@1.29.2: + dependencies: + detect-libc: 2.0.3 + optionalDependencies: + lightningcss-darwin-arm64: 1.29.2 + lightningcss-darwin-x64: 1.29.2 + lightningcss-freebsd-x64: 1.29.2 + lightningcss-linux-arm-gnueabihf: 1.29.2 + lightningcss-linux-arm64-gnu: 1.29.2 + lightningcss-linux-arm64-musl: 1.29.2 + lightningcss-linux-x64-gnu: 1.29.2 + lightningcss-linux-x64-musl: 1.29.2 + lightningcss-win32-arm64-msvc: 1.29.2 + lightningcss-win32-x64-msvc: 1.29.2 locate-path@6.0.0: dependencies: @@ -3863,7 +3922,9 @@ snapshots: lodash.merge@4.6.2: {} - lru-cache@10.2.0: {} + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 lru-cache@6.0.0: dependencies: @@ -3873,6 +3934,10 @@ snapshots: dependencies: react: 19.0.0 + magic-string@0.30.17: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.0 + merge2@1.4.1: {} micromatch@4.0.5: @@ -3888,15 +3953,15 @@ snapshots: dependencies: brace-expansion: 2.0.1 - minipass@7.0.4: {} - - ms@2.1.2: {} + minipass@7.1.2: {} - mz@2.7.0: + minizlib@3.0.2: dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 + minipass: 7.1.2 + + mkdirp@3.0.1: {} + + ms@2.1.2: {} nanoid@3.3.7: {} @@ -3931,14 +3996,10 @@ snapshots: node-releases@2.0.14: {} - normalize-path@3.0.0: {} - normalize-range@0.1.2: {} object-assign@4.1.1: {} - object-hash@3.0.0: {} - once@1.4.0: dependencies: wrappy: 1.0.2 @@ -3970,13 +4031,6 @@ snapshots: path-key@3.1.1: {} - path-parse@1.0.7: {} - - path-scurry@1.10.1: - dependencies: - lru-cache: 10.2.0 - minipass: 7.0.4 - path-type@4.0.0: {} picocolors@1.0.0: {} @@ -3985,44 +4039,11 @@ snapshots: picomatch@2.3.1: {} - pify@2.3.0: {} - - pirates@4.0.6: {} - - postcss-import@15.1.0(postcss@8.4.38): - dependencies: - postcss: 8.4.38 - postcss-value-parser: 4.2.0 - read-cache: 1.0.0 - resolve: 1.22.8 - - postcss-js@4.0.1(postcss@8.4.38): - dependencies: - camelcase-css: 2.0.1 - postcss: 8.4.38 - - postcss-load-config@4.0.2(postcss@8.4.38): - dependencies: - lilconfig: 3.1.1 - yaml: 2.4.1 - optionalDependencies: - postcss: 8.4.38 - - postcss-nested@6.0.1(postcss@8.4.38): - dependencies: - postcss: 8.4.38 - postcss-selector-parser: 6.0.16 - postcss-selector-parser@6.0.10: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 - postcss-selector-parser@6.0.16: - dependencies: - cssesc: 3.0.0 - util-deprecate: 1.0.2 - postcss-value-parser@4.2.0: {} postcss@8.4.31: @@ -4045,6 +4066,12 @@ snapshots: prelude-ls@1.2.1: {} + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + punycode@2.3.1: {} queue-microtask@1.2.3: {} @@ -4054,10 +4081,19 @@ snapshots: react: 19.0.0 scheduler: 0.25.0 + react-dropzone@14.3.8(react@19.0.0): + dependencies: + attr-accept: 2.2.5 + file-selector: 2.1.2 + prop-types: 15.8.1 + react: 19.0.0 + react-hook-form@7.54.2(react@19.0.0): dependencies: react: 19.0.0 + react-is@16.13.1: {} + react-remove-scroll-bar@2.3.8(@types/react@19.0.8)(react@19.0.0): dependencies: react: 19.0.0 @@ -4097,22 +4133,8 @@ snapshots: react@19.0.0: {} - read-cache@1.0.0: - dependencies: - pify: 2.3.0 - - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - resolve-from@4.0.0: {} - resolve@1.22.8: - dependencies: - is-core-module: 2.13.1 - path-parse: 1.0.7 - supports-preserve-symlinks-flag: 1.0.0 - reusify@1.0.4: {} rimraf@3.0.2: @@ -4162,7 +4184,7 @@ snapshots: sharp@0.33.5: dependencies: color: 4.2.3 - detect-libc: 2.0.3 + detect-libc: 2.0.4 semver: 7.7.1 optionalDependencies: '@img/sharp-darwin-arm64': 0.33.5 @@ -4192,8 +4214,6 @@ snapshots: shebang-regex@3.0.0: {} - signal-exit@4.1.0: {} - simple-swizzle@0.2.2: dependencies: is-arrayish: 0.3.2 @@ -4207,26 +4227,10 @@ snapshots: streamsearch@1.1.0: {} - string-width@4.2.3: - dependencies: - emoji-regex: 8.0.0 - is-fullwidth-code-point: 3.0.0 - strip-ansi: 6.0.1 - - string-width@5.1.2: - dependencies: - eastasianwidth: 0.2.0 - emoji-regex: 9.2.2 - strip-ansi: 7.1.0 - strip-ansi@6.0.1: dependencies: ansi-regex: 5.0.1 - strip-ansi@7.1.0: - dependencies: - ansi-regex: 6.0.1 - strip-json-comments@3.1.1: {} styled-jsx@5.1.6(react@19.0.0): @@ -4234,64 +4238,30 @@ snapshots: client-only: 0.0.1 react: 19.0.0 - sucrase@3.35.0: - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - commander: 4.1.1 - glob: 10.3.10 - lines-and-columns: 1.2.4 - mz: 2.7.0 - pirates: 4.0.6 - ts-interface-checker: 0.1.13 - supports-color@7.2.0: dependencies: has-flag: 4.0.0 - supports-preserve-symlinks-flag@1.0.0: {} + tailwind-merge@3.3.0: {} - tailwind-merge@2.5.2: {} - - tailwindcss-animate@1.0.7(tailwindcss@3.4.1): + tailwindcss-animate@1.0.7(tailwindcss@4.1.6): dependencies: - tailwindcss: 3.4.1 + tailwindcss: 4.1.6 - tailwindcss@3.4.1: - dependencies: - '@alloc/quick-lru': 5.2.0 - arg: 5.0.2 - chokidar: 3.6.0 - didyoumean: 1.2.2 - dlv: 1.1.3 - fast-glob: 3.3.2 - glob-parent: 6.0.2 - is-glob: 4.0.3 - jiti: 1.21.0 - lilconfig: 2.1.0 - micromatch: 4.0.5 - normalize-path: 3.0.0 - object-hash: 3.0.0 - picocolors: 1.0.0 - postcss: 8.4.38 - postcss-import: 15.1.0(postcss@8.4.38) - postcss-js: 4.0.1(postcss@8.4.38) - postcss-load-config: 4.0.2(postcss@8.4.38) - postcss-nested: 6.0.1(postcss@8.4.38) - postcss-selector-parser: 6.0.16 - resolve: 1.22.8 - sucrase: 3.35.0 - transitivePeerDependencies: - - ts-node + tailwindcss@4.1.6: {} - text-table@0.2.0: {} + tapable@2.2.1: {} - thenify-all@1.6.0: + tar@7.4.3: dependencies: - thenify: 3.3.1 + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.2 + minizlib: 3.0.2 + mkdirp: 3.0.1 + yallist: 5.0.0 - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 + text-table@0.2.0: {} to-regex-range@5.0.1: dependencies: @@ -4301,12 +4271,12 @@ snapshots: dependencies: typescript: 5.5.4 - ts-interface-checker@0.1.13: {} - tslib@2.8.1: {} turbo-stream@2.4.0: {} + tw-animate-css@1.3.0: {} + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -4344,7 +4314,7 @@ snapshots: util-deprecate@1.0.2: {} - vite@6.1.0(@types/node@22.4.0)(jiti@1.21.0): + vite@6.1.0(@types/node@22.4.0)(jiti@2.4.2)(lightningcss@1.29.2): dependencies: esbuild: 0.24.2 postcss: 8.5.2 @@ -4352,29 +4322,18 @@ snapshots: optionalDependencies: '@types/node': 22.4.0 fsevents: 2.3.3 - jiti: 1.21.0 + jiti: 2.4.2 + lightningcss: 1.29.2 which@2.0.2: dependencies: isexe: 2.0.0 - wrap-ansi@7.0.0: - dependencies: - ansi-styles: 4.3.0 - string-width: 4.2.3 - strip-ansi: 6.0.1 - - wrap-ansi@8.1.0: - dependencies: - ansi-styles: 6.2.1 - string-width: 5.1.2 - strip-ansi: 7.1.0 - wrappy@1.0.2: {} yallist@4.0.0: {} - yaml@2.4.1: {} + yallist@5.0.0: {} yocto-queue@0.1.0: {} diff --git a/web/postcss.config.js b/web/postcss.config.js deleted file mode 100644 index 7b75c83..0000000 --- a/web/postcss.config.js +++ /dev/null @@ -1,6 +0,0 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; diff --git a/web/src/components/theme/ModeToggle.tsx b/web/src/components/theme/ModeToggle.tsx index 41b6214..5a1581f 100644 --- a/web/src/components/theme/ModeToggle.tsx +++ b/web/src/components/theme/ModeToggle.tsx @@ -5,7 +5,7 @@ import { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, -} from "../ui/DropdownMenu"; +} from "../ui/dropdown-menu"; import { Button } from "../ui/Button"; import { useTheme } from "./useTheme"; diff --git a/web/src/components/ui/Breadcrumb.tsx b/web/src/components/ui/Breadcrumb.tsx index ecfc6a4..eb88f32 100644 --- a/web/src/components/ui/Breadcrumb.tsx +++ b/web/src/components/ui/Breadcrumb.tsx @@ -1,108 +1,102 @@ -import * as React from "react"; -import { Slot } from "@radix-ui/react-slot"; -import { ChevronRight, MoreHorizontal } from "lucide-react"; +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" -import { cn } from "@/lib/utils"; +import { cn } from "@/lib/utils" -const Breadcrumb = React.forwardRef< - HTMLElement, - React.ComponentPropsWithoutRef<"nav"> & { - separator?: React.ReactNode; - } ->(({ ...props }, ref) =>