From fb276d9b24a782379193af5085f161d5169a6d1f Mon Sep 17 00:00:00 2001 From: DioCrafts Date: Fri, 28 Mar 2025 08:09:18 +0100 Subject: [PATCH] adding ui sharing --- apply-migrations.sh | 62 ++ db/schema.sql | 91 ++- docker-compose.yml | 2 +- fix-userrole.sql | 69 -- migrations/20240320_create_auth_schema.sql | 65 -- migrations/20240323_add_userrole_type.sql | 37 - .../repositories/pg/user_pg_repository.rs | 32 +- static/css/style.css | 642 +++++++++++++++++- static/index.html | 3 +- static/js/app.js | 24 +- static/js/auth.js | 123 ++-- static/js/contextMenus.js | 234 +++++++ static/js/fileSharing.js | 379 +++++++++++ static/js/shared.js | 469 +++++++++++++ static/js/ui.js | 143 +++- static/locales/en.json | 88 ++- static/locales/es.json | 88 ++- static/shared.html | 196 ++++++ 18 files changed, 2452 insertions(+), 295 deletions(-) create mode 100755 apply-migrations.sh delete mode 100644 fix-userrole.sql delete mode 100644 migrations/20240320_create_auth_schema.sql delete mode 100644 migrations/20240323_add_userrole_type.sql create mode 100644 static/js/fileSharing.js create mode 100644 static/js/shared.js create mode 100644 static/shared.html diff --git a/apply-migrations.sh b/apply-migrations.sh new file mode 100755 index 00000000..67965393 --- /dev/null +++ b/apply-migrations.sh @@ -0,0 +1,62 @@ +#!/bin/bash + +# This script is used to apply all migrations in order +# It can be run manually or as part of container initialization + +# Determine run mode +if [ -z "$POSTGRES_DB" ]; then + # Manual mode - script was run from command line + + # Verify we are in the right environment + if [ ! -f "Cargo.toml" ]; then + echo "Error: This script must be run from the OxiCloud project root directory" + exit 1 + fi + + # Check if postgres container is running + POSTGRES_ID=$(docker-compose ps -q postgres 2>/dev/null) + if [ -z "$POSTGRES_ID" ]; then + echo "Error: PostgreSQL container is not running. Start it with 'docker-compose up -d postgres'" + exit 1 + fi + + # Set parameters for manual mode + DB_NAME="oxicloud" + MIGRATIONS_DIR="./migrations" + MIGRATIONS_CMD="docker-compose exec -T postgres psql -U postgres" +else + # Auto mode - script is running inside postgres container during initialization + echo "Running in PostgreSQL container initialization mode" + + # Set parameters for auto mode + DB_NAME="$POSTGRES_DB" + MIGRATIONS_DIR="/docker-entrypoint-initdb.d/migrations" + MIGRATIONS_CMD="psql -U $POSTGRES_USER" +fi + +echo "Applying migrations from $MIGRATIONS_DIR to database $DB_NAME" + +# Ensure schema is created (fallback) +$MIGRATIONS_CMD -d "$DB_NAME" -c "CREATE SCHEMA IF NOT EXISTS auth;" 2>/dev/null + +# Get each SQL file from migrations directory in alphabetical order +for sql_file in $(find $MIGRATIONS_DIR -name "*.sql" | sort); do + echo "Applying migration: $(basename $sql_file)" + + # Run the migration + if [ -z "$POSTGRES_DB" ]; then + # Manual mode - run through docker-compose + docker-compose exec -T postgres psql -U postgres -d "$DB_NAME" -f "/docker-entrypoint-initdb.d/migrations/$(basename $sql_file)" + else + # Auto mode - run directly + psql -U "$POSTGRES_USER" -d "$DB_NAME" -f "$sql_file" + fi + + # Check if migration was successful + if [ $? -ne 0 ]; then + echo "Error: Failed to apply migration $sql_file" + exit 1 + fi +done + +echo "All migrations applied successfully" \ No newline at end of file diff --git a/db/schema.sql b/db/schema.sql index 82838ed9..355013a2 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -1,12 +1,27 @@ -- OxiCloud Authentication Database Schema +-- Create schema for auth-related tables +CREATE SCHEMA IF NOT EXISTS auth; + +-- Create UserRole enum type +DO $BODY$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM pg_type t + JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace + WHERE t.typname = 'userrole' AND n.nspname = 'auth' + ) THEN + CREATE TYPE auth.userrole AS ENUM ('admin', 'user'); + END IF; +END $BODY$; + -- Users table -CREATE TABLE IF NOT EXISTS users ( +CREATE TABLE IF NOT EXISTS auth.users ( id VARCHAR(36) PRIMARY KEY, username VARCHAR(32) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - role VARCHAR(10) NOT NULL CHECK (role IN ('admin', 'user')), + password_hash TEXT NOT NULL, + role auth.userrole NOT NULL, storage_quota_bytes BIGINT NOT NULL DEFAULT 10737418240, -- 10GB default storage_used_bytes BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, @@ -15,40 +30,47 @@ CREATE TABLE IF NOT EXISTS users ( active BOOLEAN NOT NULL DEFAULT TRUE ); +-- Create indexes for users table +CREATE INDEX IF NOT EXISTS idx_users_username ON auth.users(username); +CREATE INDEX IF NOT EXISTS idx_users_email ON auth.users(email); + -- Sessions table for refresh tokens -CREATE TABLE IF NOT EXISTS sessions ( +CREATE TABLE IF NOT EXISTS auth.sessions ( id VARCHAR(36) PRIMARY KEY, - user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE, - refresh_token VARCHAR(255) NOT NULL, + user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + refresh_token VARCHAR(255) NOT NULL UNIQUE, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, + ip_address VARCHAR(45), -- to support IPv6 + user_agent TEXT, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - revoked BOOLEAN NOT NULL DEFAULT FALSE, - UNIQUE (user_id, refresh_token) + revoked BOOLEAN NOT NULL DEFAULT FALSE ); +-- Create indexes for sessions table +CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON auth.sessions(user_id); +CREATE INDEX IF NOT EXISTS idx_sessions_refresh_token ON auth.sessions(refresh_token); +CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON auth.sessions(expires_at); +CREATE INDEX IF NOT EXISTS idx_sessions_active ON auth.sessions(user_id, revoked, expires_at) +WHERE NOT revoked AND expires_at > NOW(); + -- File ownership tracking -CREATE TABLE IF NOT EXISTS file_ownership ( - file_id VARCHAR(36) PRIMARY KEY, - user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE, - path VARCHAR(1024) NOT NULL, +CREATE TABLE IF NOT EXISTS auth.user_files ( + id SERIAL PRIMARY KEY, + user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, + file_path TEXT NOT NULL, + file_id VARCHAR(255) NOT NULL, size_bytes BIGINT NOT NULL DEFAULT 0, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE (user_id, path) + UNIQUE(user_id, file_path) ); --- Folder ownership tracking -CREATE TABLE IF NOT EXISTS folder_ownership ( - folder_id VARCHAR(36) PRIMARY KEY, - user_id VARCHAR(36) NOT NULL REFERENCES users(id) ON DELETE CASCADE, - path VARCHAR(1024) NOT NULL, - created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT CURRENT_TIMESTAMP, - UNIQUE (user_id, path) -); +-- Create indexes for user_files +CREATE INDEX IF NOT EXISTS idx_user_files_user_id ON auth.user_files(user_id); +CREATE INDEX IF NOT EXISTS idx_user_files_file_id ON auth.user_files(file_id); -- Create admin user (password: Admin123!) -INSERT INTO users ( +INSERT INTO auth.users ( id, username, email, @@ -62,4 +84,25 @@ INSERT INTO users ( '$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHRzYWx0c2FsdA$H3VxE8LL2qPT31DM3loTg6D+O4MSc2sD7GjlQ5h7Jkw', -- Admin123! 'admin', 107374182400 -- 100GB for admin -) ON CONFLICT (id) DO NOTHING; \ No newline at end of file +) ON CONFLICT (id) DO NOTHING; + +-- Create test user (password: test123) +INSERT INTO auth.users ( + id, + username, + email, + password_hash, + role, + storage_quota_bytes +) VALUES ( + '11111111-1111-1111-1111-111111111111', + 'test', + 'test@oxicloud.local', + '$argon2id$v=19$m=65536,t=3,p=4$c2FsdHNhbHRzYWx0c2FsdA$ZG17Z7SFKhs9zWYbuk08CkHpyiznnZapYnxN5Vi62R4', -- test123 + 'user', + 10737418240 -- 10GB for test user +) ON CONFLICT (id) DO NOTHING; + +COMMENT ON TABLE auth.users IS 'Stores user account information'; +COMMENT ON TABLE auth.sessions IS 'Stores user session information for refresh tokens'; +COMMENT ON TABLE auth.user_files IS 'Tracks file ownership and storage utilization by users'; \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 29255615..1ea3becc 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,7 +10,7 @@ services: - "5432:5432" volumes: - pg_data:/var/lib/postgresql/data - - ./migrations:/docker-entrypoint-initdb.d + - ./db/schema.sql:/docker-entrypoint-initdb.d/10-schema.sql healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres"] interval: 5s diff --git a/fix-userrole.sql b/fix-userrole.sql deleted file mode 100644 index 5f07d767..00000000 --- a/fix-userrole.sql +++ /dev/null @@ -1,69 +0,0 @@ --- First create the schema if it doesn't exist -CREATE SCHEMA IF NOT EXISTS auth; - --- Output diagnostic information -\echo 'Starting migration fix for auth.userrole' -\echo 'Current schemas:' -\dt auth.* -\echo 'Current types:' -SELECT n.nspname AS schema, t.typname AS type -FROM pg_type t -JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace -WHERE n.nspname = 'auth'; -\echo '===============================' - --- Check if the type already exists and create it if not -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT 1 FROM pg_type t - JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace - WHERE t.typname = 'userrole' AND n.nspname = 'auth' - ) THEN - -- Create the type - CREATE TYPE auth.userrole AS ENUM ('admin', 'user'); - END IF; -END -$$; - --- Check if the users table exists and create it if not -DO $$ -BEGIN - IF NOT EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = 'auth' AND table_name = 'users' - ) THEN - -- Create the users table with the proper enum type - CREATE TABLE auth.users ( - id VARCHAR(36) PRIMARY KEY, - username VARCHAR(32) NOT NULL UNIQUE, - email VARCHAR(255) NOT NULL UNIQUE, - password_hash TEXT NOT NULL, - role auth.userrole NOT NULL, - storage_quota_bytes BIGINT NOT NULL, - storage_used_bytes BIGINT NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - last_login_at TIMESTAMPTZ, - active BOOLEAN NOT NULL DEFAULT TRUE - ); - ELSE - -- Check if the role column is already auth.userrole type - IF EXISTS ( - SELECT FROM information_schema.columns - WHERE table_schema = 'auth' AND table_name = 'users' - AND column_name = 'role' AND data_type <> 'USER-DEFINED' - ) THEN - -- Try to convert the role column to the new enum type - BEGIN - ALTER TABLE auth.users ALTER COLUMN role TYPE auth.userrole USING - CASE WHEN role = 'admin' THEN 'admin'::auth.userrole - WHEN role = 'user' THEN 'user'::auth.userrole - ELSE 'user'::auth.userrole END; - EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'Error converting role column: %', SQLERRM; - END; - END IF; - END IF; -END -$$; \ No newline at end of file diff --git a/migrations/20240320_create_auth_schema.sql b/migrations/20240320_create_auth_schema.sql deleted file mode 100644 index 1124dc23..00000000 --- a/migrations/20240320_create_auth_schema.sql +++ /dev/null @@ -1,65 +0,0 @@ --- Create the auth schema -CREATE SCHEMA IF NOT EXISTS auth; - --- Create UserRole enum type -CREATE TYPE auth.userrole AS ENUM ('admin', 'user'); - --- Create the users table -CREATE TABLE IF NOT EXISTS auth.users ( - id VARCHAR(36) PRIMARY KEY, - username VARCHAR(32) NOT NULL UNIQUE, - email VARCHAR(255) NOT NULL UNIQUE, - password_hash TEXT NOT NULL, - role auth.userrole NOT NULL, - storage_quota_bytes BIGINT NOT NULL, - storage_used_bytes BIGINT NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - last_login_at TIMESTAMPTZ, - active BOOLEAN NOT NULL DEFAULT TRUE -); - --- Create an index on username and email for fast lookups -CREATE INDEX IF NOT EXISTS idx_users_username ON auth.users(username); -CREATE INDEX IF NOT EXISTS idx_users_email ON auth.users(email); - --- Create the sessions table -CREATE TABLE IF NOT EXISTS auth.sessions ( - id VARCHAR(36) PRIMARY KEY, - user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - refresh_token VARCHAR(255) NOT NULL UNIQUE, - expires_at TIMESTAMPTZ NOT NULL, - ip_address VARCHAR(45), -- to support IPv6 - user_agent TEXT, - created_at TIMESTAMPTZ NOT NULL, - revoked BOOLEAN NOT NULL DEFAULT FALSE -); - --- Create indexes on user_id and refresh_token for fast lookups -CREATE INDEX IF NOT EXISTS idx_sessions_user_id ON auth.sessions(user_id); -CREATE INDEX IF NOT EXISTS idx_sessions_refresh_token ON auth.sessions(refresh_token); -CREATE INDEX IF NOT EXISTS idx_sessions_expires_at ON auth.sessions(expires_at); - --- Create an index for getting active sessions -CREATE INDEX IF NOT EXISTS idx_sessions_active ON auth.sessions(user_id, revoked, expires_at) -WHERE NOT revoked AND expires_at > NOW(); - --- Create the user_files table to track ownership of files -CREATE TABLE IF NOT EXISTS auth.user_files ( - id SERIAL PRIMARY KEY, - user_id VARCHAR(36) NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, - file_path TEXT NOT NULL, - file_id VARCHAR(255) NOT NULL, - size_bytes BIGINT NOT NULL DEFAULT 0, - created_at TIMESTAMPTZ NOT NULL, - updated_at TIMESTAMPTZ NOT NULL, - UNIQUE(user_id, file_path) -); - --- Create indexes for user_files -CREATE INDEX IF NOT EXISTS idx_user_files_user_id ON auth.user_files(user_id); -CREATE INDEX IF NOT EXISTS idx_user_files_file_id ON auth.user_files(file_id); - -COMMENT ON TABLE auth.users IS 'Stores user account information'; -COMMENT ON TABLE auth.sessions IS 'Stores user session information for refresh tokens'; -COMMENT ON TABLE auth.user_files IS 'Tracks file ownership and storage utilization by users'; \ No newline at end of file diff --git a/migrations/20240323_add_userrole_type.sql b/migrations/20240323_add_userrole_type.sql deleted file mode 100644 index 94163c44..00000000 --- a/migrations/20240323_add_userrole_type.sql +++ /dev/null @@ -1,37 +0,0 @@ --- Fix the missing UserRole enum type -DO $$ -BEGIN - -- Check if the type already exists - IF NOT EXISTS ( - SELECT 1 FROM pg_type t - JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace - WHERE t.typname = 'userrole' AND n.nspname = 'auth' - ) THEN - -- Create the type if it doesn't exist - CREATE TYPE auth.userrole AS ENUM ('admin', 'user'); - END IF; -END -$$; - --- If the table already exists but has a different role column type, --- we need to update it to use the new enum type -DO $$ -BEGIN - -- Check if the users table exists - IF EXISTS ( - SELECT FROM information_schema.tables - WHERE table_schema = 'auth' AND table_name = 'users' - ) THEN - -- Try to convert the role column to the new enum type - -- This will work if the column currently contains 'admin' or 'user' values - BEGIN - ALTER TABLE auth.users ALTER COLUMN role TYPE auth.userrole USING - CASE WHEN role = 'admin' THEN 'admin'::auth.userrole - WHEN role = 'user' THEN 'user'::auth.userrole - ELSE 'user'::auth.userrole END; - EXCEPTION WHEN OTHERS THEN - RAISE NOTICE 'Error converting role column: %', SQLERRM; - END; - END IF; -END -$$; \ No newline at end of file diff --git a/src/infrastructure/repositories/pg/user_pg_repository.rs b/src/infrastructure/repositories/pg/user_pg_repository.rs index 881f3bb2..59103efd 100644 --- a/src/infrastructure/repositories/pg/user_pg_repository.rs +++ b/src/infrastructure/repositories/pg/user_pg_repository.rs @@ -85,7 +85,7 @@ impl UserRepository for UserPgRepository { let row = sqlx::query( r#" SELECT - id, username, email, password_hash, role, + id, username, email, password_hash, role::text as role_text, storage_quota_bytes, storage_used_bytes, created_at, updated_at, last_login_at, active FROM auth.users @@ -98,9 +98,9 @@ impl UserRepository for UserPgRepository { .map_err(Self::map_sqlx_error)?; // Convert role string to UserRole enum - let role_str: String = row.get("role"); - let role = match role_str.as_str() { - "admin" => UserRole::Admin, + let role_str: Option = row.try_get("role_text").unwrap_or(None); + let role = match role_str.as_deref() { + Some("admin") => UserRole::Admin, _ => UserRole::User, }; @@ -124,7 +124,7 @@ impl UserRepository for UserPgRepository { let row = sqlx::query( r#" SELECT - id, username, email, password_hash, role, + id, username, email, password_hash, role::text as role_text, storage_quota_bytes, storage_used_bytes, created_at, updated_at, last_login_at, active FROM auth.users @@ -137,9 +137,9 @@ impl UserRepository for UserPgRepository { .map_err(Self::map_sqlx_error)?; // Convert role string to UserRole enum - let role_str: String = row.get("role"); - let role = match role_str.as_str() { - "admin" => UserRole::Admin, + let role_str: Option = row.try_get("role_text").unwrap_or(None); + let role = match role_str.as_deref() { + Some("admin") => UserRole::Admin, _ => UserRole::User, }; @@ -163,7 +163,7 @@ impl UserRepository for UserPgRepository { let row = sqlx::query( r#" SELECT - id, username, email, password_hash, role, + id, username, email, password_hash, role::text as role_text, storage_quota_bytes, storage_used_bytes, created_at, updated_at, last_login_at, active FROM auth.users @@ -176,9 +176,9 @@ impl UserRepository for UserPgRepository { .map_err(Self::map_sqlx_error)?; // Convert role string to UserRole enum - let role_str: String = row.get("role"); - let role = match role_str.as_str() { - "admin" => UserRole::Admin, + let role_str: Option = row.try_get("role_text").unwrap_or(None); + let role = match role_str.as_deref() { + Some("admin") => UserRole::Admin, _ => UserRole::User, }; @@ -276,7 +276,7 @@ impl UserRepository for UserPgRepository { let rows = sqlx::query( r#" SELECT - id, username, email, password_hash, role, + id, username, email, password_hash, role::text as role_text, storage_quota_bytes, storage_used_bytes, created_at, updated_at, last_login_at, active FROM auth.users @@ -293,9 +293,9 @@ impl UserRepository for UserPgRepository { let users = rows.into_iter() .map(|row| { // Convert role string to UserRole enum for each row - let role_str: String = row.get("role"); - let role = match role_str.as_str() { - "admin" => UserRole::Admin, + let role_str: Option = row.try_get("role_text").unwrap_or(None); + let role = match role_str.as_deref() { + Some("admin") => UserRole::Admin, _ => UserRole::User, }; diff --git a/static/css/style.css b/static/css/style.css index 997cefea..52817d26 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -888,61 +888,620 @@ body { color: #718096; } -/* Compartir diálogo */ -.share-dialog { - position: absolute; - top: 50%; - right: 50px; - width: 250px; - background-color: white; - border-radius: 8px; - box-shadow: 0 5px 20px rgba(0,0,0,0.15); - z-index: 1000; - padding: 15px; +/* Dialogs (Rename, Move, Share) */ +.rename-dialog, .share-dialog { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: none; + justify-content: center; + align-items: center; + z-index: 2000; } -.share-title { +.rename-dialog-content, .share-dialog-content { + background-color: white; + padding: 20px; + border-radius: 8px; + width: 400px; + max-width: 90%; +} + +.share-dialog-content { + width: 500px; +} + +.rename-dialog-header, .share-dialog-header { + font-size: 18px; font-weight: bold; margin-bottom: 15px; - color: #2d3748; - padding-bottom: 10px; - border-bottom: 1px solid #e0e6ed; } -.share-user { +.rename-dialog input, .share-dialog input { + width: 100%; + padding: 10px; + margin-bottom: 15px; + border: 1px solid #ddd; + border-radius: 4px; +} + +.rename-dialog-buttons, .share-dialog-buttons { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +/* Share dialog specific styles */ +.shared-item-info { + padding: 10px 0; + margin-bottom: 15px; + border-bottom: 1px solid #eee; +} + +.share-options h3, #existing-shares-section h3, #new-share-section h3 { + font-size: 14px; + font-weight: 600; + margin-bottom: 10px; + color: #333; +} + +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 5px; + font-weight: 500; +} + +.permission-options { + display: flex; + gap: 15px; + margin-top: 5px; +} + +.permission-option { display: flex; align-items: center; + gap: 5px; +} + +.existing-share-item { + background-color: #f8f9fa; + border-radius: 4px; + padding: 10px; margin-bottom: 10px; } -.user-avatar-small { - width: 24px; - height: 24px; - border-radius: 50%; - background-color: #dfe4ea; +.share-url { + font-size: 12px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + margin-bottom: 5px; + color: #1565c0; +} + +.share-info { + font-size: 11px; + display: flex; + gap: 15px; + margin-bottom: 10px; + color: #757575; +} + +.share-protected { + color: #1565c0; +} + +.share-expiration { + color: #b71c1c; +} + +.share-actions { + display: flex; + gap: 5px; + justify-content: flex-end; +} + +.share-link-actions { + display: flex; + gap: 10px; + margin-top: 5px; +} + +.btn-small { + font-size: 12px; + padding: 5px 10px; +} + +#notification-message { + width: 100%; + padding: 10px; + border: 1px solid #ddd; + border-radius: 4px; + resize: vertical; +} + +/* Shared Resources Page Styles */ +/* Header styles for shared page */ +header { + background-color: white; + border-bottom: 1px solid #e6e6e6; + padding: 15px 20px; +} + +.header-container { + display: flex; + justify-content: space-between; + align-items: center; + max-width: 1280px; + margin: 0 auto; +} + +.header-actions { + display: flex; + align-items: center; + gap: 15px; +} + +.header-button { + padding: 8px 15px; + background-color: #f0f3f7; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.2s; +} + +.header-button:hover { + background-color: #e6e9ed; +} + +/* Shared page container */ +.shared-page-container { + max-width: 1280px; + margin: 20px auto; + padding: 0 20px; +} + +.shared-header { + margin-bottom: 25px; +} + +.shared-header h2 { + font-size: 24px; + color: #2d3748; + margin-bottom: 8px; +} + +.shared-header p { + color: #718096; + font-size: 16px; +} + +/* Filters and search */ +.shared-filters { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 25px; + flex-wrap: wrap; + gap: 15px; +} + +.filter-group { + display: flex; + align-items: center; + gap: 10px; +} + +.filter-group label { + font-size: 14px; + color: #4a5568; +} + +.filter-group select { + padding: 8px 12px; + border: 1px solid #e2e8f0; + border-radius: 6px; + background-color: white; + font-size: 14px; +} + +.search-box { + display: flex; + gap: 10px; +} + +.search-box input { + padding: 8px 15px; + border: 1px solid #e2e8f0; + border-radius: 6px; + font-size: 14px; + width: 250px; +} + +.search-box button { + padding: 8px 15px; + background-color: #ff5e3a; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; +} + +.search-box button:hover { + background-color: #e64a29; +} + +/* Shared items list */ +.shared-list-container { + background-color: white; + border-radius: 10px; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); + overflow: hidden; +} + +.shared-list { + width: 100%; + border-collapse: collapse; +} + +.shared-list thead th { + padding: 15px; + text-align: left; + font-weight: 600; + color: #2d3748; + background-color: #f8f9fa; + border-bottom: 1px solid #e0e6ed; +} + +.shared-list tbody td { + padding: 15px; + border-bottom: 1px solid #f0f0f0; + vertical-align: middle; +} + +.shared-item-name { + display: flex; + align-items: center; + gap: 10px; +} + +.shared-item-actions { + display: flex; + gap: 10px; +} + +.action-btn { + width: 32px; + height: 32px; + border: 1px solid #e2e8f0; + border-radius: 6px; + background-color: white; display: flex; align-items: center; justify-content: center; - font-size: 12px; - margin-right: 10px; + cursor: pointer; + transition: all 0.2s; } -.user-name { - font-size: 13px; +.action-btn:hover { + background-color: #f0f8ff; + border-color: #90cdf4; +} + +.action-icon { + font-size: 14px; +} + +/* Empty state */ +.empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 60px 20px; + background-color: white; + border-radius: 10px; + box-shadow: 0 1px 3px rgba(0,0,0,0.05); +} + +.empty-state-icon { + font-size: 40px; + margin-bottom: 20px; + color: #a0aec0; +} + +.empty-state h3 { + font-size: 18px; + color: #2d3748; + margin-bottom: 10px; +} + +.empty-state p { + color: #718096; + margin-bottom: 20px; + text-align: center; +} + +/* Dialog styles */ +.dialog { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: none; + align-items: center; + justify-content: center; + z-index: 1000; +} + +.dialog.active { + display: flex; +} + +.dialog-content { + background-color: white; + border-radius: 10px; + box-shadow: 0 10px 25px rgba(0,0,0,0.1); + width: 500px; + max-width: 90%; + max-height: 90vh; + overflow-y: auto; +} + +.dialog-header { + padding: 15px 20px; + border-bottom: 1px solid #e2e8f0; + display: flex; + align-items: center; + justify-content: space-between; +} + +.dialog-header h3 { + font-size: 18px; + color: #2d3748; + margin: 0; +} + +.close-dialog-btn { + background: none; + border: none; + font-size: 24px; + cursor: pointer; + color: #a0aec0; +} + +.dialog-body { + padding: 20px; +} + +/* Share dialog specific styles */ +.share-item-info { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; +} + +.item-icon { + font-size: 24px; +} + +.item-name { + font-size: 16px; + font-weight: 500; color: #2d3748; } -.add-user { +.share-link-section { + margin-bottom: 20px; +} + +.share-link-container { + display: flex; + gap: 10px; +} + +.share-link-container input { + flex-grow: 1; + padding: 10px; + border: 1px solid #e2e8f0; + border-radius: 6px; + font-size: 14px; +} + +.share-link-container button { + padding: 0 15px; + background-color: #4a5568; + color: white; + border: none; + border-radius: 6px; + cursor: pointer; + transition: background-color 0.2s; +} + +.share-link-container button:hover { + background-color: #2d3748; +} + +.share-settings { + margin-bottom: 20px; +} + +.share-setting { + margin-bottom: 15px; +} + +.share-setting label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: #4a5568; +} + +.permissions-options { + display: flex; + gap: 20px; +} + +.permissions-options label { + font-weight: normal; display: flex; align-items: center; - justify-content: center; - padding: 8px; - margin-top: 10px; - background-color: #f0f3f7; - border-radius: 50px; + gap: 5px; +} + +.password-setting, .expiration-setting { + display: flex; + align-items: center; + gap: 10px; +} + +.password-setting input[type="checkbox"], .expiration-setting input[type="checkbox"] { + width: auto; +} + +.password-setting input[type="password"], .expiration-setting input[type="date"] { + flex-grow: 1; + padding: 8px 10px; + border: 1px solid #e2e8f0; + border-radius: 6px; + font-size: 14px; +} + +.password-setting button { + padding: 8px 12px; + background-color: #4a5568; + color: white; + border: none; + border-radius: 6px; cursor: pointer; - font-size: 13px; - color: #718096; + font-size: 12px; + transition: background-color 0.2s; +} + +.password-setting button:hover { + background-color: #2d3748; +} + +.share-actions { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +/* Form styles */ +.form-group { + margin-bottom: 15px; +} + +.form-group label { + display: block; + margin-bottom: 8px; + font-weight: 500; + color: #4a5568; +} + +.form-group input, .form-group textarea { + width: 100%; + padding: 10px; + border: 1px solid #e2e8f0; + border-radius: 6px; + font-size: 14px; +} + +.form-group textarea { + resize: vertical; + min-height: 80px; +} + +/* Button styles */ +.button { + padding: 8px 16px; + border: none; + border-radius: 6px; + cursor: pointer; + font-size: 14px; + transition: background-color 0.2s; +} + +.primary { + background-color: #ff5e3a; + color: white; +} + +.primary:hover { + background-color: #e64a29; +} + +.secondary { + background-color: #e2e8f0; + color: #4a5568; +} + +.secondary:hover { + background-color: #cbd5e0; +} + +.danger { + background-color: #f56565; + color: white; +} + +.danger:hover { + background-color: #e53e3e; +} + +/* Notification banner */ +.notification-banner { + position: fixed; + top: 20px; + right: 20px; + padding: 15px 20px; + background-color: white; + border-radius: 8px; + box-shadow: 0 4px 12px rgba(0,0,0,0.1); + display: flex; + align-items: center; + justify-content: space-between; + max-width: 400px; + z-index: 2000; + transform: translateY(-100px); + opacity: 0; + transition: transform 0.3s, opacity 0.3s; +} + +.notification-banner.active { + transform: translateY(0); + opacity: 1; +} + +.notification-banner.success { + border-left: 4px solid #48bb78; +} + +.notification-banner.error { + border-left: 4px solid #f56565; +} + +.close-notification-btn { + background: none; + border: none; + font-size: 18px; + cursor: pointer; + color: #a0aec0; + margin-left: 10px; } /* Responsive */ @@ -968,6 +1527,23 @@ body { .nav-item span { display: none; } + + /* Responsive styles for shared page */ + .shared-filters { + flex-direction: column; + align-items: flex-start; + } + + .shared-list thead th:nth-child(4), + .shared-list thead th:nth-child(5), + .shared-list tbody td:nth-child(4), + .shared-list tbody td:nth-child(5) { + display: none; + } + + .dialog-content { + width: 95%; + } } /* Folder selection for move dialog */ diff --git a/static/index.html b/static/index.html index d8bb0d63..b38c9705 100644 --- a/static/index.html +++ b/static/index.html @@ -14,6 +14,7 @@ + @@ -49,7 +50,7 @@ Files -