migrate to v3 - remove backward comp
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"aidanwoods.dev/go-paseto"
|
"aidanwoods.dev/go-paseto"
|
||||||
|
"github.com/google/uuid"
|
||||||
"github.com/vaultdrop/backend/internal/config"
|
"github.com/vaultdrop/backend/internal/config"
|
||||||
"github.com/vaultdrop/backend/internal/db"
|
"github.com/vaultdrop/backend/internal/db"
|
||||||
"golang.org/x/crypto/argon2"
|
"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)
|
existing, err := s.queries.GetUserByUsername(ctx, username)
|
||||||
if err == nil && existing.ID != "" {
|
if err == nil && existing.ID != uuid.Nil {
|
||||||
return nil, nil, ErrUsernameTaken
|
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)
|
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 {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("generate tokens: %w", err)
|
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) {
|
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
|
return nil, nil, ErrInvalidCredentials
|
||||||
}
|
}
|
||||||
|
|
||||||
tokens, err := s.generateTokens(ctx, user.ID)
|
tokens, err := s.generateTokens(ctx, user.ID.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, fmt.Errorf("generate tokens: %w", err)
|
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) {
|
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
|
return nil, ErrInvalidToken
|
||||||
}
|
}
|
||||||
|
|
||||||
if stored.UserID != userID {
|
if stored.UserID.String() != userID {
|
||||||
return nil, ErrInvalidToken
|
return nil, ErrInvalidToken
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,8 +173,9 @@ func (s *AuthService) generateTokens(ctx context.Context, userID string) (*Token
|
|||||||
}
|
}
|
||||||
|
|
||||||
refreshHash := hashToken(refreshToken)
|
refreshHash := hashToken(refreshToken)
|
||||||
|
userUUID := uuid.MustParse(userID)
|
||||||
_, err = s.queries.CreateRefreshToken(ctx, db.CreateRefreshTokenParams{
|
_, err = s.queries.CreateRefreshToken(ctx, db.CreateRefreshTokenParams{
|
||||||
UserID: userID,
|
UserID: userUUID,
|
||||||
TokenHash: refreshHash,
|
TokenHash: refreshHash,
|
||||||
ExpiresAt: time.Now().Add(refreshTokenTTL),
|
ExpiresAt: time.Now().Add(refreshTokenTTL),
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ package db
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const createRefreshToken = `-- name: CreateRefreshToken :one
|
const createRefreshToken = `-- name: CreateRefreshToken :one
|
||||||
@@ -17,7 +19,7 @@ RETURNING id, user_id, token_hash, expires_at, revoked, created_at
|
|||||||
`
|
`
|
||||||
|
|
||||||
type CreateRefreshTokenParams struct {
|
type CreateRefreshTokenParams struct {
|
||||||
UserID string `json:"user_id"`
|
UserID uuid.UUID `json:"user_id"`
|
||||||
TokenHash string `json:"token_hash"`
|
TokenHash string `json:"token_hash"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
}
|
}
|
||||||
@@ -39,7 +41,7 @@ func (q *Queries) CreateRefreshToken(ctx context.Context, arg CreateRefreshToken
|
|||||||
const createUser = `-- name: CreateUser :one
|
const createUser = `-- name: CreateUser :one
|
||||||
INSERT INTO users (username, password_hash)
|
INSERT INTO users (username, password_hash)
|
||||||
VALUES ($1, $2)
|
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 {
|
type CreateUserParams struct {
|
||||||
@@ -54,9 +56,9 @@ func (q *Queries) CreateUser(ctx context.Context, arg CreateUserParams) (User, e
|
|||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Username,
|
&i.Username,
|
||||||
&i.PasswordHash,
|
&i.PasswordHash,
|
||||||
|
&i.ParentUserID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.ParentUserID,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -81,25 +83,25 @@ func (q *Queries) GetRefreshToken(ctx context.Context, tokenHash string) (Refres
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getUserByID = `-- name: GetUserByID :one
|
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)
|
row := q.db.QueryRowContext(ctx, getUserByID, id)
|
||||||
var i User
|
var i User
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
&i.ID,
|
&i.ID,
|
||||||
&i.Username,
|
&i.Username,
|
||||||
&i.PasswordHash,
|
&i.PasswordHash,
|
||||||
|
&i.ParentUserID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.ParentUserID,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
|
|
||||||
const getUserByUsername = `-- name: GetUserByUsername :one
|
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) {
|
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.ID,
|
||||||
&i.Username,
|
&i.Username,
|
||||||
&i.PasswordHash,
|
&i.PasswordHash,
|
||||||
|
&i.ParentUserID,
|
||||||
&i.CreatedAt,
|
&i.CreatedAt,
|
||||||
&i.UpdatedAt,
|
&i.UpdatedAt,
|
||||||
&i.ParentUserID,
|
|
||||||
)
|
)
|
||||||
return i, err
|
return i, err
|
||||||
}
|
}
|
||||||
@@ -120,7 +122,7 @@ const revokeAllUserRefreshTokens = `-- name: RevokeAllUserRefreshTokens :exec
|
|||||||
UPDATE refresh_tokens SET revoked = TRUE WHERE user_id = $1
|
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)
|
_, err := q.db.ExecContext(ctx, revokeAllUserRefreshTokens, userID)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
@@ -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
|
||||||
|
);
|
||||||
+77
-35
@@ -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
|
-- Drop legacy tables (order matters for FK dependencies)
|
||||||
ALTER TABLE users ADD COLUMN parent_user_id UUID REFERENCES users(id);
|
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 (
|
CREATE TABLE resources (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
name TEXT NOT NULL,
|
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_parent ON resources(parent_resource_id);
|
||||||
CREATE INDEX idx_resources_checksum_owner ON resources(checksum, owner_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 (
|
CREATE TABLE resource_variants (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
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 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);
|
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 (
|
CREATE TABLE resource_placements (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
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_location ON resource_placements(storage_location_id);
|
||||||
CREATE INDEX idx_placements_status ON resource_placements(status);
|
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 (
|
CREATE TABLE retention_policies (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
user_id UUID NOT NULL REFERENCES users(id),
|
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_user ON retention_policies(user_id);
|
||||||
CREATE INDEX idx_policies_location ON retention_policies(storage_location_id);
|
CREATE INDEX idx_policies_location ON retention_policies(storage_location_id);
|
||||||
|
|
||||||
-- Sync queue
|
|
||||||
CREATE TABLE sync_queue (
|
CREATE TABLE sync_queue (
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
resource_id UUID NOT NULL REFERENCES resources(id) ON DELETE CASCADE,
|
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_resource ON sync_queue(resource_id);
|
||||||
CREATE INDEX idx_queue_location ON sync_queue(storage_location_id);
|
CREATE INDEX idx_queue_location ON sync_queue(storage_location_id);
|
||||||
|
|
||||||
-- ReBAC relations
|
-- ReBAC permission resolver
|
||||||
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
|
|
||||||
CREATE OR REPLACE FUNCTION resolve_effective_role(p_user_id UUID, p_resource_id UUID)
|
CREATE OR REPLACE FUNCTION resolve_effective_role(p_user_id UUID, p_resource_id UUID)
|
||||||
RETURNS TEXT AS $$
|
RETURNS TEXT AS $$
|
||||||
DECLARE
|
DECLARE
|
||||||
@@ -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;
|
|
||||||
@@ -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;
|
|
||||||
@@ -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
|
|
||||||
);
|
|
||||||
@@ -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;
|
|
||||||
@@ -22,8 +22,8 @@ type RebacRelation struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RefreshToken struct {
|
type RefreshToken struct {
|
||||||
ID string `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
UserID string `json:"user_id"`
|
UserID uuid.UUID `json:"user_id"`
|
||||||
TokenHash string `json:"token_hash"`
|
TokenHash string `json:"token_hash"`
|
||||||
ExpiresAt time.Time `json:"expires_at"`
|
ExpiresAt time.Time `json:"expires_at"`
|
||||||
Revoked bool `json:"revoked"`
|
Revoked bool `json:"revoked"`
|
||||||
@@ -56,8 +56,8 @@ type ResourcePlacement struct {
|
|||||||
|
|
||||||
type ResourceTag struct {
|
type ResourceTag struct {
|
||||||
ID uuid.UUID `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
TagID sql.NullString `json:"tag_id"`
|
TagID uuid.UUID `json:"tag_id"`
|
||||||
ResourceID sql.NullString `json:"resource_id"`
|
ResourceID uuid.UUID `json:"resource_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ResourceVariant struct {
|
type ResourceVariant struct {
|
||||||
@@ -103,18 +103,18 @@ type SyncQueue struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Tag struct {
|
type Tag struct {
|
||||||
ID string `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
ParentTagID sql.NullString `json:"parent_tag_id"`
|
ParentTagID uuid.NullUUID `json:"parent_tag_id"`
|
||||||
TagName string `json:"tag_name"`
|
TagName string `json:"tag_name"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
ID string `json:"id"`
|
ID uuid.UUID `json:"id"`
|
||||||
Username string `json:"username"`
|
Username string `json:"username"`
|
||||||
PasswordHash string `json:"password_hash"`
|
PasswordHash string `json:"password_hash"`
|
||||||
|
ParentUserID uuid.NullUUID `json:"parent_user_id"`
|
||||||
CreatedAt time.Time `json:"created_at"`
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
ParentUserID uuid.NullUUID `json:"parent_user_id"`
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ package db
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
|
||||||
|
"github.com/google/uuid"
|
||||||
)
|
)
|
||||||
|
|
||||||
const addTagToResource = `-- name: AddTagToResource :exec
|
const addTagToResource = `-- name: AddTagToResource :exec
|
||||||
@@ -17,8 +18,8 @@ ON CONFLICT DO NOTHING
|
|||||||
`
|
`
|
||||||
|
|
||||||
type AddTagToResourceParams struct {
|
type AddTagToResourceParams struct {
|
||||||
TagID sql.NullString `json:"tag_id"`
|
TagID uuid.UUID `json:"tag_id"`
|
||||||
ResourceID sql.NullString `json:"resource_id"`
|
ResourceID uuid.UUID `json:"resource_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) AddTagToResource(ctx context.Context, arg AddTagToResourceParams) error {
|
func (q *Queries) AddTagToResource(ctx context.Context, arg AddTagToResourceParams) error {
|
||||||
@@ -50,7 +51,7 @@ DELETE FROM tags
|
|||||||
WHERE id = $1
|
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)
|
_, err := q.db.ExecContext(ctx, deleteTag, id)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -62,7 +63,7 @@ WHERE rt.tag_id = $1
|
|||||||
ORDER BY r.created_at DESC
|
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)
|
rows, err := q.db.QueryContext(ctx, getResourcesByTagID, tagID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -102,7 +103,7 @@ SELECT id, parent_tag_id, tag_name, created_at, updated_at FROM tags
|
|||||||
WHERE id = $1
|
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)
|
row := q.db.QueryRowContext(ctx, getTag, id)
|
||||||
var i Tag
|
var i Tag
|
||||||
err := row.Scan(
|
err := row.Scan(
|
||||||
@@ -140,7 +141,7 @@ WHERE rt.resource_id = $1
|
|||||||
ORDER BY t.tag_name ASC
|
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)
|
rows, err := q.db.QueryContext(ctx, getTagsByResourceID, resourceID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -209,8 +210,8 @@ WHERE tag_id = $1 AND resource_id = $2
|
|||||||
`
|
`
|
||||||
|
|
||||||
type RemoveTagFromResourceParams struct {
|
type RemoveTagFromResourceParams struct {
|
||||||
TagID sql.NullString `json:"tag_id"`
|
TagID uuid.UUID `json:"tag_id"`
|
||||||
ResourceID sql.NullString `json:"resource_id"`
|
ResourceID uuid.UUID `json:"resource_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *Queries) RemoveTagFromResource(ctx context.Context, arg RemoveTagFromResourceParams) error {
|
func (q *Queries) RemoveTagFromResource(ctx context.Context, arg RemoveTagFromResourceParams) error {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ func SetupRoutes(r *gin.Engine, h *Handler, authMiddleware *auth.AuthService) {
|
|||||||
protected := api.Group("")
|
protected := api.Group("")
|
||||||
protected.Use(authMiddleware.RequireAuth())
|
protected.Use(authMiddleware.RequireAuth())
|
||||||
|
|
||||||
// Resources (replaces /files)
|
// Resources
|
||||||
protected.GET("/resources", h.Resource.List)
|
protected.GET("/resources", h.Resource.List)
|
||||||
protected.POST("/resources/upload", h.Resource.Upload)
|
protected.POST("/resources/upload", h.Resource.Upload)
|
||||||
protected.POST("/resources/move", h.Resource.MoveResources)
|
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.DELETE("/resources/:id", h.Resource.Delete)
|
||||||
protected.GET("/resources/:id", h.Resource.Get)
|
protected.GET("/resources/:id", h.Resource.Get)
|
||||||
|
|
||||||
// Tags on resources
|
// Tags
|
||||||
protected.POST("/resources/:id/tags", h.Resource.AddTags)
|
protected.POST("/resources/:id/tags", h.Resource.AddTags)
|
||||||
protected.GET("/resources/:id/tags", h.Resource.GetTags)
|
protected.GET("/resources/:id/tags", h.Resource.GetTags)
|
||||||
|
|
||||||
// Variants
|
// Variants
|
||||||
protected.GET("/resources/:id/variants", h.Resource.GetVariants)
|
protected.GET("/resources/:id/variants", h.Resource.GetVariants)
|
||||||
|
|
||||||
// Dedup check
|
// Dedup
|
||||||
protected.POST("/resources/dedup-check", h.Resource.CheckDuplicates)
|
protected.POST("/resources/dedup-check", h.Resource.CheckDuplicates)
|
||||||
|
|
||||||
// Sharing (ReBAC)
|
// 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/share", h.Share.List)
|
||||||
protected.GET("/resources/:id/access", h.Share.Check)
|
protected.GET("/resources/:id/access", h.Share.Check)
|
||||||
|
|
||||||
// Device management
|
// Devices
|
||||||
protected.GET("/devices", h.Device.List)
|
protected.GET("/devices", h.Device.List)
|
||||||
protected.POST("/devices", h.Device.Register)
|
protected.POST("/devices", h.Device.Register)
|
||||||
|
|
||||||
// Placements
|
|
||||||
protected.GET("/resources/:id/placements", h.Resource.GetVariants)
|
|
||||||
|
|
||||||
// Sync
|
// Sync
|
||||||
protected.POST("/sync/pull", h.Sync.Pull)
|
protected.POST("/sync/pull", h.Sync.Pull)
|
||||||
protected.POST("/sync/push", h.Sync.Push)
|
protected.POST("/sync/push", h.Sync.Push)
|
||||||
@@ -61,18 +58,4 @@ func SetupRoutes(r *gin.Engine, h *Handler, authMiddleware *auth.AuthService) {
|
|||||||
// OCR
|
// OCR
|
||||||
protected.POST("/ocr/jobs", h.OCR.CreateJob)
|
protected.POST("/ocr/jobs", h.OCR.CreateJob)
|
||||||
protected.GET("/ocr/jobs/:id", h.OCR.GetJobStatus)
|
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,6 @@ type Resource struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
MimeType string `json:"mimeType"`
|
MimeType string `json:"mimeType"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
StorageKey string `json:"-"`
|
|
||||||
Checksum string `json:"-"`
|
|
||||||
OcrText string `json:"ocrText,omitempty"`
|
OcrText string `json:"ocrText,omitempty"`
|
||||||
Tags []Tag `json:"tags"`
|
Tags []Tag `json:"tags"`
|
||||||
IsFolder bool `json:"isFolder"`
|
IsFolder bool `json:"isFolder"`
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
package model
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
package model
|
|
||||||
@@ -52,14 +52,4 @@ func (s *PlacementService) ListUserLocations(userID string) ([]db.StorageLocatio
|
|||||||
return s.queries.ListStorageLocationsByUser(context.Background(), userUUID)
|
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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ func (s *ResourceService) Upload(file *multipart.FileHeader, ownerID string) (*m
|
|||||||
return nil, fmt.Errorf("create resource in db: %w", err)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("create server placement: %w", err)
|
return nil, fmt.Errorf("create server placement: %w", err)
|
||||||
}
|
}
|
||||||
@@ -98,8 +98,8 @@ func (s *ResourceService) Upload(file *multipart.FileHeader, ownerID string) (*m
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *ResourceService) ensureServerPlacement(resourceID uuid.UUID, dst string) (db.ResourcePlacement, error) {
|
func (s *ResourceService) ensureServerPlacement(resourceID, ownerID uuid.UUID, dst string) (db.ResourcePlacement, error) {
|
||||||
serverLoc, err := s.queries.GetServerStorageLocation(context.Background(), uuid.Nil)
|
serverLoc, err := s.queries.GetServerStorageLocation(context.Background(), ownerID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return db.ResourcePlacement{}, fmt.Errorf("get server location: %w", err)
|
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))
|
resources := make([]model.Resource, len(dbResources))
|
||||||
for i, r := range 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("get tags for resource %s: %w", r.ID, err)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("get resource: %w", err)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("get tags: %w", err)
|
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{
|
err = s.queries.AddTagToResource(context.Background(), db.AddTagToResourceParams{
|
||||||
TagID: sql.NullString{String: tag.ID, Valid: true},
|
TagID: tag.ID,
|
||||||
ResourceID: sql.NullString{String: resourceUUID.String(), Valid: true},
|
ResourceID: resourceUUID,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("link tag %q to resource: %w", name, err)
|
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) {
|
func (s *ResourceService) GetTagsByResourceID(resourceID string) ([]model.Tag, error) {
|
||||||
resourceUUID, _ := uuid.Parse(resourceID)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("get tags: %w", err)
|
return nil, fmt.Errorf("get tags: %w", err)
|
||||||
}
|
}
|
||||||
tags := make([]model.Tag, len(dbTags))
|
tags := make([]model.Tag, len(dbTags))
|
||||||
for i, t := range 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
|
return tags, nil
|
||||||
}
|
}
|
||||||
@@ -283,7 +283,7 @@ func (s *ResourceService) ListResourcesByParentID(parentID, ownerID string) ([]m
|
|||||||
}
|
}
|
||||||
resources := make([]model.Resource, len(dbResources))
|
resources := make([]model.Resource, len(dbResources))
|
||||||
for i, r := range 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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("get tags for resource %s: %w", r.ID, err)
|
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 {
|
func dbResourceToModel(r db.Resource, dbTags []db.Tag) model.Resource {
|
||||||
tags := make([]model.Tag, len(dbTags))
|
tags := make([]model.Tag, len(dbTags))
|
||||||
for i, t := range 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 := ""
|
parentID := ""
|
||||||
@@ -383,7 +383,6 @@ func dbResourceToModel(r db.Resource, dbTags []db.Tag) model.Resource {
|
|||||||
Name: r.Name,
|
Name: r.Name,
|
||||||
MimeType: r.MimeType,
|
MimeType: r.MimeType,
|
||||||
Size: r.Size,
|
Size: r.Size,
|
||||||
Checksum: r.Checksum,
|
|
||||||
OcrText: r.OcrText,
|
OcrText: r.OcrText,
|
||||||
IsFolder: r.IsFolder,
|
IsFolder: r.IsFolder,
|
||||||
ParentResourceID: parentID,
|
ParentResourceID: parentID,
|
||||||
|
|||||||
Reference in New Issue
Block a user