diff --git a/backend/internal/auth/service.go b/backend/internal/auth/service.go index 7daced1..a080e71 100644 --- a/backend/internal/auth/service.go +++ b/backend/internal/auth/service.go @@ -11,6 +11,7 @@ import ( "time" "aidanwoods.dev/go-paseto" + "github.com/google/uuid" "github.com/vaultdrop/backend/internal/config" "github.com/vaultdrop/backend/internal/db" "golang.org/x/crypto/argon2" @@ -70,7 +71,7 @@ func (s *AuthService) Register(ctx context.Context, username, password string) ( } existing, err := s.queries.GetUserByUsername(ctx, username) - if err == nil && existing.ID != "" { + if err == nil && existing.ID != uuid.Nil { return nil, nil, ErrUsernameTaken } @@ -87,12 +88,12 @@ func (s *AuthService) Register(ctx context.Context, username, password string) ( return nil, nil, fmt.Errorf("create user: %w", err) } - tokens, err := s.generateTokens(ctx, user.ID) + tokens, err := s.generateTokens(ctx, user.ID.String()) if err != nil { return nil, nil, fmt.Errorf("generate tokens: %w", err) } - return tokens, &UserResponse{ID: user.ID, Username: user.Username}, nil + return tokens, &UserResponse{ID: user.ID.String(), Username: user.Username}, nil } func (s *AuthService) Login(ctx context.Context, username, password string) (*TokenPair, *UserResponse, error) { @@ -105,12 +106,12 @@ func (s *AuthService) Login(ctx context.Context, username, password string) (*To return nil, nil, ErrInvalidCredentials } - tokens, err := s.generateTokens(ctx, user.ID) + tokens, err := s.generateTokens(ctx, user.ID.String()) if err != nil { return nil, nil, fmt.Errorf("generate tokens: %w", err) } - return tokens, &UserResponse{ID: user.ID, Username: user.Username}, nil + return tokens, &UserResponse{ID: user.ID.String(), Username: user.Username}, nil } func (s *AuthService) Refresh(ctx context.Context, refreshToken string) (*TokenPair, error) { @@ -125,7 +126,7 @@ func (s *AuthService) Refresh(ctx context.Context, refreshToken string) (*TokenP return nil, ErrInvalidToken } - if stored.UserID != userID { + if stored.UserID.String() != userID { return nil, ErrInvalidToken } @@ -172,8 +173,9 @@ func (s *AuthService) generateTokens(ctx context.Context, userID string) (*Token } refreshHash := hashToken(refreshToken) + userUUID := uuid.MustParse(userID) _, err = s.queries.CreateRefreshToken(ctx, db.CreateRefreshTokenParams{ - UserID: userID, + UserID: userUUID, TokenHash: refreshHash, ExpiresAt: time.Now().Add(refreshTokenTTL), }) diff --git a/backend/internal/db/auth.sql.go b/backend/internal/db/auth.sql.go index ea5c7e5..2fb135d 100644 --- a/backend/internal/db/auth.sql.go +++ b/backend/internal/db/auth.sql.go @@ -8,6 +8,8 @@ package db import ( "context" "time" + + "github.com/google/uuid" ) const createRefreshToken = `-- name: CreateRefreshToken :one @@ -17,7 +19,7 @@ RETURNING id, user_id, token_hash, expires_at, revoked, created_at ` type CreateRefreshTokenParams struct { - UserID string `json:"user_id"` + UserID uuid.UUID `json:"user_id"` TokenHash string `json:"token_hash"` ExpiresAt time.Time `json:"expires_at"` } @@ -39,7 +41,7 @@ func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshToken const createUser = `-- name: CreateUser :one INSERT INTO users (username, password_hash) VALUES ($1, $2) -RETURNING id, username, password_hash, created_at, updated_at, parent_user_id +RETURNING id, username, password_hash, parent_user_id, created_at, updated_at ` type CreateUserParams struct { @@ -54,9 +56,9 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e &i.ID, &i.Username, &i.PasswordHash, + &i.ParentUserID, &i.CreatedAt, &i.UpdatedAt, - &i.ParentUserID, ) return i, err } @@ -81,25 +83,25 @@ func (q *Queries) GetRefreshToken(ctx context.Context, tokenHash string) (Refres } const getUserByID = `-- name: GetUserByID :one -SELECT id, username, password_hash, created_at, updated_at, parent_user_id FROM users WHERE id = $1 +SELECT id, username, password_hash, parent_user_id, created_at, updated_at FROM users WHERE id = $1 ` -func (q *Queries) GetUserByID(ctx context.Context, id string) (User, error) { +func (q *Queries) GetUserByID(ctx context.Context, id uuid.UUID) (User, error) { row := q.db.QueryRowContext(ctx, getUserByID, id) var i User err := row.Scan( &i.ID, &i.Username, &i.PasswordHash, + &i.ParentUserID, &i.CreatedAt, &i.UpdatedAt, - &i.ParentUserID, ) return i, err } const getUserByUsername = `-- name: GetUserByUsername :one -SELECT id, username, password_hash, created_at, updated_at, parent_user_id FROM users WHERE username = $1 +SELECT id, username, password_hash, parent_user_id, created_at, updated_at FROM users WHERE username = $1 ` func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, error) { @@ -109,9 +111,9 @@ func (q *Queries) GetUserByUsername(ctx context.Context, username string) (User, &i.ID, &i.Username, &i.PasswordHash, + &i.ParentUserID, &i.CreatedAt, &i.UpdatedAt, - &i.ParentUserID, ) return i, err } @@ -120,7 +122,7 @@ const revokeAllUserRefreshTokens = `-- name: RevokeAllUserRefreshTokens :exec UPDATE refresh_tokens SET revoked = TRUE WHERE user_id = $1 ` -func (q *Queries) RevokeAllUserRefreshTokens(ctx context.Context, userID string) error { +func (q *Queries) RevokeAllUserRefreshTokens(ctx context.Context, userID uuid.UUID) error { _, err := q.db.ExecContext(ctx, revokeAllUserRefreshTokens, userID) return err } diff --git a/backend/internal/db/migrations/012_v3_new_tables.down.sql b/backend/internal/db/migrations/012_v3_new_tables.down.sql deleted file mode 100644 index f810c14..0000000 --- a/backend/internal/db/migrations/012_v3_new_tables.down.sql +++ /dev/null @@ -1,10 +0,0 @@ --- VaultDrop 012 down: drop V3 new tables -DROP FUNCTION IF EXISTS resolve_effective_role; -DROP TABLE IF EXISTS rebac_relations; -DROP TABLE IF EXISTS sync_queue; -DROP TABLE IF EXISTS retention_policies; -DROP TABLE IF EXISTS resource_placements; -DROP TABLE IF EXISTS storage_locations; -DROP TABLE IF EXISTS resource_variants; -DROP TABLE IF EXISTS resources; -ALTER TABLE users DROP COLUMN IF EXISTS parent_user_id; diff --git a/backend/internal/db/migrations/012_v3_schema.down.sql b/backend/internal/db/migrations/012_v3_schema.down.sql new file mode 100644 index 0000000..e25753b --- /dev/null +++ b/backend/internal/db/migrations/012_v3_schema.down.sql @@ -0,0 +1,73 @@ +-- VaultDrop 012 down: revert to V1 schema +DROP FUNCTION IF EXISTS resolve_effective_role; + +DROP TABLE IF EXISTS sync_queue CASCADE; +DROP TABLE IF EXISTS retention_policies CASCADE; +DROP TABLE IF EXISTS rebac_relations CASCADE; +DROP TABLE IF EXISTS resource_placements CASCADE; +DROP TABLE IF EXISTS resource_variants CASCADE; +DROP TABLE IF EXISTS resource_tags CASCADE; +DROP TABLE IF EXISTS refresh_tokens CASCADE; +DROP TABLE IF EXISTS storage_locations CASCADE; +DROP TABLE IF EXISTS resources CASCADE; +DROP TABLE IF EXISTS tags CASCADE; +DROP TABLE IF EXISTS users CASCADE; + +-- Restore V1 tables +CREATE TABLE users ( + id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE tags ( + id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), + parent_tag_id TEXT, + tag_name TEXT NOT NULL, + tag_type TEXT NOT NULL DEFAULT 'none', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE files ( + id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), + name TEXT NOT NULL, + mime_type TEXT NOT NULL DEFAULT '', + size BIGINT NOT NULL DEFAULT 0, + storage_key TEXT NOT NULL DEFAULT '', + checksum TEXT NOT NULL DEFAULT '', + ocr_text TEXT NOT NULL DEFAULT '', + is_folder BOOLEAN NOT NULL DEFAULT false, + parent_file_id TEXT, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE file_tags ( + id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), + tag_id TEXT, + file_id TEXT +); + +CREATE TABLE thumbnails ( + id TEXT PRIMARY KEY DEFAULT gen_random_uuid(), + file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE, + page_number INTEGER NOT NULL, + resolution_label TEXT NOT NULL, + width INTEGER NOT NULL, + height INTEGER NOT NULL, + storage_key TEXT NOT NULL, + mime_type TEXT NOT NULL DEFAULT 'image/jpeg', + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE refresh_tokens ( + id TEXT NOT NULL DEFAULT gen_random_uuid(), + user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL, + expires_at TIMESTAMP NOT NULL, + revoked BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/backend/internal/db/migrations/012_v3_new_tables.up.sql b/backend/internal/db/migrations/012_v3_schema.up.sql similarity index 73% rename from backend/internal/db/migrations/012_v3_new_tables.up.sql rename to backend/internal/db/migrations/012_v3_schema.up.sql index 14c7a39..4f78973 100644 --- a/backend/internal/db/migrations/012_v3_new_tables.up.sql +++ b/backend/internal/db/migrations/012_v3_schema.up.sql @@ -1,9 +1,34 @@ --- VaultDrop 012: V3 new tables - resources, storage_locations, resource_placements, etc. +-- VaultDrop 012: Clean V3 schema +-- Drops all legacy V1 tables, recreates everything with UUIDs --- Add parent_user_id to users for groups/organizations -ALTER TABLE users ADD COLUMN parent_user_id UUID REFERENCES users(id); +-- Drop legacy tables (order matters for FK dependencies) +DROP TABLE IF EXISTS file_tags CASCADE; +DROP TABLE IF EXISTS resource_tags CASCADE; +DROP TABLE IF EXISTS thumbnails CASCADE; +DROP TABLE IF EXISTS refresh_tokens CASCADE; +DROP TABLE IF EXISTS files CASCADE; +DROP TABLE IF EXISTS tags CASCADE; +DROP TABLE IF EXISTS users CASCADE; --- Resources (replaces files) +-- Level 1: No dependencies +CREATE TABLE users ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + username TEXT NOT NULL UNIQUE, + password_hash TEXT NOT NULL, + parent_user_id UUID REFERENCES users(id), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TABLE tags ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + parent_tag_id UUID REFERENCES tags(id), + tag_name TEXT NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Level 2: Depend on users CREATE TABLE resources ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, @@ -22,7 +47,40 @@ CREATE INDEX idx_resources_owner ON resources(owner_id); CREATE INDEX idx_resources_parent ON resources(parent_resource_id); CREATE INDEX idx_resources_checksum_owner ON resources(checksum, owner_id); --- Resource variants (replaces thumbnails) +CREATE TABLE storage_locations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id), + device_name TEXT NOT NULL, + role TEXT NOT NULL CHECK (role IN ('primary', 'device', 'backup', 'server')), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + last_seen_at TIMESTAMP +); + +CREATE INDEX idx_locations_user ON storage_locations(user_id); + +CREATE TABLE refresh_tokens ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash TEXT NOT NULL, + expires_at TIMESTAMP NOT NULL, + revoked BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE INDEX idx_refresh_tokens_user_id ON refresh_tokens(user_id); +CREATE INDEX idx_refresh_tokens_token_hash ON refresh_tokens(token_hash); + +-- Level 3: Depend on level 1-2 +CREATE TABLE resource_tags ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + tag_id UUID NOT NULL REFERENCES tags(id), + resource_id UUID NOT NULL REFERENCES resources(id), + UNIQUE(tag_id, resource_id) +); + +CREATE INDEX idx_resource_tags_tag ON resource_tags(tag_id); +CREATE INDEX idx_resource_tags_resource ON resource_tags(resource_id); + CREATE TABLE resource_variants ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE, @@ -39,19 +97,6 @@ CREATE TABLE resource_variants ( CREATE INDEX idx_variants_resource ON resource_variants(resource_id); CREATE UNIQUE INDEX idx_variants_resource_type_page ON resource_variants(resource_id, variant_type, page_number); --- Storage locations (devices, server, backup) -CREATE TABLE storage_locations ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - user_id UUID NOT NULL REFERENCES users(id), - device_name TEXT NOT NULL, - role TEXT NOT NULL CHECK (role IN ('primary', 'device', 'backup', 'server')), - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - last_seen_at TIMESTAMP -); - -CREATE INDEX idx_locations_user ON storage_locations(user_id); - --- Resource placements (pivot resource × storage_location) CREATE TABLE resource_placements ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE, @@ -67,7 +112,19 @@ CREATE INDEX idx_placements_resource ON resource_placements(resource_id); CREATE INDEX idx_placements_location ON resource_placements(storage_location_id); CREATE INDEX idx_placements_status ON resource_placements(status); --- Retention policies +CREATE TABLE rebac_relations ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE, + subject_user_id UUID NOT NULL REFERENCES users(id), + role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'editor', 'viewer')), + granted_by UUID NOT NULL REFERENCES users(id), + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + UNIQUE(resource_id, subject_user_id) +); + +CREATE INDEX idx_rebac_resource ON rebac_relations(resource_id); +CREATE INDEX idx_rebac_subject ON rebac_relations(subject_user_id); + CREATE TABLE retention_policies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id), @@ -80,7 +137,6 @@ CREATE TABLE retention_policies ( CREATE INDEX idx_policies_user ON retention_policies(user_id); CREATE INDEX idx_policies_location ON retention_policies(storage_location_id); --- Sync queue CREATE TABLE sync_queue ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE, @@ -96,21 +152,7 @@ CREATE INDEX idx_queue_status ON sync_queue(status); CREATE INDEX idx_queue_resource ON sync_queue(resource_id); CREATE INDEX idx_queue_location ON sync_queue(storage_location_id); --- ReBAC relations -CREATE TABLE rebac_relations ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE, - subject_user_id UUID NOT NULL REFERENCES users(id), - role TEXT NOT NULL CHECK (role IN ('owner', 'admin', 'editor', 'viewer')), - granted_by UUID NOT NULL REFERENCES users(id), - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE(resource_id, subject_user_id) -); - -CREATE INDEX idx_rebac_resource ON rebac_relations(resource_id); -CREATE INDEX idx_rebac_subject ON rebac_relations(subject_user_id); - --- Function to resolve effective role with recursive parent/group traversal +-- ReBAC permission resolver CREATE OR REPLACE FUNCTION resolve_effective_role(p_user_id UUID, p_resource_id UUID) RETURNS TEXT AS $$ DECLARE diff --git a/backend/internal/db/migrations/013_v3_migrate_data.down.sql b/backend/internal/db/migrations/013_v3_migrate_data.down.sql deleted file mode 100644 index 6a7820a..0000000 --- a/backend/internal/db/migrations/013_v3_migrate_data.down.sql +++ /dev/null @@ -1,15 +0,0 @@ --- VaultDrop 013 down: revert data migration --- Re-add tag_type to tags -ALTER TABLE tags ADD COLUMN IF NOT EXISTS tag_type TEXT NOT NULL DEFAULT 'none'; - -DROP TABLE IF EXISTS resource_tags; - -DELETE FROM resource_variants; - -DELETE FROM rebac_relations; - -DELETE FROM resource_placements; - -DELETE FROM storage_locations; - -DELETE FROM resources; diff --git a/backend/internal/db/migrations/013_v3_migrate_data.up.sql b/backend/internal/db/migrations/013_v3_migrate_data.up.sql deleted file mode 100644 index b6ba94c..0000000 --- a/backend/internal/db/migrations/013_v3_migrate_data.up.sql +++ /dev/null @@ -1,100 +0,0 @@ --- VaultDrop 013: Migrate data from V1 to V3 --- Assigns all existing files to user 'pixel' - --- 1. Ensure user 'pixel' exists (create if not) -INSERT INTO users (username, password_hash) -SELECT 'pixel', '$argon2id$v=19$m=65536,t=1,p=4$placeholder$placeholder' -WHERE NOT EXISTS (SELECT 1 FROM users WHERE username = 'pixel'); - --- 2. Migrate files -> resources -INSERT INTO resources (id, name, mime_type, size, checksum, ocr_text, is_folder, parent_resource_id, owner_id, created_at, updated_at) -SELECT - f.id::uuid, - f.name, - f.mime_type, - f.size, - f.checksum, - f.ocr_text, - f.is_folder, - f.parent_file_id::uuid, - (SELECT id FROM users WHERE username = 'pixel'), - f.created_at, - f.updated_at -FROM files f -ON CONFLICT (id) DO NOTHING; - --- 3. Create server storage_location for user 'pixel' -INSERT INTO storage_locations (id, user_id, device_name, role) -SELECT - gen_random_uuid(), - (SELECT id FROM users WHERE username = 'pixel'), - 'VaultDrop Server', - 'server' -WHERE NOT EXISTS ( - SELECT 1 FROM storage_locations - WHERE user_id = (SELECT id FROM users WHERE username = 'pixel') - AND role = 'server' -); - --- 4. Create resource_placements for migrated resources -INSERT INTO resource_placements (resource_id, storage_location_id, status, storage_key, synced_at) -SELECT - r.id, - sl.id, - 'synced', - f.storage_key, - CURRENT_TIMESTAMP -FROM resources r -JOIN files f ON f.id::uuid = r.id -CROSS JOIN storage_locations sl -WHERE sl.user_id = (SELECT id FROM users WHERE username = 'pixel') -AND sl.role = 'server' -ON CONFLICT (resource_id, storage_location_id) DO NOTHING; - --- 5. Create rebac_relations (owner) for all migrated resources -INSERT INTO rebac_relations (resource_id, subject_user_id, role, granted_by) -SELECT - r.id, - u.id, - 'owner', - u.id -FROM resources r -CROSS JOIN (SELECT id FROM users WHERE username = 'pixel') u -ON CONFLICT (resource_id, subject_user_id) DO NOTHING; - --- 6. Migrate thumbnails -> resource_variants -INSERT INTO resource_variants (id, resource_id, variant_type, page_number, width, height, mime_type, generated_by, storage_key, created_at) -SELECT - t.id::uuid, - t.file_id::uuid, - CASE - WHEN t.resolution_label = 'thumbnail' THEN 'thumbnail_small' - WHEN t.resolution_label = 'full' THEN 'thumbnail_full' - ELSE t.resolution_label - END, - t.page_number, - t.width, - t.height, - t.mime_type, - 'server', - t.storage_key, - t.created_at -FROM thumbnails t -ON CONFLICT (id) DO NOTHING; - --- 7. Create resource_tags from file_tags (keep old file_tags for now) -CREATE TABLE IF NOT EXISTS resource_tags ( - id UUID PRIMARY KEY DEFAULT gen_random_uuid(), - tag_id TEXT, - resource_id TEXT -); - -INSERT INTO resource_tags (id, tag_id, resource_id) -SELECT - gen_random_uuid(), - ft.tag_id, - ft.file_id -FROM file_tags ft; - --- 8. Remove tag_type column from tags -ALTER TABLE tags DROP COLUMN IF EXISTS tag_type; diff --git a/backend/internal/db/migrations/014_v3_cleanup.down.sql b/backend/internal/db/migrations/014_v3_cleanup.down.sql deleted file mode 100644 index 892a087..0000000 --- a/backend/internal/db/migrations/014_v3_cleanup.down.sql +++ /dev/null @@ -1,33 +0,0 @@ --- VaultDrop 014 down: restore legacy tables - -CREATE TABLE files ( - id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), - name TEXT NOT NULL, - mime_type TEXT NOT NULL DEFAULT '', - size BIGINT NOT NULL DEFAULT 0, - storage_key TEXT NOT NULL DEFAULT '', - checksum TEXT NOT NULL DEFAULT '', - ocr_text TEXT NOT NULL DEFAULT '', - is_folder BOOLEAN NOT NULL DEFAULT false, - parent_file_id TEXT, - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE thumbnails ( - id TEXT PRIMARY KEY DEFAULT gen_random_uuid(), - file_id TEXT NOT NULL REFERENCES files(id) ON DELETE CASCADE, - page_number INTEGER NOT NULL, - resolution_label TEXT NOT NULL, - width INTEGER NOT NULL, - height INTEGER NOT NULL, - storage_key TEXT NOT NULL, - mime_type TEXT NOT NULL DEFAULT 'image/jpeg', - created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE file_tags ( - id TEXT PRIMARY KEY NOT NULL DEFAULT gen_random_uuid(), - tag_id TEXT, - file_id TEXT -); diff --git a/backend/internal/db/migrations/014_v3_cleanup.up.sql b/backend/internal/db/migrations/014_v3_cleanup.up.sql deleted file mode 100644 index 54215a4..0000000 --- a/backend/internal/db/migrations/014_v3_cleanup.up.sql +++ /dev/null @@ -1,10 +0,0 @@ --- VaultDrop 014: Drop legacy V1 tables - --- Tags now use resource_tags instead of file_tags -DROP TABLE IF EXISTS file_tags; - --- Thumbnails migrated to resource_variants -DROP TABLE IF EXISTS thumbnails; - --- Files migrated to resources -DROP TABLE IF EXISTS files; diff --git a/backend/internal/db/models.go b/backend/internal/db/models.go index 3b8880d..7e67fc4 100644 --- a/backend/internal/db/models.go +++ b/backend/internal/db/models.go @@ -22,8 +22,8 @@ type RebacRelation struct { } type RefreshToken struct { - ID string `json:"id"` - UserID string `json:"user_id"` + ID uuid.UUID `json:"id"` + UserID uuid.UUID `json:"user_id"` TokenHash string `json:"token_hash"` ExpiresAt time.Time `json:"expires_at"` Revoked bool `json:"revoked"` @@ -55,9 +55,9 @@ type ResourcePlacement struct { } type ResourceTag struct { - ID uuid.UUID `json:"id"` - TagID sql.NullString `json:"tag_id"` - ResourceID sql.NullString `json:"resource_id"` + ID uuid.UUID `json:"id"` + TagID uuid.UUID `json:"tag_id"` + ResourceID uuid.UUID `json:"resource_id"` } type ResourceVariant struct { @@ -103,18 +103,18 @@ type SyncQueue struct { } type Tag struct { - ID string `json:"id"` - ParentTagID sql.NullString `json:"parent_tag_id"` - TagName string `json:"tag_name"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` + ID uuid.UUID `json:"id"` + ParentTagID uuid.NullUUID `json:"parent_tag_id"` + TagName string `json:"tag_name"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` } type User struct { - ID string `json:"id"` + ID uuid.UUID `json:"id"` Username string `json:"username"` PasswordHash string `json:"password_hash"` + ParentUserID uuid.NullUUID `json:"parent_user_id"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` - ParentUserID uuid.NullUUID `json:"parent_user_id"` } diff --git a/backend/internal/db/tags.sql.go b/backend/internal/db/tags.sql.go index 8e853a0..76941ef 100644 --- a/backend/internal/db/tags.sql.go +++ b/backend/internal/db/tags.sql.go @@ -7,7 +7,8 @@ package db import ( "context" - "database/sql" + + "github.com/google/uuid" ) const addTagToResource = `-- name: AddTagToResource :exec @@ -17,8 +18,8 @@ ON CONFLICT DO NOTHING ` type AddTagToResourceParams struct { - TagID sql.NullString `json:"tag_id"` - ResourceID sql.NullString `json:"resource_id"` + TagID uuid.UUID `json:"tag_id"` + ResourceID uuid.UUID `json:"resource_id"` } func (q *Queries) AddTagToResource(ctx context.Context, arg AddTagToResourceParams) error { @@ -50,7 +51,7 @@ DELETE FROM tags WHERE id = $1 ` -func (q *Queries) DeleteTag(ctx context.Context, id string) error { +func (q *Queries) DeleteTag(ctx context.Context, id uuid.UUID) error { _, err := q.db.ExecContext(ctx, deleteTag, id) return err } @@ -62,7 +63,7 @@ WHERE rt.tag_id = $1 ORDER BY r.created_at DESC ` -func (q *Queries) GetResourcesByTagID(ctx context.Context, tagID sql.NullString) ([]Resource, error) { +func (q *Queries) GetResourcesByTagID(ctx context.Context, tagID uuid.UUID) ([]Resource, error) { rows, err := q.db.QueryContext(ctx, getResourcesByTagID, tagID) if err != nil { return nil, err @@ -102,7 +103,7 @@ SELECT id, parent_tag_id, tag_name, created_at, updated_at FROM tags WHERE id = $1 ` -func (q *Queries) GetTag(ctx context.Context, id string) (Tag, error) { +func (q *Queries) GetTag(ctx context.Context, id uuid.UUID) (Tag, error) { row := q.db.QueryRowContext(ctx, getTag, id) var i Tag err := row.Scan( @@ -140,7 +141,7 @@ WHERE rt.resource_id = $1 ORDER BY t.tag_name ASC ` -func (q *Queries) GetTagsByResourceID(ctx context.Context, resourceID sql.NullString) ([]Tag, error) { +func (q *Queries) GetTagsByResourceID(ctx context.Context, resourceID uuid.UUID) ([]Tag, error) { rows, err := q.db.QueryContext(ctx, getTagsByResourceID, resourceID) if err != nil { return nil, err @@ -209,8 +210,8 @@ WHERE tag_id = $1 AND resource_id = $2 ` type RemoveTagFromResourceParams struct { - TagID sql.NullString `json:"tag_id"` - ResourceID sql.NullString `json:"resource_id"` + TagID uuid.UUID `json:"tag_id"` + ResourceID uuid.UUID `json:"resource_id"` } func (q *Queries) RemoveTagFromResource(ctx context.Context, arg RemoveTagFromResourceParams) error { diff --git a/backend/internal/handler/router.go b/backend/internal/handler/router.go index d54c1ca..d6a2595 100644 --- a/backend/internal/handler/router.go +++ b/backend/internal/handler/router.go @@ -21,7 +21,7 @@ func SetupRoutes(r *gin.Engine, h *Handler, authMiddleware *auth.AuthService) { protected := api.Group("") protected.Use(authMiddleware.RequireAuth()) - // Resources (replaces /files) + // Resources protected.GET("/resources", h.Resource.List) protected.POST("/resources/upload", h.Resource.Upload) protected.POST("/resources/move", h.Resource.MoveResources) @@ -31,14 +31,14 @@ func SetupRoutes(r *gin.Engine, h *Handler, authMiddleware *auth.AuthService) { protected.DELETE("/resources/:id", h.Resource.Delete) protected.GET("/resources/:id", h.Resource.Get) - // Tags on resources + // Tags protected.POST("/resources/:id/tags", h.Resource.AddTags) protected.GET("/resources/:id/tags", h.Resource.GetTags) // Variants protected.GET("/resources/:id/variants", h.Resource.GetVariants) - // Dedup check + // Dedup protected.POST("/resources/dedup-check", h.Resource.CheckDuplicates) // Sharing (ReBAC) @@ -47,13 +47,10 @@ func SetupRoutes(r *gin.Engine, h *Handler, authMiddleware *auth.AuthService) { protected.GET("/resources/:id/share", h.Share.List) protected.GET("/resources/:id/access", h.Share.Check) - // Device management + // Devices protected.GET("/devices", h.Device.List) protected.POST("/devices", h.Device.Register) - // Placements - protected.GET("/resources/:id/placements", h.Resource.GetVariants) - // Sync protected.POST("/sync/pull", h.Sync.Pull) protected.POST("/sync/push", h.Sync.Push) @@ -61,18 +58,4 @@ func SetupRoutes(r *gin.Engine, h *Handler, authMiddleware *auth.AuthService) { // OCR protected.POST("/ocr/jobs", h.OCR.CreateJob) protected.GET("/ocr/jobs/:id", h.OCR.GetJobStatus) - - // Legacy /files/* endpoints (maintain backward compatibility) - protected.GET("/files", h.Resource.List) - protected.POST("/files/upload", h.Resource.Upload) - protected.POST("/files/move", h.Resource.MoveResources) - protected.POST("/files/folders", h.Resource.CreateFolder) - protected.GET("/files/folders", h.Resource.ListFolders) - protected.GET("/files/folders/:id/files", h.Resource.ListByParent) - protected.DELETE("/files/:id", h.Resource.Delete) - protected.GET("/files/:id", h.Resource.Get) - protected.POST("/files/:id/tags", h.Resource.AddTags) - protected.GET("/files/:id/tags", h.Resource.GetTags) - protected.GET("/files/:id/thumbnails", h.Resource.GetVariants) - protected.POST("/files/dedup-check", h.Resource.CheckDuplicates) } diff --git a/backend/internal/model/file.go b/backend/internal/model/file.go index 84e47e1..d4540af 100644 --- a/backend/internal/model/file.go +++ b/backend/internal/model/file.go @@ -12,8 +12,6 @@ type Resource struct { Name string `json:"name"` MimeType string `json:"mimeType"` Size int64 `json:"size"` - StorageKey string `json:"-"` - Checksum string `json:"-"` OcrText string `json:"ocrText,omitempty"` Tags []Tag `json:"tags"` IsFolder bool `json:"isFolder"` diff --git a/backend/internal/model/tag.go b/backend/internal/model/tag.go deleted file mode 100644 index 8b53790..0000000 --- a/backend/internal/model/tag.go +++ /dev/null @@ -1 +0,0 @@ -package model diff --git a/backend/internal/model/thumbnail.go b/backend/internal/model/thumbnail.go deleted file mode 100644 index 8b53790..0000000 --- a/backend/internal/model/thumbnail.go +++ /dev/null @@ -1 +0,0 @@ -package model diff --git a/backend/internal/service/placement.go b/backend/internal/service/placement.go index af5931b..7252e67 100644 --- a/backend/internal/service/placement.go +++ b/backend/internal/service/placement.go @@ -52,14 +52,4 @@ func (s *PlacementService) ListUserLocations(userID string) ([]db.StorageLocatio return s.queries.ListStorageLocationsByUser(context.Background(), userUUID) } -func (s *PlacementService) ensureServerLocation(userID uuid.UUID) (db.StorageLocation, error) { - loc, err := s.queries.GetServerStorageLocation(context.Background(), userID) - if err != nil { - return s.queries.CreateStorageLocation(context.Background(), db.CreateStorageLocationParams{ - UserID: userID, - DeviceName: "VaultDrop Server", - Role: "server", - }) - } - return loc, nil -} + diff --git a/backend/internal/service/resource.go b/backend/internal/service/resource.go index 33355ea..61f4926 100644 --- a/backend/internal/service/resource.go +++ b/backend/internal/service/resource.go @@ -81,7 +81,7 @@ func (s *ResourceService) Upload(file *multipart.FileHeader, ownerID string) (*m return nil, fmt.Errorf("create resource in db: %w", err) } - placement, err := s.ensureServerPlacement(dbResource.ID, dst) + placement, err := s.ensureServerPlacement(dbResource.ID, ownerUUID, dst) if err != nil { return nil, fmt.Errorf("create server placement: %w", err) } @@ -98,8 +98,8 @@ func (s *ResourceService) Upload(file *multipart.FileHeader, ownerID string) (*m }, nil } -func (s *ResourceService) ensureServerPlacement(resourceID uuid.UUID, dst string) (db.ResourcePlacement, error) { - serverLoc, err := s.queries.GetServerStorageLocation(context.Background(), uuid.Nil) +func (s *ResourceService) ensureServerPlacement(resourceID, ownerID uuid.UUID, dst string) (db.ResourcePlacement, error) { + serverLoc, err := s.queries.GetServerStorageLocation(context.Background(), ownerID) if err != nil { return db.ResourcePlacement{}, fmt.Errorf("get server location: %w", err) } @@ -137,7 +137,7 @@ func (s *ResourceService) List(ownerID string) ([]model.Resource, error) { resources := make([]model.Resource, len(dbResources)) for i, r := range dbResources { - tags, err := s.queries.GetTagsByResourceID(context.Background(), sql.NullString{String: r.ID.String(), Valid: true}) + tags, err := s.queries.GetTagsByResourceID(context.Background(), r.ID) if err != nil { return nil, fmt.Errorf("get tags for resource %s: %w", r.ID, err) } @@ -152,7 +152,7 @@ func (s *ResourceService) Get(id string) (*model.Resource, error) { if err != nil { return nil, fmt.Errorf("get resource: %w", err) } - tags, err := s.queries.GetTagsByResourceID(context.Background(), sql.NullString{String: r.ID.String(), Valid: true}) + tags, err := s.queries.GetTagsByResourceID(context.Background(), r.ID) if err != nil { return nil, fmt.Errorf("get tags: %w", err) } @@ -202,8 +202,8 @@ func (s *ResourceService) AddTags(resourceID string, tagNames []string) error { } err = s.queries.AddTagToResource(context.Background(), db.AddTagToResourceParams{ - TagID: sql.NullString{String: tag.ID, Valid: true}, - ResourceID: sql.NullString{String: resourceUUID.String(), Valid: true}, + TagID: tag.ID, + ResourceID: resourceUUID, }) if err != nil { return fmt.Errorf("link tag %q to resource: %w", name, err) @@ -214,13 +214,13 @@ func (s *ResourceService) AddTags(resourceID string, tagNames []string) error { func (s *ResourceService) GetTagsByResourceID(resourceID string) ([]model.Tag, error) { resourceUUID, _ := uuid.Parse(resourceID) - dbTags, err := s.queries.GetTagsByResourceID(context.Background(), sql.NullString{String: resourceUUID.String(), Valid: true}) + dbTags, err := s.queries.GetTagsByResourceID(context.Background(), resourceUUID) if err != nil { return nil, fmt.Errorf("get tags: %w", err) } tags := make([]model.Tag, len(dbTags)) for i, t := range dbTags { - tags[i] = model.Tag{ID: t.ID, Name: t.TagName} + tags[i] = model.Tag{ID: t.ID.String(), Name: t.TagName} } return tags, nil } @@ -283,7 +283,7 @@ func (s *ResourceService) ListResourcesByParentID(parentID, ownerID string) ([]m } resources := make([]model.Resource, len(dbResources)) for i, r := range dbResources { - tags, err := s.queries.GetTagsByResourceID(context.Background(), sql.NullString{String: r.ID.String(), Valid: true}) + tags, err := s.queries.GetTagsByResourceID(context.Background(), r.ID) if err != nil { return nil, fmt.Errorf("get tags for resource %s: %w", r.ID, err) } @@ -370,7 +370,7 @@ func (s *ResourceService) FindDuplicatesByNameSize(name string, size int64) ([]d func dbResourceToModel(r db.Resource, dbTags []db.Tag) model.Resource { tags := make([]model.Tag, len(dbTags)) for i, t := range dbTags { - tags[i] = model.Tag{ID: t.ID, Name: t.TagName} + tags[i] = model.Tag{ID: t.ID.String(), Name: t.TagName} } parentID := "" @@ -383,7 +383,6 @@ func dbResourceToModel(r db.Resource, dbTags []db.Tag) model.Resource { Name: r.Name, MimeType: r.MimeType, Size: r.Size, - Checksum: r.Checksum, OcrText: r.OcrText, IsFolder: r.IsFolder, ParentResourceID: parentID,