├── .gitignore ├── version.yaml ├── ui ├── src │ ├── assets │ │ ├── Images │ │ │ ├── logo.png │ │ │ ├── banner.png │ │ │ └── favicon.ico │ │ └── style.css │ ├── stores │ │ └── formStore.js │ ├── utils │ │ ├── fileDownloader.js │ │ └── passwordGenerator.js │ ├── router │ │ └── index.js │ ├── pages │ │ ├── ErrorGeneral.vue │ │ ├── Error404.vue │ │ ├── View.vue │ │ └── Create.vue │ ├── components │ │ ├── SecretDisplay.vue │ │ └── PasswordInput.vue │ ├── main.js │ ├── App.vue │ └── services │ │ └── api.js ├── package.json ├── vite.config.js ├── index.html └── package-lock.json ├── internal ├── database │ ├── helpers.go │ ├── database.go │ └── database_test.go ├── models │ └── send.go ├── security │ ├── hash.go │ ├── security_test.go │ ├── hash_test.go │ └── security.go ├── config │ ├── config.go │ └── config_test.go ├── routes │ ├── routes.go │ └── routes_test.go └── handlers │ ├── handlers_test.go │ └── handlers.go ├── nginx.conf ├── Makefile ├── docker-compose.prod.sample.yaml ├── LICENSE ├── cmd └── server │ └── main.go ├── docker-compose.yaml ├── SECURITY.md ├── go.mod ├── Dockerfile ├── .github └── workflows │ ├── build&test.yaml │ └── build-and-push-docker.yaml ├── CHANGELOG.md ├── Readme.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules 3 | ui/dist 4 | /.vs 5 | /.vscode 6 | -------------------------------------------------------------------------------- /version.yaml: -------------------------------------------------------------------------------- 1 | #Application version following https://semver.org/ 2 | version: 1.0.9 -------------------------------------------------------------------------------- /ui/src/assets/Images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kek-Sec/GopherDrop/HEAD/ui/src/assets/Images/logo.png -------------------------------------------------------------------------------- /ui/src/assets/Images/banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kek-Sec/GopherDrop/HEAD/ui/src/assets/Images/banner.png -------------------------------------------------------------------------------- /ui/src/assets/Images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kek-Sec/GopherDrop/HEAD/ui/src/assets/Images/favicon.ico -------------------------------------------------------------------------------- /internal/database/helpers.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import "os" 4 | 5 | // removeFile deletes a file from the filesystem. 6 | func removeFile(path string) { 7 | os.Remove(path) 8 | } 9 | -------------------------------------------------------------------------------- /internal/models/send.go: -------------------------------------------------------------------------------- 1 | package models 2 | 3 | import "time" 4 | 5 | // Send represents a stored secret send, either text or file. 6 | type Send struct { 7 | Hash string `gorm:"primary_key"` // Random hash as the unique identifier 8 | Type string 9 | Data string 10 | FilePath string 11 | FileName string 12 | Password string 13 | OneTime bool 14 | ExpiresAt time.Time 15 | CreatedAt time.Time 16 | UpdatedAt time.Time 17 | } 18 | -------------------------------------------------------------------------------- /ui/src/stores/formStore.js: -------------------------------------------------------------------------------- 1 | import { reactive } from 'vue'; 2 | 3 | /** 4 | * A simple reactive store to handle cross-component communication 5 | * for resetting the create form. 6 | */ 7 | export const formStore = reactive({ 8 | // A counter that components can watch. Incrementing it signals a reset. 9 | resetCounter: 0, 10 | 11 | // Function to trigger the reset from anywhere in the app. 12 | triggerReset() { 13 | this.resetCounter++; 14 | } 15 | }); -------------------------------------------------------------------------------- /ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gopherdrop-ui", 3 | "version": "1.0.1", 4 | "scripts": { 5 | "dev": "vite", 6 | "build": "vite build", 7 | "preview": "vite preview" 8 | }, 9 | "dependencies": { 10 | "@mdi/font": "^7.4.47", 11 | "vite-plugin-vuetify": "^1.0.1", 12 | "vue": "^3.3.4", 13 | "vue-router": "^4.2.5", 14 | "vuetify": "^3.7.5" 15 | }, 16 | "devDependencies": { 17 | "@vitejs/plugin-vue": "^4.0.0", 18 | "vite": "^4.5.9" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/security/hash.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base64" 6 | "errors" 7 | ) 8 | 9 | // GenerateHash creates a secure random hash of the specified length. 10 | func GenerateHash(length int) (string, error) { 11 | if length < 0 { 12 | return "", errors.New("length must be a non-negative integer") 13 | } 14 | 15 | bytes := make([]byte, length) 16 | _, err := rand.Read(bytes) 17 | if err != nil { 18 | return "", err 19 | } 20 | return base64.URLEncoding.EncodeToString(bytes)[:length], nil 21 | } 22 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | server_name localhost; 4 | 5 | client_max_body_size 0; 6 | 7 | root /usr/share/nginx/html; 8 | index index.html; 9 | 10 | # Serve the frontend 11 | location / { 12 | try_files $uri $uri/ /index.html; 13 | } 14 | 15 | # Proxy API requests to the backend service 16 | location /api/ { 17 | proxy_pass http://app:8080/; 18 | proxy_set_header Host $host; 19 | proxy_set_header X-Real-IP $remote_addr; 20 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 21 | proxy_set_header X-Forwarded-Proto $scheme; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/utils/fileDownloader.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module fileDownloader 3 | * @description Provides a function for downloading files in the browser. 4 | */ 5 | 6 | /** 7 | * Triggers a browser download for a file blob. 8 | * @param {Blob} blob - The file blob to download. 9 | * @param {string} filename - The desired name for the downloaded file. 10 | */ 11 | export function downloadFile(blob, filename) { 12 | const url = URL.createObjectURL(blob); 13 | const a = document.createElement('a'); 14 | a.href = url; 15 | a.download = filename || 'download'; 16 | document.body.appendChild(a); 17 | a.click(); 18 | document.body.removeChild(a); 19 | URL.revokeObjectURL(url); 20 | } -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build build-debug up up-debug down logs dbshell start-db stop-db test ui-build ui-up ui-down 2 | 3 | # Default build (production) 4 | build: 5 | docker compose build 6 | 7 | # Build in debug mode 8 | build-debug: 9 | docker compose build --build-arg DEBUG=true --build-arg GIN_MODE=debug 10 | 11 | up: 12 | docker compose up -d 13 | 14 | down: 15 | docker compose down 16 | 17 | logs: 18 | docker compose logs -f app 19 | 20 | dbshell: 21 | docker compose exec db psql -U $(DB_USER) -d $(DB_NAME) 22 | 23 | start-db: 24 | docker compose up -d db 25 | 26 | stop-db: 27 | docker compose stop db 28 | 29 | test: 30 | go test ./... -v 31 | 32 | ui-build: 33 | @echo "Building UI..." 34 | cd ui && npm install && npm run build 35 | 36 | ui-up: 37 | cd ui && npm run dev 38 | -------------------------------------------------------------------------------- /ui/src/utils/passwordGenerator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module passwordGenerator 3 | * @description Provides a function for generating random passwords. 4 | */ 5 | 6 | /** 7 | * Generates a random password. 8 | * @param {number} length - The length of the password. 9 | * @param {string} charset - The character set to use for the password. 10 | * @returns {string} The generated password. 11 | */ 12 | export function generatePassword(length = 12, charset = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+') { 13 | let generatedPassword = ''; 14 | const randomValues = new Uint32Array(length); 15 | window.crypto.getRandomValues(randomValues); 16 | for (let i = 0; i < length; i++) { 17 | const randomIndex = randomValues[i] % charset.length; 18 | generatedPassword += charset[randomIndex]; 19 | } 20 | return generatedPassword; 21 | } -------------------------------------------------------------------------------- /ui/src/router/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Sets up the Vue Router. 3 | * The import paths are now relative to the `src/router/` directory. 4 | */ 5 | import { createRouter, createWebHistory } from 'vue-router'; 6 | import Create from '../pages/Create.vue'; // Corrected Path 7 | import View from '../pages/View.vue'; // Corrected Path 8 | import Error404 from '../pages/Error404.vue'; // Corrected Path 9 | import ErrorGeneral from '../pages/ErrorGeneral.vue'; // Corrected Path 10 | 11 | const routes = [ 12 | { path: '/', component: Create, name: 'create' }, 13 | { path: '/view/:hash', component: View, name: 'view' }, 14 | { path: '/error', component: ErrorGeneral, name: 'error' }, 15 | { path: '/:pathMatch(.*)*', component: Error404, name: 'not-found' } 16 | ]; 17 | 18 | const router = createRouter({ 19 | history: createWebHistory(), 20 | routes 21 | }); 22 | 23 | export default router; 24 | -------------------------------------------------------------------------------- /ui/src/pages/ErrorGeneral.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 22 | 23 | -------------------------------------------------------------------------------- /ui/src/assets/style.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Global styles for responsive and clean layout. 3 | */ 4 | @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;700&display=swap'); 5 | 6 | html, body { 7 | margin: 0; 8 | padding: 0; 9 | font-family: 'Inter', Arial, sans-serif; 10 | color: #333; 11 | background: #f4f4f9; /* Fallback background */ 12 | } 13 | 14 | /* Add a subtle gradient background */ 15 | body { 16 | background: linear-gradient(180deg, rgba(232,222,248,0.3) 0%, rgba(255,255,255,1) 30%); 17 | } 18 | 19 | .v-application { 20 | background: transparent !important; 21 | } 22 | 23 | 24 | a { 25 | color: #6750A4; /* Using the new primary color */ 26 | text-decoration: none; 27 | transition: color 0.3s ease; 28 | } 29 | 30 | a:hover { 31 | text-decoration: underline; 32 | } 33 | 34 | @media (max-width: 600px) { 35 | main, header { 36 | padding: 0.5rem; 37 | } 38 | } -------------------------------------------------------------------------------- /ui/src/pages/Error404.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 24 | -------------------------------------------------------------------------------- /ui/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import vue from '@vitejs/plugin-vue' 3 | import vuetify from 'vite-plugin-vuetify' 4 | 5 | // Create HTML plugin instance with template parameters 6 | const htmlPlugin = () => { 7 | return { 8 | name: 'html-transform', 9 | transformIndexHtml(html) { 10 | return html.replace( 11 | /__TITLE__/g, 12 | process.env.VITE_APP_TITLE || 'GopherDrop' 13 | ).replace( 14 | /__DESCRIPTION__/g, 15 | process.env.VITE_APP_DESCRIPTION || 'Secure one-time secret and file sharing' 16 | ) 17 | } 18 | } 19 | } 20 | 21 | export default defineConfig({ 22 | plugins: [vue(), vuetify(), htmlPlugin()], 23 | define: { 24 | 'process.env.VITE_APP_TITLE': JSON.stringify(process.env.VITE_APP_TITLE || 'GopherDrop'), 25 | 'process.env.VITE_APP_DESCRIPTION': JSON.stringify(process.env.VITE_APP_DESCRIPTION || 'Secure one-time secret and file sharing') 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /internal/security/security_test.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestPadKey(t *testing.T) { 8 | key := PadKey("short") 9 | if len(key) != 32 { 10 | t.Fatalf("expected key length 32, got %d", len(key)) 11 | } 12 | } 13 | 14 | func TestEncryptDecryptData(t *testing.T) { 15 | key := []byte(PadKey("password")) 16 | original := []byte("hello world") 17 | enc, err := EncryptData(original, key) 18 | if err != nil { 19 | t.Fatalf("encryption failed: %v", err) 20 | } 21 | dec, err := DecryptData(enc, key) 22 | if err != nil { 23 | t.Fatalf("decryption failed: %v", err) 24 | } 25 | if string(dec) != string(original) { 26 | t.Fatalf("expected '%s' got '%s'", original, dec) 27 | } 28 | } 29 | 30 | func TestGenerateHash(t *testing.T) { 31 | hash, err := GenerateHash(16) 32 | if err != nil { 33 | t.Fatalf("failed to generate hash: %v", err) 34 | } 35 | if len(hash) != 16 { 36 | t.Fatalf("expected length 16, got %d", len(hash)) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/database/database.go: -------------------------------------------------------------------------------- 1 | // Package database handles database connection and maintenance routines. 2 | package database 3 | 4 | import ( 5 | "log" 6 | "time" 7 | 8 | "github.com/jinzhu/gorm" 9 | _ "github.com/lib/pq" 10 | 11 | "github.com/kek-Sec/gopherdrop/internal/config" 12 | "github.com/kek-Sec/gopherdrop/internal/models" 13 | ) 14 | 15 | // InitDB connects to PostgreSQL using environment variables. 16 | func InitDB(cfg config.Config) *gorm.DB { 17 | dsn := "host=" + cfg.DBHost + " user=" + cfg.DBUser + " password=" + cfg.DBPass + " dbname=" + cfg.DBName + " sslmode=" + cfg.DBSSLMode 18 | db, err := gorm.Open("postgres", dsn) 19 | if err != nil { 20 | log.Fatal(err) 21 | } 22 | return db 23 | } 24 | 25 | // CleanupExpired periodically removes expired sends from the database. 26 | func CleanupExpired(db *gorm.DB) { 27 | for { 28 | time.Sleep(time.Hour) 29 | var sends []models.Send 30 | db.Where("expires_at < ?", time.Now()).Find(&sends) 31 | for _, s := range sends { 32 | if s.Type == "file" && s.FilePath != "" { 33 | removeFile(s.FilePath) 34 | } 35 | db.Delete(&s) 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docker-compose.prod.sample.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: postgres:17-alpine 4 | container_name: gopherdrop_db 5 | environment: 6 | POSTGRES_USER: user 7 | POSTGRES_PASSWORD: pass 8 | POSTGRES_DB: gopherdropdb 9 | volumes: 10 | - ./db_data:/var/lib/postgresql/data 11 | healthcheck: 12 | test: ["CMD-SHELL", "pg_isready -U user -d gopherdropdb"] 13 | interval: 10s 14 | timeout: 5s 15 | retries: 5 16 | networks: 17 | - gophernet 18 | 19 | app: 20 | image: petrakisg/gopherdrop:1.0.4 21 | container_name: gopherdrop_app 22 | environment: 23 | DB_HOST: db 24 | DB_USER: user 25 | DB_PASSWORD: pass 26 | DB_NAME: gopherdropdb 27 | DB_SSLMODE: disable 28 | SECRET_KEY: supersecretkeysupersecretkey32 29 | LISTEN_ADDR: :8080 30 | STORAGE_PATH: /app/storage 31 | MAX_FILE_SIZE: 10485760 32 | depends_on: 33 | db: 34 | condition: service_healthy 35 | volumes: 36 | - ./app_storage:/app/storage 37 | networks: 38 | - gophernet 39 | 40 | networks: 41 | gophernet: 42 | external: true 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) [2024] [Petrakis Georgios] 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cmd/server/main.go: -------------------------------------------------------------------------------- 1 | // Package main starts the GopherDrop server. 2 | package main 3 | 4 | import ( 5 | "log" 6 | "net/http" 7 | "time" 8 | 9 | "github.com/kek-Sec/gopherdrop/internal/config" 10 | "github.com/kek-Sec/gopherdrop/internal/database" 11 | "github.com/kek-Sec/gopherdrop/internal/models" 12 | "github.com/kek-Sec/gopherdrop/internal/routes" 13 | ) 14 | 15 | var version = "dev" 16 | 17 | // main initializes configuration, database, and the HTTP server. 18 | func main() { 19 | log.Printf("Starting GopherDrop: %s", version) 20 | 21 | // Load configuration 22 | cfg := config.LoadConfig() 23 | 24 | // Initialize database 25 | db := database.InitDB(cfg) 26 | db.AutoMigrate(&models.Send{}) 27 | go database.CleanupExpired(db) 28 | 29 | // Setup router 30 | r := routes.SetupRouter(cfg, db) 31 | 32 | // Configure HTTP server with timeouts 33 | server := &http.Server{ 34 | Addr: cfg.ListenAddr, 35 | Handler: r, 36 | ReadTimeout: 15 * time.Second, 37 | WriteTimeout: 15 * time.Second, 38 | IdleTimeout: 60 * time.Second, 39 | } 40 | 41 | // Start the server 42 | log.Printf("Listening on %s", cfg.ListenAddr) 43 | log.Fatal(server.ListenAndServe()) 44 | } 45 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: postgres:17-alpine 4 | container_name: gopherdrop_db 5 | environment: 6 | POSTGRES_USER: user 7 | POSTGRES_PASSWORD: pass 8 | POSTGRES_DB: gopherdropdb 9 | volumes: 10 | - db_data:/var/lib/postgresql/data 11 | healthcheck: 12 | test: ["CMD-SHELL", "pg_isready -U user -d gopherdropdb"] 13 | interval: 10s 14 | timeout: 5s 15 | retries: 5 16 | networks: 17 | - gopherdrop 18 | 19 | app: 20 | build: 21 | context: . 22 | dockerfile: Dockerfile 23 | args: 24 | - VITE_API_URL=/api 25 | - VITE_APP_TITLE=TEST 26 | - VITE_APP_DESCRIPTION=TEST_DESCR 27 | container_name: gopherdrop_app 28 | environment: 29 | DB_HOST: db 30 | DB_USER: user 31 | DB_PASSWORD: pass 32 | DB_NAME: gopherdropdb 33 | DB_SSLMODE: disable 34 | SECRET_KEY: supersecretkeysupersecretkey32 35 | LISTEN_ADDR: :8080 36 | STORAGE_PATH: /app/storage 37 | MAX_FILE_SIZE: 10485760 38 | depends_on: 39 | db: 40 | condition: service_healthy 41 | ports: 42 | - "8081:80" 43 | networks: 44 | - gopherdrop 45 | 46 | networks: 47 | gopherdrop: 48 | driver: bridge 49 | 50 | volumes: 51 | db_data: {} 52 | -------------------------------------------------------------------------------- /ui/src/components/SecretDisplay.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | -------------------------------------------------------------------------------- /internal/security/hash_test.go: -------------------------------------------------------------------------------- 1 | package security 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/stretchr/testify/assert" 7 | ) 8 | 9 | func TestGenerateHash_ValidLength(t *testing.T) { 10 | length := 16 11 | hash, err := GenerateHash(length) 12 | 13 | assert.NoError(t, err, "GenerateHash should not return an error") 14 | assert.Equal(t, length, len(hash), "Generated hash should have the correct length") 15 | } 16 | 17 | func TestGenerateHash_ZeroLength(t *testing.T) { 18 | length := 0 19 | hash, err := GenerateHash(length) 20 | 21 | assert.NoError(t, err, "GenerateHash should not return an error for zero length") 22 | assert.Equal(t, 0, len(hash), "Generated hash should be empty for zero length") 23 | } 24 | func TestGenerateHash_NegativeLength(t *testing.T) { 25 | length := -1 26 | hash, err := GenerateHash(length) 27 | 28 | assert.Error(t, err, "GenerateHash should return an error for negative length") 29 | assert.Empty(t, hash, "Generated hash should be empty for negative length") 30 | } 31 | 32 | 33 | func TestGenerateHash_UniqueHashes(t *testing.T) { 34 | length := 16 35 | hash1, err1 := GenerateHash(length) 36 | hash2, err2 := GenerateHash(length) 37 | 38 | assert.NoError(t, err1, "First GenerateHash call should not return an error") 39 | assert.NoError(t, err2, "Second GenerateHash call should not return an error") 40 | assert.NotEqual(t, hash1, hash2, "Generated hashes should be unique") 41 | } 42 | -------------------------------------------------------------------------------- /ui/src/components/PasswordInput.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | -------------------------------------------------------------------------------- /internal/config/config.go: -------------------------------------------------------------------------------- 1 | // Package config provides application configuration loading from environment variables. 2 | package config 3 | 4 | import ( 5 | "os" 6 | "strconv" 7 | ) 8 | 9 | // Config holds all environment-based configuration. 10 | type Config struct { 11 | DBHost string 12 | DBUser string 13 | DBPass string 14 | DBName string 15 | DBSSLMode string 16 | SecretKey string 17 | ListenAddr string 18 | StoragePath string 19 | MaxFileSize int64 20 | } 21 | 22 | // LoadConfig returns a Config object populated from environment variables. 23 | func LoadConfig() Config { 24 | cfg := Config{ 25 | DBHost: os.Getenv("DB_HOST"), 26 | DBUser: os.Getenv("DB_USER"), 27 | DBPass: os.Getenv("DB_PASSWORD"), 28 | DBName: os.Getenv("DB_NAME"), 29 | DBSSLMode: os.Getenv("DB_SSLMODE"), 30 | SecretKey: os.Getenv("SECRET_KEY"), 31 | ListenAddr: os.Getenv("LISTEN_ADDR"), 32 | StoragePath: os.Getenv("STORAGE_PATH"), 33 | } 34 | 35 | if cfg.ListenAddr == "" { 36 | cfg.ListenAddr = ":8080" 37 | } 38 | if cfg.StoragePath == "" { 39 | cfg.StoragePath = "/tmp" 40 | } 41 | 42 | maxFileSizeStr := os.Getenv("MAX_FILE_SIZE") 43 | if maxFileSizeStr == "" { 44 | // Default 10MB if not set 45 | cfg.MaxFileSize = 10 * 1024 * 1024 46 | } else { 47 | size, err := strconv.ParseInt(maxFileSizeStr, 10, 64) 48 | if err != nil { 49 | cfg.MaxFileSize = 10 * 1024 * 1024 50 | } else { 51 | cfg.MaxFileSize = size 52 | } 53 | } 54 | 55 | return cfg 56 | } 57 | -------------------------------------------------------------------------------- /internal/routes/routes.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-contrib/cors" 7 | "github.com/gin-gonic/gin" 8 | "github.com/jinzhu/gorm" 9 | "golang.org/x/time/rate" 10 | 11 | "github.com/kek-Sec/gopherdrop/internal/handlers" 12 | "github.com/kek-Sec/gopherdrop/internal/config" 13 | ) 14 | 15 | // Define a rate limiter with 1 request per second and a burst of 5. 16 | var limiter = rate.NewLimiter(1, 6) 17 | 18 | // rateLimiterMiddleware applies rate limiting to the endpoint. 19 | func rateLimiterMiddleware(c *gin.Context) { 20 | if !limiter.Allow() { 21 | c.JSON(http.StatusTooManyRequests, gin.H{"error": "too many requests"}) 22 | c.Abort() 23 | return 24 | } 25 | c.Next() 26 | } 27 | 28 | // SetupRouter initializes the Gin router with routes and middleware. 29 | func SetupRouter(cfg config.Config, db *gorm.DB) *gin.Engine { 30 | r := gin.Default() 31 | 32 | // Enable CORS middleware 33 | r.Use(cors.New(cors.Config{ 34 | AllowOrigins: []string{"*"}, 35 | AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, 36 | AllowHeaders: []string{"Origin", "Content-Type", "Authorization"}, 37 | ExposeHeaders: []string{"Content-Length", "Content-Disposition"}, 38 | AllowCredentials: true, 39 | })) 40 | 41 | // Apply rate limiting only to the POST /send endpoint 42 | r.POST("/send", rateLimiterMiddleware, handlers.CreateSend(cfg, db)) 43 | 44 | // Other routes without rate limiting 45 | r.GET("/send/:id", handlers.GetSend(cfg, db)) 46 | r.GET("/send/:id/check", handlers.CheckPasswordProtection(db)) 47 | 48 | return r 49 | } 50 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | | Version | Supported | 6 | | ------- | ------------------ | 7 | | 1.0.x | :white_check_mark: | 8 | | < 1.0.0 | :x: | 9 | 10 | ## Reporting a Vulnerability 11 | 12 | We take security seriously and appreciate your efforts to responsibly disclose vulnerabilities. If you discover a security issue in **GopherDrop**, please report it through GitHub’s security advisories: 13 | 14 | 1. **Go to the Repository**: Visit the [GopherDrop repository](https://github.com/kek-Sec/gopherdrop). 15 | 16 | 2. **Report a Vulnerability**: 17 | - Click on **"Security"** in the top navigation bar. 18 | - Select **"Report a vulnerability"**. 19 | - Fill in the details of the vulnerability, including: 20 | - Steps to reproduce the issue. 21 | - Potential impact or severity. 22 | - Any relevant code snippets, screenshots, or logs. 23 | 24 | ### What to Expect 25 | 26 | - **Acknowledgment**: We will acknowledge your report within **48 hours**. 27 | - **Initial Assessment**: We aim to provide an initial assessment within **5 business days**. 28 | - **Updates**: You will receive regular updates as we investigate and resolve the issue. 29 | - **Resolution**: Once resolved, we will notify you and credit you in the release notes unless you wish to remain anonymous. 30 | 31 | ### Responsible Disclosure 32 | 33 | Please **do not publicly disclose** the vulnerability until we have addressed it. We are committed to resolving security issues promptly and responsibly. 34 | 35 | --- 36 | 37 | Thank you for helping us keep **GopherDrop** secure! 38 | -------------------------------------------------------------------------------- /ui/src/main.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Main application entry point. 3 | * Initializes Vue, Vuetify, and the Router. 4 | */ 5 | import { createApp } from 'vue' 6 | import App from './App.vue' 7 | import router from './router' 8 | 9 | // Import Vuetify and styles 10 | import 'vuetify/styles' 11 | import { createVuetify } from 'vuetify' 12 | import '@mdi/font/css/materialdesignicons.css' 13 | 14 | // Create a custom light theme 15 | const customLightTheme = { 16 | dark: false, 17 | colors: { 18 | primary: '#6750A4', // A modern purple 19 | secondary: '#E8DEF8', // A light, complementary purple 20 | background: '#FFFFFF', 21 | surface: '#FEF7FF', // Off-white for cards and surfaces 22 | info: '#6750A4', // Using primary color for info state for consistency 23 | error: '#B3261E', 24 | success: '#4CAF50', 25 | warning: '#FB8C00', 26 | } 27 | }; 28 | 29 | // Create a custom dark theme 30 | const customDarkTheme = { 31 | dark: true, 32 | colors: { 33 | primary: '#D0BCFF', // A lighter purple for dark mode contrast 34 | secondary: '#4A4458', 35 | background: '#141218', 36 | surface: '#262329', 37 | info: '#D0BCFF', // Lighter purple for info state 38 | error: '#F2B8B5', 39 | success: '#B7F3B9', 40 | warning: '#FFD6A8', 41 | } 42 | } 43 | 44 | // Create Vuetify instance 45 | const vuetify = createVuetify({ 46 | theme: { 47 | defaultTheme: 'customLightTheme', 48 | themes: { 49 | customLightTheme, 50 | customDarkTheme 51 | }, 52 | }, 53 | icons: { 54 | defaultSet: 'mdi', 55 | }, 56 | }) 57 | 58 | // Create and mount the Vue application 59 | const app = createApp(App) 60 | app.use(router) 61 | app.use(vuetify) 62 | app.mount('#app') -------------------------------------------------------------------------------- /ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | __TITLE__ 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 |
38 | 39 | 40 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /internal/security/security.go: -------------------------------------------------------------------------------- 1 | // Package security provides encryption and decryption functions. 2 | package security 3 | 4 | import ( 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "crypto/rand" 8 | "encoding/base64" 9 | "errors" 10 | "io" 11 | ) 12 | 13 | // PadKey ensures the key is 32 bytes. 14 | func PadKey(k string) string { 15 | for len(k) < 32 { 16 | k += "0" 17 | } 18 | return k[:32] 19 | } 20 | 21 | // EncryptData encrypts data using AES-256 CFB mode. 22 | func EncryptData(data []byte, key []byte) (string, error) { 23 | block, err := aes.NewCipher(key) 24 | if err != nil { 25 | return "", err 26 | } 27 | 28 | // Generate a random IV 29 | iv := make([]byte, aes.BlockSize) 30 | if _, err := io.ReadFull(rand.Reader, iv); err != nil { 31 | return "", err 32 | } 33 | 34 | if len(data) > (1<<31 - 1 - aes.BlockSize) { 35 | return "", errors.New("data too large to encrypt") 36 | } 37 | ciphertext := make([]byte, aes.BlockSize+len(data)) 38 | copy(ciphertext[:aes.BlockSize], iv) 39 | 40 | stream := cipher.NewCFBEncrypter(block, iv) 41 | stream.XORKeyStream(ciphertext[aes.BlockSize:], data) 42 | 43 | return base64.StdEncoding.EncodeToString(ciphertext), nil 44 | } 45 | 46 | // DecryptData decrypts data previously encrypted with AES-256 CFB. 47 | func DecryptData(enc string, key []byte) ([]byte, error) { 48 | data, err := base64.StdEncoding.DecodeString(enc) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | block, err := aes.NewCipher(key) 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | if len(data) < aes.BlockSize { 59 | return nil, errors.New("ciphertext too short") 60 | } 61 | 62 | iv := data[:aes.BlockSize] 63 | data = data[aes.BlockSize:] 64 | 65 | stream := cipher.NewCFBDecrypter(block, iv) 66 | stream.XORKeyStream(data, data) 67 | 68 | return data, nil 69 | } 70 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/kek-Sec/gopherdrop 2 | 3 | go 1.22.4 4 | 5 | require ( 6 | github.com/gin-contrib/cors v1.7.2 7 | github.com/gin-gonic/gin v1.10.0 8 | github.com/jinzhu/gorm v1.9.16 9 | github.com/lib/pq v1.10.9 10 | github.com/stretchr/testify v1.10.0 11 | ) 12 | 13 | require ( 14 | github.com/bytedance/sonic v1.11.6 // indirect 15 | github.com/bytedance/sonic/loader v0.1.1 // indirect 16 | github.com/cloudwego/base64x v0.1.4 // indirect 17 | github.com/cloudwego/iasm v0.2.0 // indirect 18 | github.com/davecgh/go-spew v1.1.1 // indirect 19 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 20 | github.com/gin-contrib/sse v0.1.0 // indirect 21 | github.com/go-playground/locales v0.14.1 // indirect 22 | github.com/go-playground/universal-translator v0.18.1 // indirect 23 | github.com/go-playground/validator/v10 v10.20.0 // indirect 24 | github.com/goccy/go-json v0.10.2 // indirect 25 | github.com/jinzhu/inflection v1.0.0 // indirect 26 | github.com/json-iterator/go v1.1.12 // indirect 27 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 28 | github.com/kr/text v0.2.0 // indirect 29 | github.com/leodido/go-urn v1.4.0 // indirect 30 | github.com/mattn/go-isatty v0.0.20 // indirect 31 | github.com/mattn/go-sqlite3 v1.14.0 // indirect 32 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 33 | github.com/modern-go/reflect2 v1.0.2 // indirect 34 | github.com/pelletier/go-toml/v2 v2.2.2 // indirect 35 | github.com/pmezard/go-difflib v1.0.0 // indirect 36 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 37 | github.com/ugorji/go/codec v1.2.12 // indirect 38 | golang.org/x/arch v0.8.0 // indirect 39 | golang.org/x/crypto v0.31.0 // indirect 40 | golang.org/x/net v0.25.0 // indirect 41 | golang.org/x/sys v0.28.0 // indirect 42 | golang.org/x/text v0.21.0 // indirect 43 | golang.org/x/time v0.8.0 44 | google.golang.org/protobuf v1.34.1 // indirect 45 | gopkg.in/yaml.v3 v3.0.1 // indirect 46 | ) 47 | -------------------------------------------------------------------------------- /internal/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestLoadConfig_Defaults(t *testing.T) { 11 | // Clear environment variables to test defaults 12 | os.Clearenv() 13 | 14 | cfg := LoadConfig() 15 | 16 | assert.Equal(t, ":8080", cfg.ListenAddr, "ListenAddr should default to :8080") 17 | assert.Equal(t, "/tmp", cfg.StoragePath, "StoragePath should default to /tmp") 18 | assert.Equal(t, int64(10*1024*1024), cfg.MaxFileSize, "MaxFileSize should default to 10MB") 19 | } 20 | 21 | func TestLoadConfig_EnvVariables(t *testing.T) { 22 | // Set environment variables 23 | os.Setenv("DB_HOST", "localhost") 24 | os.Setenv("DB_USER", "testuser") 25 | os.Setenv("DB_PASSWORD", "testpass") 26 | os.Setenv("DB_NAME", "testdb") 27 | os.Setenv("DB_SSLMODE", "disable") 28 | os.Setenv("SECRET_KEY", "supersecret") 29 | os.Setenv("LISTEN_ADDR", ":9090") 30 | os.Setenv("STORAGE_PATH", "/var/data") 31 | os.Setenv("MAX_FILE_SIZE", "5242880") // 5MB 32 | 33 | cfg := LoadConfig() 34 | 35 | assert.Equal(t, "localhost", cfg.DBHost, "DBHost should match the environment variable") 36 | assert.Equal(t, "testuser", cfg.DBUser, "DBUser should match the environment variable") 37 | assert.Equal(t, "testpass", cfg.DBPass, "DBPass should match the environment variable") 38 | assert.Equal(t, "testdb", cfg.DBName, "DBName should match the environment variable") 39 | assert.Equal(t, "disable", cfg.DBSSLMode, "DBSSLMode should match the environment variable") 40 | assert.Equal(t, "supersecret", cfg.SecretKey, "SecretKey should match the environment variable") 41 | assert.Equal(t, ":9090", cfg.ListenAddr, "ListenAddr should match the environment variable") 42 | assert.Equal(t, "/var/data", cfg.StoragePath, "StoragePath should match the environment variable") 43 | assert.Equal(t, int64(5242880), cfg.MaxFileSize, "MaxFileSize should match the environment variable") 44 | } 45 | 46 | func TestLoadConfig_InvalidMaxFileSize(t *testing.T) { 47 | // Set invalid MAX_FILE_SIZE 48 | os.Setenv("MAX_FILE_SIZE", "invalid") 49 | 50 | cfg := LoadConfig() 51 | 52 | assert.Equal(t, int64(10*1024*1024), cfg.MaxFileSize, "MaxFileSize should default to 10MB on invalid input") 53 | } 54 | -------------------------------------------------------------------------------- /internal/database/database_test.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/jinzhu/gorm" 8 | _ "github.com/jinzhu/gorm/dialects/sqlite" 9 | 10 | "github.com/kek-Sec/gopherdrop/internal/config" 11 | "github.com/kek-Sec/gopherdrop/internal/models" 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | // TestInitDB tests the InitDB function. 16 | func TestInitDB(t *testing.T) { 17 | cfg := config.Config{ 18 | DBHost: "", 19 | DBUser: "", 20 | DBPass: "", 21 | DBName: ":memory:", 22 | DBSSLMode: "", 23 | } 24 | 25 | db, err := gorm.Open("sqlite3", cfg.DBName) 26 | if err != nil { 27 | t.Fatalf("Failed to initialize test database: %v", err) 28 | } 29 | defer db.Close() 30 | 31 | assert.NotNil(t, db, "Database connection should not be nil") 32 | } 33 | 34 | // TestCleanupExpired tests the CleanupExpired function. 35 | func TestCleanupExpired(t *testing.T) { 36 | // Setup an in-memory database 37 | db, err := gorm.Open("sqlite3", ":memory:") 38 | if err != nil { 39 | t.Fatalf("Failed to initialize test database: %v", err) 40 | } 41 | defer db.Close() 42 | 43 | // Migrate the Send model 44 | db.AutoMigrate(&models.Send{}) 45 | 46 | // Create some test data 47 | now := time.Now() 48 | expiredSend := models.Send{ 49 | Hash: "expired1", 50 | Type: "text", 51 | Data: "some data", 52 | ExpiresAt: now.Add(-1 * time.Hour), 53 | } 54 | 55 | validSend := models.Send{ 56 | Hash: "valid1", 57 | Type: "text", 58 | Data: "valid data", 59 | ExpiresAt: now.Add(1 * time.Hour), 60 | } 61 | 62 | db.Create(&expiredSend) 63 | db.Create(&validSend) 64 | 65 | // Run the cleanup function (without the infinite loop) 66 | func() { 67 | var sends []models.Send 68 | db.Where("expires_at < ?", time.Now()).Find(&sends) 69 | for _, s := range sends { 70 | db.Delete(&s) 71 | } 72 | }() 73 | 74 | // Verify that the expired send was deleted 75 | var result models.Send 76 | err = db.Where("hash = ?", "expired1").First(&result).Error 77 | assert.Equal(t, gorm.ErrRecordNotFound, err, "Expired send should be deleted") 78 | 79 | // Verify that the valid send still exists 80 | err = db.Where("hash = ?", "valid1").First(&result).Error 81 | assert.Nil(t, err, "Valid send should still exist") 82 | } 83 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Stage 1: Build the Go Backend 2 | FROM golang:1.22-alpine AS backend-builder 3 | 4 | WORKDIR /app 5 | COPY . . 6 | 7 | # Add build arguments for debug mode and versioning 8 | ARG DEBUG=false 9 | ARG GIN_MODE=release 10 | ENV GIN_MODE=${GIN_MODE} 11 | ARG VERSION 12 | ENV VERSION=${VERSION} 13 | 14 | # Set build tags based on the DEBUG flag and include versioning information 15 | RUN if [ "$DEBUG" = "true" ]; then \ 16 | go mod download && go build -o server -tags debug -ldflags="-X main.version=DEBUG" ./cmd/server/main.go; \ 17 | else \ 18 | go mod download && go build -o server -ldflags="-X main.version=${VERSION}" ./cmd/server/main.go; \ 19 | fi 20 | 21 | # Stage 2: Build the Vue.js Frontend 22 | FROM node:23-alpine AS frontend-builder 23 | 24 | WORKDIR /app 25 | COPY ui/package.json ui/package-lock.json ./ 26 | RUN npm install --legacy-peer-deps 27 | 28 | # Add build arguments for customization 29 | ARG VITE_API_URL="/api" 30 | ARG VITE_APP_TITLE="GopherDrop" 31 | ARG VITE_APP_DESCRIPTION="Secure one-time secret and file sharing" 32 | 33 | ENV VITE_API_URL=${VITE_API_URL} 34 | ENV VITE_APP_TITLE=${VITE_APP_TITLE} 35 | ENV VITE_APP_DESCRIPTION=${VITE_APP_DESCRIPTION} 36 | 37 | COPY ui ./ 38 | RUN npm run build 39 | 40 | # Stage 3: Combine Backend and Frontend into a Single Image 41 | FROM nginx:alpine 42 | 43 | # Add OCI Image Spec labels 44 | ARG GIT_COMMIT_SHA 45 | ARG GIT_VERSION 46 | 47 | LABEL org.opencontainers.image.title="GopherDrop" \ 48 | org.opencontainers.image.description="GopherDrop - Secure one-time secret sharing service" \ 49 | org.opencontainers.image.source="https://github.com/kek-Sec/gopherdrop" \ 50 | org.opencontainers.image.url="https://github.com/kek-Sec/gopherdrop" \ 51 | org.opencontainers.image.documentation="https://github.com/kek-Sec/gopherdrop" \ 52 | org.opencontainers.image.licenses="MIT" 53 | 54 | # Copy the Go server binary 55 | COPY --from=backend-builder /app/server /app/server 56 | 57 | # Copy the frontend static files 58 | COPY --from=frontend-builder /app/dist /usr/share/nginx/html 59 | 60 | # Copy Nginx configuration 61 | COPY nginx.conf /etc/nginx/conf.d/default.conf 62 | 63 | # Create the storage directory for the backend 64 | RUN mkdir -p /app/storage 65 | 66 | # Expose the ports for Nginx and the Go server 67 | EXPOSE 80 8080 68 | 69 | # Run both the Go server and Nginx using a simple script 70 | CMD ["/bin/sh", "-c", "/app/server & nginx -g 'daemon off;'"] 71 | -------------------------------------------------------------------------------- /ui/src/App.vue: -------------------------------------------------------------------------------- 1 | 44 | 45 | 80 | 81 | -------------------------------------------------------------------------------- /.github/workflows/build&test.yaml: -------------------------------------------------------------------------------- 1 | name: 🛠️ Build, Test & Scan 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build-go: 10 | name: 🐹 Build Go Project 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: 📥 Checkout Code 15 | uses: actions/checkout@v4 16 | 17 | - name: 🐹 Set Up Go 18 | uses: actions/setup-go@v5 19 | with: 20 | go-version: '1.22' 21 | 22 | - name: 📦 Install Go Dependencies 23 | run: go mod tidy 24 | 25 | - name: 🛠️ Build Go Project 26 | run: go build -o server ./cmd/server/main.go 27 | 28 | build-vue: 29 | name: 🌐 Build Vue Project 30 | runs-on: ubuntu-latest 31 | 32 | steps: 33 | - name: 📥 Checkout Code 34 | uses: actions/checkout@v4 35 | 36 | - name: 🌐 Set Up Node.js 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version: '20' 40 | 41 | - name: 📦 Install Vue Dependencies 42 | run: | 43 | cd ui 44 | npm install 45 | 46 | - name: 🖥️ Build Vue Project 47 | run: | 48 | cd ui 49 | npm run build 50 | 51 | test-go: 52 | name: ✅ Run Go Tests 53 | runs-on: ubuntu-latest 54 | 55 | steps: 56 | - name: 📥 Checkout Code 57 | uses: actions/checkout@v4 58 | 59 | - name: 🐹 Set Up Go 60 | uses: actions/setup-go@v5 61 | with: 62 | go-version: '1.22' 63 | 64 | - name: 📦 Install Go Dependencies 65 | run: go mod tidy 66 | 67 | - name: ✅ Run Go Tests with Coverage 68 | run: go test ./... -v -coverprofile=coverage.out 69 | 70 | - name: 📊 Upload Code Coverage Report 71 | uses: actions/upload-artifact@v4 72 | with: 73 | name: go-code-coverage 74 | path: coverage.out 75 | 76 | - name: 📝 Publish Coverage to Coveralls 77 | uses: shogo82148/actions-goveralls@v1 78 | with: 79 | path-to-profile: coverage.out 80 | github-token: ${{ secrets.GITHUB_TOKEN }} 81 | 82 | scan-vulnerabilities: 83 | name: 🔍 Scan for Vulnerabilities 84 | runs-on: ubuntu-latest 85 | 86 | steps: 87 | - name: 📥 Checkout Code 88 | uses: actions/checkout@v4 89 | 90 | - name: 🐹 Set Up Go 91 | uses: actions/setup-go@v5 92 | with: 93 | go-version: '1.22' 94 | 95 | - name: 📦 Install Go Dependencies 96 | run: go mod tidy 97 | 98 | - name: 🔍 Run Golang Security Scanner 99 | uses: securego/gosec@v2.21.4 100 | with: 101 | args: '-no-fail -fmt sarif -out results.sarif ./...' 102 | 103 | - name: 📝 Upload SARIF Results 104 | uses: github/codeql-action/upload-sarif@v3 105 | with: 106 | sarif_file: results.sarif 107 | 108 | - name: 🌐 Set Up Node.js 109 | uses: actions/setup-node@v4 110 | with: 111 | node-version: '20' 112 | 113 | - name: 📦 Install Vue Dependencies 114 | run: | 115 | cd ui 116 | npm install 117 | 118 | - name: 🔍 Run NPM Audit 119 | run: | 120 | cd ui 121 | npm audit --audit-level=high || true 122 | -------------------------------------------------------------------------------- /.github/workflows/build-and-push-docker.yaml: -------------------------------------------------------------------------------- 1 | name: 🐳 Build and Push Docker Images 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | workflow_dispatch: 11 | 12 | permissions: 13 | contents: read 14 | packages: write 15 | 16 | jobs: 17 | build-backend: 18 | name: 🛠️ Build and Push 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: 🚀 Checkout Code 22 | uses: actions/checkout@v4 23 | 24 | # Get version from version.yaml 25 | - name: 📁 Get version 26 | run: | 27 | echo "VERSION=$(cat version.yaml | sed -n 's/version: //p')" >> $GITHUB_ENV 28 | echo $VERSION 29 | 30 | # Set build tags based on branch 31 | - name: 🏗️ Set Build tags 32 | run: | 33 | if [ $GITHUB_REF = 'refs/heads/main' ]; then 34 | echo "IMAGE_TAGS=$VERSION" >> $GITHUB_ENV 35 | else 36 | echo "IMAGE_TAGS=rc-$VERSION-$GITHUB_RUN_NUMBER" >> $GITHUB_ENV 37 | fi 38 | 39 | - name: 🛠️ Set up Docker Buildx 40 | uses: docker/setup-buildx-action@v2 41 | 42 | # Login to GitHub Container Registry 43 | - name: 🔐 Login to GitHub Container Registry 44 | uses: docker/login-action@v2 45 | with: 46 | registry: ghcr.io 47 | username: ${{ github.actor }} 48 | password: ${{ secrets.TOKEN }} 49 | 50 | # Login to Docker Hub 51 | - name: 🔐 Login to Docker Hub 52 | if: github.ref == 'refs/heads/main' 53 | uses: docker/login-action@v2 54 | with: 55 | username: ${{ secrets.DOCKERHUB_USERNAME }} 56 | password: ${{ secrets.DOCKERHUB_TOKEN }} 57 | 58 | - name: 📦 Build and Push Image to GHCR (PR) 59 | if: github.event_name == 'pull_request' 60 | uses: docker/build-push-action@v4 61 | with: 62 | context: . 63 | file: ./Dockerfile 64 | push: true 65 | tags: ghcr.io/kek-sec/gopherdrop:${{ env.IMAGE_TAGS }} 66 | annotations: | 67 | org.opencontainers.image.description=GopherDrop, a secure one-time secret sharing service 68 | build-args: | 69 | VERSION=${{ env.VERSION }} 70 | labels: | 71 | org.opencontainers.image.source=https://github.com/kek-sec/gopherdrop 72 | cache-from: type=registry,ref=ghcr.io/kek-sec/gopherdrop:latest 73 | cache-to: type=registry,ref=ghcr.io/kek-sec/gopherdrop:latest 74 | 75 | - name: 📦 Build and Push Image to GHCR and Docker Hub (Main) 76 | if: github.ref == 'refs/heads/main' 77 | uses: docker/build-push-action@v4 78 | with: 79 | context: . 80 | file: ./Dockerfile 81 | push: true 82 | tags: | 83 | ghcr.io/kek-sec/gopherdrop:${{ env.IMAGE_TAGS }} 84 | petrakisg/gopherdrop:${{ env.IMAGE_TAGS }} 85 | annotations: | 86 | org.opencontainers.image.description=GopherDrop, a secure one-time secret sharing service 87 | build-args: | 88 | VERSION=${{ env.VERSION }} 89 | labels: | 90 | org.opencontainers.image.source=https://github.com/kek-sec/gopherdrop 91 | cache-from: type=registry,ref=ghcr.io/kek-sec/gopherdrop:latest 92 | cache-to: type=registry,ref=ghcr.io/kek-sec/gopherdrop:latest -------------------------------------------------------------------------------- /ui/src/pages/View.vue: -------------------------------------------------------------------------------- 1 | 39 | 40 | 95 | 96 | -------------------------------------------------------------------------------- /ui/src/services/api.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @module api 3 | * @description Provides functions to interact with the backend API. 4 | */ 5 | 6 | const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080'; 7 | console.log('API_URL:', API_URL); 8 | 9 | /** 10 | * Maps HTTP status codes to error messages for the createSend function. 11 | * @type {Object} 12 | */ 13 | const createSendErrorMessages = { 14 | 413: 'File too large', 15 | 422: 'Invalid form data', 16 | 429: 'Too many requests – please try again later' 17 | }; 18 | 19 | /** 20 | * Creates a new "send" with the provided form data. 21 | * @param {FormData} formData - The data for creating the send. 22 | * @returns {Promise} The server's JSON response. 23 | * @throws {Error} If the request fails. 24 | */ 25 | export async function createSend(formData) { 26 | const res = await fetch(`${API_URL}/send`, { 27 | method: 'POST', 28 | body: formData 29 | }); 30 | 31 | if (!res.ok) { 32 | const errorMessage = createSendErrorMessages[res.status] || 'Failed to create send'; 33 | throw new Error(errorMessage); 34 | } 35 | return res.json(); 36 | } 37 | 38 | /** 39 | * Retrieves a "send" by its hash. 40 | * @param {string} hash - The hash of the send to retrieve. 41 | * @param {string} [password=''] - The password for the send. 42 | * @returns {Promise} An object containing the send data or a notFound flag. 43 | * @throws {Error} If the retrieval fails. 44 | */ 45 | export async function getSend(hash, password = '') { 46 | let url = `${API_URL}/send/${hash}`; 47 | 48 | // Manually append the password as a query parameter if it exists. 49 | if (password) { 50 | const params = new URLSearchParams({ password }); 51 | url += `?${params.toString()}`; 52 | } 53 | 54 | const res = await fetch(url); // Use the resilient URL string. 55 | 56 | if (res.status === 404) { 57 | console.log('Secret not found.'); 58 | return { notFound: true }; 59 | } 60 | 61 | if (!res.ok) { 62 | console.error('Failed to retrieve send:', res.statusText); 63 | throw new Error('Failed to retrieve send'); 64 | } 65 | 66 | const contentDisposition = res.headers.get('Content-Disposition'); 67 | let filename = `download-${hash}`; 68 | if (contentDisposition) { 69 | const matches = contentDisposition.match(/filename="(.+?)"/); 70 | if (matches && matches[1]) { 71 | filename = matches[1]; 72 | } 73 | } 74 | 75 | const contentType = res.headers.get('content-type'); 76 | if (contentType.includes('application/octet-stream')) { 77 | const blob = await res.blob(); 78 | return { file: blob, filename }; 79 | } 80 | 81 | const text = await res.text(); 82 | return { text }; 83 | } 84 | 85 | /** 86 | * Checks if a "send" is password protected. 87 | * @param {string} hash - The hash of the send to check. 88 | * @returns {Promise} An object indicating if a password is required or a notFound flag. 89 | * @throws {Error} If the check fails. 90 | */ 91 | export async function checkPasswordProtection(hash) { 92 | const res = await fetch(`${API_URL}/send/${hash}/check`); 93 | 94 | if (res.status === 404) { 95 | return { notFound: true }; 96 | } 97 | 98 | if (!res.ok) { 99 | throw new Error('Failed to check password protection'); 100 | } 101 | 102 | return res.json(); 103 | } -------------------------------------------------------------------------------- /internal/routes/routes_test.go: -------------------------------------------------------------------------------- 1 | package routes 2 | 3 | import ( 4 | "net/http" 5 | "net/http/httptest" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/jinzhu/gorm" 12 | _ "github.com/jinzhu/gorm/dialects/sqlite" 13 | 14 | "github.com/kek-Sec/gopherdrop/internal/config" 15 | "github.com/kek-Sec/gopherdrop/internal/models" 16 | "github.com/stretchr/testify/assert" 17 | ) 18 | 19 | func setupTestDB() *gorm.DB { 20 | db, err := gorm.Open("sqlite3", ":memory:") 21 | if err != nil { 22 | panic(err) 23 | } 24 | 25 | // Auto-migrate the Send model 26 | if err := db.AutoMigrate(&models.Send{}).Error; err != nil { 27 | panic("Failed to migrate database: " + err.Error()) 28 | } 29 | 30 | // Insert a test record 31 | send := models.Send{ 32 | Hash: "testhash", 33 | Type: "text", 34 | Data: "testdata", 35 | Password: "", 36 | OneTime: false, 37 | ExpiresAt: time.Now().Add(time.Hour), 38 | } 39 | db.Create(&send) 40 | 41 | return db 42 | } 43 | 44 | 45 | func setupTestRouter() *gin.Engine { 46 | cfg := config.Config{ 47 | SecretKey: "supersecretkey", 48 | } 49 | db := setupTestDB() 50 | return SetupRouter(cfg, db) 51 | } 52 | 53 | func TestRoutesExist(t *testing.T) { 54 | router := setupTestRouter() 55 | 56 | tests := []struct { 57 | method string 58 | endpoint string 59 | payload string 60 | status int 61 | }{ 62 | {"POST", "/send", "type=text&data=test", http.StatusOK}, 63 | {"GET", "/send/testhash", "", http.StatusNotFound}, 64 | {"GET", "/send/testhash/check", "", http.StatusNotFound}, 65 | } 66 | 67 | for _, tt := range tests { 68 | t.Run(tt.method+" "+tt.endpoint, func(t *testing.T) { 69 | var req *http.Request 70 | if tt.method == "POST" { 71 | req = httptest.NewRequest(tt.method, tt.endpoint, strings.NewReader(tt.payload)) 72 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 73 | } else { 74 | req = httptest.NewRequest(tt.method, tt.endpoint, nil) 75 | } 76 | 77 | w := httptest.NewRecorder() 78 | router.ServeHTTP(w, req) 79 | assert.NotEqual(t, http.StatusNotFound, w.Code, "Route %s %s should exist", tt.method, tt.endpoint) 80 | }) 81 | } 82 | } 83 | 84 | 85 | func TestCORSHeaders(t *testing.T) { 86 | router := setupTestRouter() 87 | 88 | req, _ := http.NewRequest("OPTIONS", "/send", nil) 89 | req.Header.Set("Origin", "*") 90 | w := httptest.NewRecorder() 91 | 92 | router.ServeHTTP(w, req) 93 | 94 | assert.Equal(t, http.StatusNoContent, w.Code, "CORS preflight should return 204 No Content") 95 | assert.Equal(t, "*", w.Header().Get("Access-Control-Allow-Origin")) 96 | assert.Contains(t, w.Header().Get("Access-Control-Allow-Methods"), "POST") 97 | assert.Contains(t, w.Header().Get("Access-Control-Allow-Headers"), "Content-Type") 98 | } 99 | 100 | func TestRateLimiter(t *testing.T) { 101 | router := setupTestRouter() 102 | 103 | // Define a payload for the POST request 104 | payload := "type=text&data=test" 105 | 106 | // Simulate 5 requests (the burst capacity) in quick succession with slight delays 107 | for i := 0; i < 5; i++ { 108 | req := httptest.NewRequest("POST", "/send", strings.NewReader(payload)) 109 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 110 | 111 | w := httptest.NewRecorder() 112 | router.ServeHTTP(w, req) 113 | 114 | assert.Equal(t, http.StatusOK, w.Code, "Request %d should succeed within the burst capacity", i+1) 115 | 116 | // Introduce a small delay (e.g., 10 milliseconds) between requests 117 | time.Sleep(10 * time.Millisecond) 118 | } 119 | 120 | // The 6th request should be rate limited and return a 429 status 121 | req := httptest.NewRequest("POST", "/send", strings.NewReader(payload)) 122 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 123 | 124 | w := httptest.NewRecorder() 125 | router.ServeHTTP(w, req) 126 | 127 | assert.Equal(t, http.StatusTooManyRequests, w.Code, "6th request should be rate limited") 128 | } 129 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [1.0.9] 4 | 5 | ### Changed 6 | - Improved the "Type" selector on the Create Secret page to use a visually appealing slider toggle with icons for "Text" and "File". 7 | - After creating a secret, all input fields are now cleared for a better user experience. 8 | - Enhanced file upload validation to reliably detect selected files and prevent false "Please select a file" errors. 9 | 10 | ### Fixed 11 | - Fixed an issue where file uploads would sometimes incorrectly show "Please select a file" even when a file was chosen. 12 | 13 | ### UI/UX 14 | - The form now resets when navigating to the Create page via the logo or Create+ button. 15 | 16 | ## [1.0.8] 17 | ### UI Enhancements 18 | - **Redesigned Header and Footer**: Modernized header/footer with theme toggle and improved visual consistency. 19 | - **Improved 404 Page**: Updated design for better responsiveness and refined typography. 20 | 21 | ### Theme and Styling Updates 22 | - **Custom Themes**: Introduced custom light/dark themes and theme switching functionality. 23 | - **Global Styling Updates**: Updated font to "Inter" and added a gradient background. 24 | 25 | ### Component Enhancements 26 | - **Reusable Password Input**: Created a component for password handling, including visibility and generation. 27 | - **Secret Display Component**: Added a component for secret copying and file downloading with alerts. 28 | 29 | ### Code Cleanup and Refactoring 30 | - **Removed Unused API Code**: Deleted obsolete `api.js` to simplify codebase. 31 | - **Form Reset Logic**: Refactored `Create.vue` to use a centralized store for form reset. 32 | 33 | ## [1.0.7] 34 | ### Added 35 | - Customizable application title and description through environment variables 36 | - New build arguments in Dockerfile: VITE_APP_TITLE and VITE_APP_DESCRIPTION 37 | - Support for title and description customization in docker-compose configuration 38 | 39 | ### [1.0.6] 40 | * `cmd/server/main.go`: Added rate limiting to POST requests. 41 | * `ui/src/pages/Create.vue`: Added error handling for 429 responses. 42 | 43 | ### [1.0.5] 44 | UI Enhancements: 45 | 46 | * `ui/index.html`: Added a link to the Animate.css library for animation effects. 47 | * [`ui/src/App.vue`: Integrated animation classes into various elements, including the header logo, buttons, and alerts. 48 | 49 | Password Management Enhancements: 50 | * `ui/src/pages/Create.vue`: Added functionality to toggle password visibility and generate random passwords, along with corresponding tooltips and animations. 51 | 404 Error Page Improvements: 52 | * `ui/src/pages/Error404.vue`: Redesigned the 404 error page with a more user-friendly card layout, including an icon, message, and a button to navigate back home. 53 | 54 | Other Enhancements: 55 | * `ui/src/pages/Create.vue`, `ui/src/pages/View.vue`: Applied animation classes to various elements to provide a more dynamic user experience. 56 | 57 | ### [1.0.4] 58 | - Reworked CD pipelines to follow semver tagging 59 | - added version.yaml 60 | 61 | ### [1.0.1] 62 | 63 | #### Added 64 | - Initial unified Dockerfile combining backend and frontend. 65 | - Support for Traefik reverse proxy configuration. 66 | - Environment variables for configuring API URLs dynamically. 67 | - Ability to copy the generated shareable link with improved formatting. 68 | - New CORS configuration to support wildcard origins in development mode. 69 | - Automatic HTTPS redirection using Traefik middlewares. 70 | 71 | #### Fixed 72 | - Corrected Vite's `VITE_API_URL` handling to avoid hardcoded URLs. 73 | - Resolved 404 errors for static assets when accessed via Traefik. 74 | - Fixed MIME type issues for serving CSS files behind Traefik. 75 | 76 | --- 77 | 78 | ### [1.0.0] - 2024-12-18 79 | 80 | #### Added 81 | - Secure one-time secret sharing with encrypted text and file support. 82 | - Password protection for shared secrets. 83 | - One-time retrieval mechanism to ensure secrets are accessed only once. 84 | - Expiration settings for shared secrets. 85 | - Responsive UI built with Vue.js and Vuetify. 86 | - Dockerized deployment with `docker-compose.yml` for easy setup. 87 | - Nginx reverse proxy configuration for serving the frontend and API. 88 | - Health checks for PostgreSQL in `docker-compose.yml`. 89 | 90 | #### Changed 91 | - Updated Docker images to use multi-stage builds for backend and frontend. 92 | - Improved project documentation and added sections for installation, configuration, and deployment. 93 | 94 | --- -------------------------------------------------------------------------------- /internal/handlers/handlers_test.go: -------------------------------------------------------------------------------- 1 | package handlers 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "mime/multipart" 7 | "net/http" 8 | "net/http/httptest" 9 | "testing" 10 | "time" 11 | 12 | "github.com/gin-gonic/gin" 13 | "github.com/jinzhu/gorm" 14 | _ "github.com/jinzhu/gorm/dialects/sqlite" 15 | 16 | "github.com/kek-Sec/gopherdrop/internal/config" 17 | "github.com/kek-Sec/gopherdrop/internal/models" 18 | ) 19 | 20 | func setupTestDB() *gorm.DB { 21 | db, err := gorm.Open("sqlite3", ":memory:") 22 | if err != nil { 23 | panic(err) 24 | } 25 | db.AutoMigrate(&models.Send{}) 26 | return db 27 | } 28 | 29 | func setupTestRouter(cfg config.Config, db *gorm.DB) *gin.Engine { 30 | r := gin.Default() 31 | r.POST("/send", CreateSend(cfg, db)) 32 | r.GET("/send/:id", GetSend(cfg, db)) 33 | r.GET("/send/:id/check", CheckPasswordProtection(db)) 34 | return r 35 | } 36 | 37 | func createMultipartRequest(fieldName, content string) (*bytes.Buffer, string) { 38 | body := &bytes.Buffer{} 39 | writer := multipart.NewWriter(body) 40 | 41 | // Create form field 42 | _ = writer.WriteField("type", "text") 43 | _ = writer.WriteField(fieldName, content) 44 | 45 | // Close writer to finalize boundary 46 | writer.Close() 47 | return body, writer.FormDataContentType() 48 | } 49 | 50 | func createMultipartFileRequest(fieldName, filename, content string) (*bytes.Buffer, string) { 51 | body := &bytes.Buffer{} 52 | writer := multipart.NewWriter(body) 53 | 54 | // Add form field 55 | _ = writer.WriteField("type", "file") 56 | 57 | // Add file field 58 | part, _ := writer.CreateFormFile(fieldName, filename) 59 | io.WriteString(part, content) 60 | 61 | writer.Close() 62 | return body, writer.FormDataContentType() 63 | } 64 | 65 | func TestCheckPasswordProtection(t *testing.T) { 66 | db := setupTestDB() 67 | cfg := config.Config{ 68 | SecretKey: "supersecretkeysupersecretkey32", 69 | MaxFileSize: 1024 * 1024, // 1MB 70 | } 71 | r := setupTestRouter(cfg, db) 72 | 73 | // Create a send with a password 74 | sendWithPassword := models.Send{ 75 | Hash: "protectedhash", 76 | Type: "text", 77 | Data: "encryptedDataHere", 78 | Password: "password123", 79 | ExpiresAt: time.Now().Add(24 * time.Hour), 80 | } 81 | db.Create(&sendWithPassword) 82 | 83 | // Create a send without a password 84 | sendWithoutPassword := models.Send{ 85 | Hash: "unprotectedhash", 86 | Type: "text", 87 | Data: "encryptedDataHere", 88 | Password: "", 89 | ExpiresAt: time.Now().Add(24 * time.Hour), 90 | } 91 | db.Create(&sendWithoutPassword) 92 | 93 | // Test for send with password 94 | w := httptest.NewRecorder() 95 | req, _ := http.NewRequest("GET", "/send/protectedhash/check", nil) 96 | r.ServeHTTP(w, req) 97 | 98 | if w.Code != http.StatusOK { 99 | t.Fatalf("expected 200 got %d", w.Code) 100 | } 101 | 102 | expectedBody := `{"requiresPassword":true}` 103 | if w.Body.String() != expectedBody { 104 | t.Fatalf("expected body %s got %s", expectedBody, w.Body.String()) 105 | } 106 | 107 | // Test for send without password 108 | w = httptest.NewRecorder() 109 | req, _ = http.NewRequest("GET", "/send/unprotectedhash/check", nil) 110 | r.ServeHTTP(w, req) 111 | 112 | if w.Code != http.StatusOK { 113 | t.Fatalf("expected 200 got %d", w.Code) 114 | } 115 | 116 | expectedBody = `{"requiresPassword":false}` 117 | if w.Body.String() != expectedBody { 118 | t.Fatalf("expected body %s got %s", expectedBody, w.Body.String()) 119 | } 120 | } 121 | 122 | func TestCreateSendText(t *testing.T) { 123 | db := setupTestDB() 124 | cfg := config.Config{ 125 | SecretKey: "supersecretkeysupersecretkey32", 126 | MaxFileSize: 1024 * 1024, // 1MB 127 | } 128 | r := setupTestRouter(cfg, db) 129 | 130 | body, contentType := createMultipartRequest("data", "This is a test message.") 131 | 132 | w := httptest.NewRecorder() 133 | req, _ := http.NewRequest("POST", "/send", body) 134 | req.Header.Set("Content-Type", contentType) 135 | r.ServeHTTP(w, req) 136 | 137 | if w.Code != http.StatusOK { 138 | t.Fatalf("expected 200 got %d", w.Code) 139 | } 140 | } 141 | 142 | func TestCreateSendFileTooLarge(t *testing.T) { 143 | db := setupTestDB() 144 | cfg := config.Config{ 145 | SecretKey: "supersecretkeysupersecretkey32", 146 | MaxFileSize: 10, // Only allow 10 bytes 147 | } 148 | r := setupTestRouter(cfg, db) 149 | 150 | // Create a file with more than 10 bytes 151 | body, contentType := createMultipartFileRequest("file", "test.txt", "This file is too large.") 152 | 153 | w := httptest.NewRecorder() 154 | req, _ := http.NewRequest("POST", "/send", body) 155 | req.Header.Set("Content-Type", contentType) 156 | r.ServeHTTP(w, req) 157 | 158 | if w.Code != http.StatusRequestEntityTooLarge { 159 | t.Fatalf("expected 413 got %d", w.Code) 160 | } 161 | } 162 | 163 | func TestGetNonExistentSend(t *testing.T) { 164 | db := setupTestDB() 165 | cfg := config.Config{ 166 | SecretKey: "supersecretkeysupersecretkey32", 167 | MaxFileSize: 1024 * 1024, 168 | } 169 | r := setupTestRouter(cfg, db) 170 | 171 | w := httptest.NewRecorder() 172 | req, _ := http.NewRequest("GET", "/send/unknownhash", nil) 173 | r.ServeHTTP(w, req) 174 | 175 | if w.Code != http.StatusNotFound { 176 | t.Fatalf("expected 404 got %d", w.Code) 177 | } 178 | } 179 | 180 | func TestExpiredSend(t *testing.T) { 181 | db := setupTestDB() 182 | cfg := config.Config{ 183 | SecretKey: "supersecretkeysupersecretkey32", 184 | MaxFileSize: 1024 * 1024, 185 | } 186 | r := setupTestRouter(cfg, db) 187 | 188 | // Create expired send 189 | send := models.Send{ 190 | Hash: "expiredhash", 191 | Type: "text", 192 | Data: "encryptedDataHere", 193 | ExpiresAt: time.Now().Add(-1 * time.Hour), 194 | } 195 | db.Create(&send) 196 | 197 | w := httptest.NewRecorder() 198 | req, _ := http.NewRequest("GET", "/send/expiredhash", nil) 199 | r.ServeHTTP(w, req) 200 | 201 | if w.Code != http.StatusNotFound { 202 | t.Fatalf("expected 404 got %d", w.Code) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # 🛠️ **GopherDrop** – Secure One-Time Secret Sharing 🏁 2 | 3 | ![Docker Image Version](https://img.shields.io/docker/v/petrakisg/gopherdrop?sort=semver&label=Docker%20Image%20Version&logo=docker) 4 | ![Docker Pulls](https://img.shields.io/docker/pulls/petrakisg/gopherdrop) 5 | ![GitHub branch check runs](https://img.shields.io/github/check-runs/kek-Sec/GopherDrop/main) 6 | ![Coveralls](https://img.shields.io/coverallsCoverage/github/kek-Sec/GopherDrop) 7 | ![GitHub go.mod Go version](https://img.shields.io/github/go-mod/go-version/kek-Sec/GopherDrop) 8 | 9 | 10 | 11 | ### Demo: [https://gopherdrop.yup.gr](http://gopherdrop.yup.gr) 12 | 13 | GopherDrop is a secure, self-hostable REST API and UI for sharing encrypted one-time secrets and files, inspired by Bitwarden's Send feature. Built with **Go**, **Vue.js**, and **Vuetify**, GopherDrop is designed for simplicity, security, and ease of deployment. 14 | 15 | ![GopherDrop Banner](ui/src/assets/Images/banner.png) 16 | 17 | --- 18 | 19 | ## 📋 **Table of Contents** 20 | 21 | 1. [Features](#-features) 22 | 2. [Installation](#-installation) 23 | 3. [Build and Run](#-build-and-run) 24 | 4. [Configuration](#-configuration) 25 | 5. [Endpoints](#-endpoints) 26 | 6. [Docker Deployment](#-docker-deployment) 27 | 7. [Contributing](#-contributing) 28 | 8. [License](#-license) 29 | 9. [Community and Support](#-community-and-support) 30 | 31 | --- 32 | 33 | ## 🌟 **Features** 34 | 35 | - **Send Text or Files**: Share sensitive information securely. 36 | - **Password Protection**: Encrypt your secrets with a password. 37 | - **One-Time Retrieval**: Automatically delete secrets after a single access. 38 | - **Expiration Settings**: Define how long a secret remains available. 39 | - **Responsive UI**: Built with Vue.js and Vuetify for a modern user experience. 40 | - **Dockerized Deployment**: Simple setup with Docker and Docker Compose. 41 | - **Production and Debug Modes**: Easily switch between production and debug builds. 42 | 43 | 44 | --- 45 | 46 | ## 🐳 **Docker Deployment** 47 | 48 | ### **Production `docker-compose.yml`** 49 | 50 | > docker-compose.prod.sample.yaml 51 | 52 | --- 53 | 54 | ## 📥 **Installation** 55 | 56 | ### **Prerequisites** 57 | 58 | - **Docker**: [Install Docker](https://docs.docker.com/get-docker/) 59 | - **Docker Compose**: [Install Docker Compose](https://docs.docker.com/compose/install/) 60 | 61 | ### **Clone the Repository** 62 | 63 | ```bash 64 | git clone https://github.com/kek-Sec/gopherdrop.git 65 | cd gopherdrop 66 | ``` 67 | --- 68 | 69 | ## 🛠️ **Build and Run** 70 | 71 | ### **Local Setup** 72 | 73 | To build and run GopherDrop in production mode: 74 | 75 | ```bash 76 | make build # Build the Docker images 77 | make up # Start the backend, frontend, and database services 78 | ``` 79 | 80 | ### **Debug Setup** 81 | 82 | To build and run GopherDrop in debug mode: 83 | 84 | ```bash 85 | make build-debug # Build the Docker images with debug mode enabled 86 | make up # Start the backend, frontend, and database services in debug mode 87 | ``` 88 | 89 | ### **Stopping Services** 90 | 91 | ```bash 92 | make down 93 | ``` 94 | 95 | ### **Running Tests** 96 | 97 | ```bash 98 | make test 99 | ``` 100 | 101 | ## ⚙️ **Configuration** 102 | 103 | ### **Using `.env` File** 104 | 105 | Create a `.env` file in the project root to securely store your secrets: 106 | 107 | ```env 108 | DB_HOST=db 109 | DB_USER=user 110 | DB_PASSWORD=pass 111 | DB_NAME=gopherdropdb 112 | DB_SSLMODE=disable 113 | SECRET_KEY=supersecretkeysupersecretkey32 114 | LISTEN_ADDR=:8080 115 | STORAGE_PATH=/app/storage 116 | MAX_FILE_SIZE=10485760 117 | ``` 118 | 119 | ### **Environment Variables** 120 | 121 | | Variable | Description | Default Value | 122 | |------------------|---------------------------------|--------------------------------------| 123 | | `DB_HOST` | Database host | `db` | 124 | | `DB_USER` | Database username | `user` | 125 | | `DB_PASSWORD` | Database password | `pass` | 126 | | `DB_NAME` | Database name | `gopherdropdb` | 127 | | `SECRET_KEY` | Secret key for encryption | `supersecretkeysupersecretkey32` | 128 | | `LISTEN_ADDR` | API listen address | `:8080` | 129 | | `STORAGE_PATH` | Path for storing uploaded files | `/app/storage` | 130 | | `MAX_FILE_SIZE` | Maximum file size in bytes | `10485760` (10 MB) | 131 | 132 | ### **Build Arguments** 133 | 134 | | Argument | Description | Default Value | 135 | |----------------------|--------------------------------------|----------------------------------------------| 136 | | `VITE_API_URL` | API endpoint URL | `/api` | 137 | | `VITE_APP_TITLE` | Custom application title | `GopherDrop` | 138 | | `VITE_APP_DESCRIPTION` | Custom application description | `Secure one-time secret and file sharing` | 139 | | `DEBUG` | Enable debug mode | `false` | 140 | | `GIN_MODE` | Gin framework mode | `release` | 141 | | `VERSION` | Application version | `-` | 142 | 143 | --- 144 | 145 | ## 🖥️ **Endpoints** 146 | 147 | ### **API Endpoints** 148 | 149 | | Method | Endpoint | Description | 150 | |--------|--------------------|------------------------------------------| 151 | | `POST` | `/send` | Create a new send (text or file) | 152 | | `GET` | `/send/:id` | Retrieve a send by its hash | 153 | | `GET` | `/send/:id/check` | Check if a send requires a password | 154 | 155 | 156 | --- 157 | 158 | ## 🤝 **Contributing** 159 | 160 | 1. Fork the repository. 161 | 2. Create a new branch: `git checkout -b my-feature-branch` 162 | 3. Make your changes and add tests. 163 | 4. Submit a pull request. 164 | 165 | --- 166 | 167 | ## 📝 **License** 168 | 169 | GopherDrop is licensed under the [MIT License](LICENSE). 170 | 171 | --- 172 | 173 | ## 💬 **Community and Support** 174 | 175 | - **Issues**: [GitHub Issues](https://github.com/kek-Sec/gopherdrop/issues) 176 | - **Discussions**: [GitHub Discussions](https://github.com/kek-Sec/gopherdrop/discussions) 177 | -------------------------------------------------------------------------------- /ui/src/pages/Create.vue: -------------------------------------------------------------------------------- 1 | 93 | 94 | 198 | 199 | -------------------------------------------------------------------------------- /internal/handlers/handlers.go: -------------------------------------------------------------------------------- 1 | // Package handlers contains logic for creating and retrieving sends. 2 | package handlers 3 | 4 | import ( 5 | "fmt" 6 | "io" 7 | "log" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | "time" 12 | 13 | "github.com/gin-gonic/gin" 14 | "github.com/jinzhu/gorm" 15 | 16 | "github.com/kek-Sec/gopherdrop/internal/config" 17 | "github.com/kek-Sec/gopherdrop/internal/models" 18 | "github.com/kek-Sec/gopherdrop/internal/security" 19 | ) 20 | 21 | // CreateSend handles creation of a new send. 22 | // It accepts form data for type (text/file), optional password, one-time use, and expiration. 23 | func CreateSend(cfg config.Config, db *gorm.DB) gin.HandlerFunc { 24 | return func(c *gin.Context) { 25 | // Explicitly parse the multipart form before accessing form values. 26 | // This is crucial for handling mixed file/text forms reliably in Gin. 27 | if err := c.Request.ParseMultipartForm(cfg.MaxFileSize + 1024*1024); err != nil { // Add buffer to max size 28 | log.Println("Error parsing multipart form:", err) 29 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid form data or file too large"}) 30 | return 31 | } 32 | 33 | // Use c.Request.FormValue now that the form is parsed. 34 | stype := c.Request.FormValue("type") 35 | pw := c.Request.FormValue("password") 36 | ot := c.Request.FormValue("onetime") 37 | exp := c.Request.FormValue("expires") 38 | 39 | log.Println("CreateSend called with type:", stype) 40 | 41 | if stype == "" { 42 | log.Println("Error: 'type' field is missing") 43 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Type field is required"}) 44 | return 45 | } 46 | 47 | oneTime := (ot == "true") 48 | log.Println("One-Time:", oneTime) 49 | 50 | var expiresAt time.Time 51 | if exp != "" { 52 | d, err := time.ParseDuration(exp) 53 | if err != nil { 54 | log.Println("Error parsing expiration duration:", err) 55 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Invalid expiration duration"}) 56 | return 57 | } 58 | expiresAt = time.Now().Add(d) 59 | } else { 60 | expiresAt = time.Now().Add(24 * time.Hour) 61 | } 62 | log.Println("Expires At:", expiresAt) 63 | 64 | hash, err := security.GenerateHash(16) 65 | if err != nil { 66 | log.Println("Error generating hash:", err) 67 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate hash"}) 68 | return 69 | } 70 | log.Println("Generated Hash:", hash) 71 | 72 | key := deriveKey(pw, cfg) 73 | 74 | if stype == "text" { 75 | text := c.Request.FormValue("data") 76 | if text == "" { 77 | log.Println("Error: 'data' field is empty for text type") 78 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Data field is required for text type"}) 79 | return 80 | } 81 | 82 | enc, err := security.EncryptData([]byte(text), key) 83 | if err != nil { 84 | log.Println("Error encrypting text data:", err) 85 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Failed to encrypt text data"}) 86 | return 87 | } 88 | 89 | s := models.Send{ 90 | Hash: hash, 91 | Type: "text", 92 | Data: enc, 93 | Password: pw, 94 | OneTime: oneTime, 95 | ExpiresAt: expiresAt, 96 | } 97 | db.Create(&s) 98 | log.Println("Text send created successfully with hash:", hash) 99 | c.JSON(http.StatusOK, gin.H{"hash": s.Hash}) 100 | return 101 | } 102 | 103 | if stype == "file" { 104 | // Use c.Request.FormFile now that the form is parsed. 105 | file, header, err := c.Request.FormFile("file") 106 | if err != nil { 107 | log.Println("Error retrieving file from form data:", err) 108 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Failed to retrieve file from form data"}) 109 | return 110 | } 111 | defer file.Close() 112 | 113 | log.Println("Received file:", header.Filename, "Size:", header.Size) 114 | 115 | if header.Size > cfg.MaxFileSize { 116 | log.Printf("Error: File size (%d bytes) exceeds maximum allowed size (%d bytes)\n", header.Size, cfg.MaxFileSize) 117 | c.AbortWithStatusJSON(http.StatusRequestEntityTooLarge, gin.H{"error": "File size exceeds the maximum allowed limit"}) 118 | return 119 | } 120 | 121 | data, err := io.ReadAll(file) 122 | if err != nil { 123 | log.Println("Error reading file data:", err) 124 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Failed to read file data"}) 125 | return 126 | } 127 | 128 | enc, err := security.EncryptData(data, key) 129 | if err != nil { 130 | log.Println("Error encrypting file data:", err) 131 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Failed to encrypt file data"}) 132 | return 133 | } 134 | 135 | fp := filepath.Join(cfg.StoragePath, hash) 136 | if err := os.WriteFile(fp, []byte(enc), 0600); err != nil { 137 | log.Println("Error writing encrypted file to storage:", err) 138 | c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "Failed to write encrypted file to storage"}) 139 | return 140 | } 141 | 142 | log.Println("File saved successfully to:", fp) 143 | 144 | s := models.Send{ 145 | Hash: hash, 146 | Type: "file", 147 | FilePath: fp, 148 | FileName: header.Filename, 149 | Password: pw, 150 | OneTime: oneTime, 151 | ExpiresAt: expiresAt, 152 | } 153 | db.Create(&s) 154 | log.Println("File send created successfully with hash:", hash) 155 | c.JSON(http.StatusOK, gin.H{"hash": s.Hash}) 156 | return 157 | } 158 | 159 | log.Println("Error: Unsupported send type:", stype) 160 | c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": "Unsupported send type"}) 161 | } 162 | } 163 | 164 | // GetSend handles retrieving and decrypting a send by its hash. 165 | func GetSend(cfg config.Config, db *gorm.DB) gin.HandlerFunc { 166 | return func(c *gin.Context) { 167 | hash := c.Param("id") 168 | var s models.Send 169 | 170 | if db.First(&s, "hash = ?", hash).RecordNotFound() { 171 | c.AbortWithStatus(http.StatusNotFound) 172 | return 173 | } 174 | 175 | if time.Now().After(s.ExpiresAt) { 176 | deleteSendAndFile(db, &s) 177 | c.AbortWithStatus(http.StatusNotFound) 178 | return 179 | } 180 | 181 | pw := c.Query("password") 182 | if s.Password != "" && s.Password != pw { 183 | c.AbortWithStatus(http.StatusForbidden) 184 | return 185 | } 186 | 187 | key := deriveKey(pw, cfg) 188 | 189 | if s.Type == "text" { 190 | d, err := security.DecryptData(s.Data, key) 191 | if err != nil { 192 | c.AbortWithStatus(http.StatusInternalServerError) 193 | return 194 | } 195 | c.String(http.StatusOK, string(d)) 196 | } else { 197 | d, err := os.ReadFile(s.FilePath) 198 | if err != nil { 199 | c.AbortWithStatus(http.StatusInternalServerError) 200 | return 201 | } 202 | dec, err := security.DecryptData(string(d), key) 203 | if err != nil { 204 | c.AbortWithStatus(http.StatusInternalServerError) 205 | return 206 | } 207 | c.Header("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, s.FileName)) 208 | c.Data(http.StatusOK, "application/octet-stream", dec) 209 | } 210 | 211 | if s.OneTime { 212 | deleteSendAndFile(db, &s) 213 | } 214 | } 215 | } 216 | 217 | func deriveKey(pw string, cfg config.Config) []byte { 218 | if pw != "" { 219 | return []byte(security.PadKey(pw)) 220 | } 221 | return []byte(security.PadKey(cfg.SecretKey)) 222 | } 223 | 224 | func deleteSendAndFile(db *gorm.DB, s *models.Send) { 225 | if s.Type == "file" && s.FilePath != "" { 226 | os.Remove(s.FilePath) 227 | } 228 | db.Delete(&s) 229 | } 230 | 231 | // CheckPasswordProtection checks if a send requires a password. 232 | func CheckPasswordProtection(db *gorm.DB) gin.HandlerFunc { 233 | return func(c *gin.Context) { 234 | hash := c.Param("id") 235 | var s models.Send 236 | 237 | if db.First(&s, "hash = ?", hash).RecordNotFound() { 238 | c.AbortWithStatus(http.StatusNotFound) 239 | return 240 | } 241 | 242 | if time.Now().After(s.ExpiresAt) { 243 | deleteSendAndFile(db, &s) 244 | c.AbortWithStatus(http.StatusNotFound) 245 | return 246 | } 247 | 248 | // Return whether the send requires a password 249 | c.JSON(http.StatusOK, gin.H{"requiresPassword": s.Password != ""}) 250 | } 251 | } -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= 2 | github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= 3 | github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= 4 | github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= 5 | github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= 6 | github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= 7 | github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= 8 | github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= 9 | github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= 10 | github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= 11 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 12 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 13 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 14 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 15 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd h1:83Wprp6ROGeiHFAP8WJdI2RoxALQYgdllERc3N5N2DM= 16 | github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= 17 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5 h1:Yzb9+7DPaBjB8zlTR87/ElzFsnQfuHnVUVqpZZIcV5Y= 18 | github.com/erikstmartin/go-testdb v0.0.0-20160219214506-8d10e4a1bae5/go.mod h1:a2zkGnVExMxdzMo3M0Hi/3sEU+cWnZpSni0O6/Yb/P0= 19 | github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= 20 | github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= 21 | github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw= 22 | github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E= 23 | github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 24 | github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= 25 | github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= 26 | github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= 27 | github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= 28 | github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 29 | github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= 30 | github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 31 | github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= 32 | github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 33 | github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= 34 | github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= 35 | github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs= 36 | github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= 37 | github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= 38 | github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= 39 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe h1:lXe2qZdvpiX5WZkZR4hgp4KJVfY3nMkvmwbVkpv1rVY= 40 | github.com/golang-sql/civil v0.0.0-20190719163853-cb61b32ac6fe/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 41 | github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= 42 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 43 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 44 | github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= 45 | github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= 46 | github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 47 | github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= 48 | github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= 49 | github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 50 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 51 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 52 | github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 53 | github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= 54 | github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= 55 | github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= 56 | github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= 57 | github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= 58 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 59 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 60 | github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= 61 | github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 62 | github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= 63 | github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= 64 | github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= 65 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 66 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 67 | github.com/mattn/go-sqlite3 v1.14.0 h1:mLyGNKR8+Vv9CAU7PphKa2hkEqxxhn8i32J6FPj1/QA= 68 | github.com/mattn/go-sqlite3 v1.14.0/go.mod h1:JIl7NbARA7phWnGvh0LKTyg7S9BA+6gx71ShQilpsus= 69 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 70 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 71 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 72 | github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= 73 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 74 | github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= 75 | github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= 76 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 77 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 78 | github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= 79 | github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= 80 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 81 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 82 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 83 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 84 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 85 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 86 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 87 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 88 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 89 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 90 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 91 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 92 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 93 | github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= 94 | github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 95 | github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= 96 | github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 97 | golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= 98 | golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= 99 | golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= 100 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 101 | golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 102 | golang.org/x/crypto v0.0.0-20191205180655-e7c4368fe9dd/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 103 | golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= 104 | golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= 105 | golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 106 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 107 | golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 108 | golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 109 | golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= 110 | golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= 111 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 112 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 113 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 114 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 115 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 116 | golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= 117 | golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 118 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 119 | golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= 120 | golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= 121 | golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= 122 | golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 123 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 124 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 125 | google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= 126 | google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 127 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 128 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 129 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 130 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 131 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 132 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 133 | nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= 134 | rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= 135 | -------------------------------------------------------------------------------- /ui/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gopherdrop-ui", 3 | "version": "1.0.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "gopherdrop-ui", 9 | "version": "1.0.1", 10 | "dependencies": { 11 | "@mdi/font": "^7.4.47", 12 | "vite-plugin-vuetify": "^1.0.1", 13 | "vue": "^3.3.4", 14 | "vue-router": "^4.2.5", 15 | "vuetify": "^3.7.5" 16 | }, 17 | "devDependencies": { 18 | "@vitejs/plugin-vue": "^4.0.0", 19 | "vite": "^4.5.9" 20 | } 21 | }, 22 | "node_modules/@babel/helper-string-parser": { 23 | "version": "7.25.9", 24 | "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", 25 | "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", 26 | "engines": { 27 | "node": ">=6.9.0" 28 | } 29 | }, 30 | "node_modules/@babel/helper-validator-identifier": { 31 | "version": "7.25.9", 32 | "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", 33 | "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", 34 | "engines": { 35 | "node": ">=6.9.0" 36 | } 37 | }, 38 | "node_modules/@babel/parser": { 39 | "version": "7.26.3", 40 | "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", 41 | "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", 42 | "dependencies": { 43 | "@babel/types": "^7.26.3" 44 | }, 45 | "bin": { 46 | "parser": "bin/babel-parser.js" 47 | }, 48 | "engines": { 49 | "node": ">=6.0.0" 50 | } 51 | }, 52 | "node_modules/@babel/types": { 53 | "version": "7.26.3", 54 | "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", 55 | "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", 56 | "dependencies": { 57 | "@babel/helper-string-parser": "^7.25.9", 58 | "@babel/helper-validator-identifier": "^7.25.9" 59 | }, 60 | "engines": { 61 | "node": ">=6.9.0" 62 | } 63 | }, 64 | "node_modules/@esbuild/android-arm": { 65 | "version": "0.18.20", 66 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz", 67 | "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", 68 | "cpu": [ 69 | "arm" 70 | ], 71 | "optional": true, 72 | "os": [ 73 | "android" 74 | ], 75 | "engines": { 76 | "node": ">=12" 77 | } 78 | }, 79 | "node_modules/@esbuild/android-arm64": { 80 | "version": "0.18.20", 81 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", 82 | "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", 83 | "cpu": [ 84 | "arm64" 85 | ], 86 | "optional": true, 87 | "os": [ 88 | "android" 89 | ], 90 | "engines": { 91 | "node": ">=12" 92 | } 93 | }, 94 | "node_modules/@esbuild/android-x64": { 95 | "version": "0.18.20", 96 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz", 97 | "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", 98 | "cpu": [ 99 | "x64" 100 | ], 101 | "optional": true, 102 | "os": [ 103 | "android" 104 | ], 105 | "engines": { 106 | "node": ">=12" 107 | } 108 | }, 109 | "node_modules/@esbuild/darwin-arm64": { 110 | "version": "0.18.20", 111 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", 112 | "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", 113 | "cpu": [ 114 | "arm64" 115 | ], 116 | "optional": true, 117 | "os": [ 118 | "darwin" 119 | ], 120 | "engines": { 121 | "node": ">=12" 122 | } 123 | }, 124 | "node_modules/@esbuild/darwin-x64": { 125 | "version": "0.18.20", 126 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", 127 | "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", 128 | "cpu": [ 129 | "x64" 130 | ], 131 | "optional": true, 132 | "os": [ 133 | "darwin" 134 | ], 135 | "engines": { 136 | "node": ">=12" 137 | } 138 | }, 139 | "node_modules/@esbuild/freebsd-arm64": { 140 | "version": "0.18.20", 141 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", 142 | "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", 143 | "cpu": [ 144 | "arm64" 145 | ], 146 | "optional": true, 147 | "os": [ 148 | "freebsd" 149 | ], 150 | "engines": { 151 | "node": ">=12" 152 | } 153 | }, 154 | "node_modules/@esbuild/freebsd-x64": { 155 | "version": "0.18.20", 156 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", 157 | "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", 158 | "cpu": [ 159 | "x64" 160 | ], 161 | "optional": true, 162 | "os": [ 163 | "freebsd" 164 | ], 165 | "engines": { 166 | "node": ">=12" 167 | } 168 | }, 169 | "node_modules/@esbuild/linux-arm": { 170 | "version": "0.18.20", 171 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", 172 | "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", 173 | "cpu": [ 174 | "arm" 175 | ], 176 | "optional": true, 177 | "os": [ 178 | "linux" 179 | ], 180 | "engines": { 181 | "node": ">=12" 182 | } 183 | }, 184 | "node_modules/@esbuild/linux-arm64": { 185 | "version": "0.18.20", 186 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", 187 | "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", 188 | "cpu": [ 189 | "arm64" 190 | ], 191 | "optional": true, 192 | "os": [ 193 | "linux" 194 | ], 195 | "engines": { 196 | "node": ">=12" 197 | } 198 | }, 199 | "node_modules/@esbuild/linux-ia32": { 200 | "version": "0.18.20", 201 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", 202 | "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", 203 | "cpu": [ 204 | "ia32" 205 | ], 206 | "optional": true, 207 | "os": [ 208 | "linux" 209 | ], 210 | "engines": { 211 | "node": ">=12" 212 | } 213 | }, 214 | "node_modules/@esbuild/linux-loong64": { 215 | "version": "0.18.20", 216 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", 217 | "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", 218 | "cpu": [ 219 | "loong64" 220 | ], 221 | "optional": true, 222 | "os": [ 223 | "linux" 224 | ], 225 | "engines": { 226 | "node": ">=12" 227 | } 228 | }, 229 | "node_modules/@esbuild/linux-mips64el": { 230 | "version": "0.18.20", 231 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", 232 | "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", 233 | "cpu": [ 234 | "mips64el" 235 | ], 236 | "optional": true, 237 | "os": [ 238 | "linux" 239 | ], 240 | "engines": { 241 | "node": ">=12" 242 | } 243 | }, 244 | "node_modules/@esbuild/linux-ppc64": { 245 | "version": "0.18.20", 246 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", 247 | "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", 248 | "cpu": [ 249 | "ppc64" 250 | ], 251 | "optional": true, 252 | "os": [ 253 | "linux" 254 | ], 255 | "engines": { 256 | "node": ">=12" 257 | } 258 | }, 259 | "node_modules/@esbuild/linux-riscv64": { 260 | "version": "0.18.20", 261 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", 262 | "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", 263 | "cpu": [ 264 | "riscv64" 265 | ], 266 | "optional": true, 267 | "os": [ 268 | "linux" 269 | ], 270 | "engines": { 271 | "node": ">=12" 272 | } 273 | }, 274 | "node_modules/@esbuild/linux-s390x": { 275 | "version": "0.18.20", 276 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", 277 | "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", 278 | "cpu": [ 279 | "s390x" 280 | ], 281 | "optional": true, 282 | "os": [ 283 | "linux" 284 | ], 285 | "engines": { 286 | "node": ">=12" 287 | } 288 | }, 289 | "node_modules/@esbuild/linux-x64": { 290 | "version": "0.18.20", 291 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", 292 | "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", 293 | "cpu": [ 294 | "x64" 295 | ], 296 | "optional": true, 297 | "os": [ 298 | "linux" 299 | ], 300 | "engines": { 301 | "node": ">=12" 302 | } 303 | }, 304 | "node_modules/@esbuild/netbsd-x64": { 305 | "version": "0.18.20", 306 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", 307 | "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", 308 | "cpu": [ 309 | "x64" 310 | ], 311 | "optional": true, 312 | "os": [ 313 | "netbsd" 314 | ], 315 | "engines": { 316 | "node": ">=12" 317 | } 318 | }, 319 | "node_modules/@esbuild/openbsd-x64": { 320 | "version": "0.18.20", 321 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", 322 | "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", 323 | "cpu": [ 324 | "x64" 325 | ], 326 | "optional": true, 327 | "os": [ 328 | "openbsd" 329 | ], 330 | "engines": { 331 | "node": ">=12" 332 | } 333 | }, 334 | "node_modules/@esbuild/sunos-x64": { 335 | "version": "0.18.20", 336 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", 337 | "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", 338 | "cpu": [ 339 | "x64" 340 | ], 341 | "optional": true, 342 | "os": [ 343 | "sunos" 344 | ], 345 | "engines": { 346 | "node": ">=12" 347 | } 348 | }, 349 | "node_modules/@esbuild/win32-arm64": { 350 | "version": "0.18.20", 351 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", 352 | "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", 353 | "cpu": [ 354 | "arm64" 355 | ], 356 | "optional": true, 357 | "os": [ 358 | "win32" 359 | ], 360 | "engines": { 361 | "node": ">=12" 362 | } 363 | }, 364 | "node_modules/@esbuild/win32-ia32": { 365 | "version": "0.18.20", 366 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", 367 | "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", 368 | "cpu": [ 369 | "ia32" 370 | ], 371 | "optional": true, 372 | "os": [ 373 | "win32" 374 | ], 375 | "engines": { 376 | "node": ">=12" 377 | } 378 | }, 379 | "node_modules/@esbuild/win32-x64": { 380 | "version": "0.18.20", 381 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", 382 | "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", 383 | "cpu": [ 384 | "x64" 385 | ], 386 | "optional": true, 387 | "os": [ 388 | "win32" 389 | ], 390 | "engines": { 391 | "node": ">=12" 392 | } 393 | }, 394 | "node_modules/@jridgewell/sourcemap-codec": { 395 | "version": "1.5.0", 396 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 397 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" 398 | }, 399 | "node_modules/@mdi/font": { 400 | "version": "7.4.47", 401 | "resolved": "https://registry.npmjs.org/@mdi/font/-/font-7.4.47.tgz", 402 | "integrity": "sha512-43MtGpd585SNzHZPcYowu/84Vz2a2g31TvPMTm9uTiCSWzaheQySUcSyUH/46fPnuPQWof2yd0pGBtzee/IQWw==" 403 | }, 404 | "node_modules/@vitejs/plugin-vue": { 405 | "version": "4.6.2", 406 | "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", 407 | "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", 408 | "dev": true, 409 | "engines": { 410 | "node": "^14.18.0 || >=16.0.0" 411 | }, 412 | "peerDependencies": { 413 | "vite": "^4.0.0 || ^5.0.0", 414 | "vue": "^3.2.25" 415 | } 416 | }, 417 | "node_modules/@vue/compiler-core": { 418 | "version": "3.5.13", 419 | "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz", 420 | "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==", 421 | "dependencies": { 422 | "@babel/parser": "^7.25.3", 423 | "@vue/shared": "3.5.13", 424 | "entities": "^4.5.0", 425 | "estree-walker": "^2.0.2", 426 | "source-map-js": "^1.2.0" 427 | } 428 | }, 429 | "node_modules/@vue/compiler-dom": { 430 | "version": "3.5.13", 431 | "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz", 432 | "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==", 433 | "dependencies": { 434 | "@vue/compiler-core": "3.5.13", 435 | "@vue/shared": "3.5.13" 436 | } 437 | }, 438 | "node_modules/@vue/compiler-sfc": { 439 | "version": "3.5.13", 440 | "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz", 441 | "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==", 442 | "dependencies": { 443 | "@babel/parser": "^7.25.3", 444 | "@vue/compiler-core": "3.5.13", 445 | "@vue/compiler-dom": "3.5.13", 446 | "@vue/compiler-ssr": "3.5.13", 447 | "@vue/shared": "3.5.13", 448 | "estree-walker": "^2.0.2", 449 | "magic-string": "^0.30.11", 450 | "postcss": "^8.4.48", 451 | "source-map-js": "^1.2.0" 452 | } 453 | }, 454 | "node_modules/@vue/compiler-ssr": { 455 | "version": "3.5.13", 456 | "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz", 457 | "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==", 458 | "dependencies": { 459 | "@vue/compiler-dom": "3.5.13", 460 | "@vue/shared": "3.5.13" 461 | } 462 | }, 463 | "node_modules/@vue/devtools-api": { 464 | "version": "6.6.4", 465 | "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz", 466 | "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==" 467 | }, 468 | "node_modules/@vue/reactivity": { 469 | "version": "3.5.13", 470 | "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz", 471 | "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==", 472 | "dependencies": { 473 | "@vue/shared": "3.5.13" 474 | } 475 | }, 476 | "node_modules/@vue/runtime-core": { 477 | "version": "3.5.13", 478 | "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz", 479 | "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==", 480 | "dependencies": { 481 | "@vue/reactivity": "3.5.13", 482 | "@vue/shared": "3.5.13" 483 | } 484 | }, 485 | "node_modules/@vue/runtime-dom": { 486 | "version": "3.5.13", 487 | "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz", 488 | "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==", 489 | "dependencies": { 490 | "@vue/reactivity": "3.5.13", 491 | "@vue/runtime-core": "3.5.13", 492 | "@vue/shared": "3.5.13", 493 | "csstype": "^3.1.3" 494 | } 495 | }, 496 | "node_modules/@vue/server-renderer": { 497 | "version": "3.5.13", 498 | "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz", 499 | "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==", 500 | "dependencies": { 501 | "@vue/compiler-ssr": "3.5.13", 502 | "@vue/shared": "3.5.13" 503 | }, 504 | "peerDependencies": { 505 | "vue": "3.5.13" 506 | } 507 | }, 508 | "node_modules/@vue/shared": { 509 | "version": "3.5.13", 510 | "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz", 511 | "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==" 512 | }, 513 | "node_modules/@vuetify/loader-shared": { 514 | "version": "1.7.1", 515 | "resolved": "https://registry.npmjs.org/@vuetify/loader-shared/-/loader-shared-1.7.1.tgz", 516 | "integrity": "sha512-kLUvuAed6RCvkeeTNJzuy14pqnkur8lTuner7v7pNE/kVhPR97TuyXwBSBMR1cJeiLiOfu6SF5XlCYbXByEx1g==", 517 | "dependencies": { 518 | "find-cache-dir": "^3.3.2", 519 | "upath": "^2.0.1" 520 | }, 521 | "peerDependencies": { 522 | "vue": "^3.0.0", 523 | "vuetify": "^3.0.0-beta.4" 524 | } 525 | }, 526 | "node_modules/commondir": { 527 | "version": "1.0.1", 528 | "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", 529 | "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==" 530 | }, 531 | "node_modules/csstype": { 532 | "version": "3.1.3", 533 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", 534 | "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" 535 | }, 536 | "node_modules/debug": { 537 | "version": "4.4.0", 538 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", 539 | "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", 540 | "dependencies": { 541 | "ms": "^2.1.3" 542 | }, 543 | "engines": { 544 | "node": ">=6.0" 545 | }, 546 | "peerDependenciesMeta": { 547 | "supports-color": { 548 | "optional": true 549 | } 550 | } 551 | }, 552 | "node_modules/entities": { 553 | "version": "4.5.0", 554 | "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", 555 | "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", 556 | "engines": { 557 | "node": ">=0.12" 558 | }, 559 | "funding": { 560 | "url": "https://github.com/fb55/entities?sponsor=1" 561 | } 562 | }, 563 | "node_modules/esbuild": { 564 | "version": "0.18.20", 565 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz", 566 | "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", 567 | "hasInstallScript": true, 568 | "bin": { 569 | "esbuild": "bin/esbuild" 570 | }, 571 | "engines": { 572 | "node": ">=12" 573 | }, 574 | "optionalDependencies": { 575 | "@esbuild/android-arm": "0.18.20", 576 | "@esbuild/android-arm64": "0.18.20", 577 | "@esbuild/android-x64": "0.18.20", 578 | "@esbuild/darwin-arm64": "0.18.20", 579 | "@esbuild/darwin-x64": "0.18.20", 580 | "@esbuild/freebsd-arm64": "0.18.20", 581 | "@esbuild/freebsd-x64": "0.18.20", 582 | "@esbuild/linux-arm": "0.18.20", 583 | "@esbuild/linux-arm64": "0.18.20", 584 | "@esbuild/linux-ia32": "0.18.20", 585 | "@esbuild/linux-loong64": "0.18.20", 586 | "@esbuild/linux-mips64el": "0.18.20", 587 | "@esbuild/linux-ppc64": "0.18.20", 588 | "@esbuild/linux-riscv64": "0.18.20", 589 | "@esbuild/linux-s390x": "0.18.20", 590 | "@esbuild/linux-x64": "0.18.20", 591 | "@esbuild/netbsd-x64": "0.18.20", 592 | "@esbuild/openbsd-x64": "0.18.20", 593 | "@esbuild/sunos-x64": "0.18.20", 594 | "@esbuild/win32-arm64": "0.18.20", 595 | "@esbuild/win32-ia32": "0.18.20", 596 | "@esbuild/win32-x64": "0.18.20" 597 | } 598 | }, 599 | "node_modules/estree-walker": { 600 | "version": "2.0.2", 601 | "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", 602 | "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==" 603 | }, 604 | "node_modules/find-cache-dir": { 605 | "version": "3.3.2", 606 | "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", 607 | "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", 608 | "dependencies": { 609 | "commondir": "^1.0.1", 610 | "make-dir": "^3.0.2", 611 | "pkg-dir": "^4.1.0" 612 | }, 613 | "engines": { 614 | "node": ">=8" 615 | }, 616 | "funding": { 617 | "url": "https://github.com/avajs/find-cache-dir?sponsor=1" 618 | } 619 | }, 620 | "node_modules/find-up": { 621 | "version": "4.1.0", 622 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", 623 | "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", 624 | "dependencies": { 625 | "locate-path": "^5.0.0", 626 | "path-exists": "^4.0.0" 627 | }, 628 | "engines": { 629 | "node": ">=8" 630 | } 631 | }, 632 | "node_modules/fsevents": { 633 | "version": "2.3.3", 634 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", 635 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", 636 | "hasInstallScript": true, 637 | "optional": true, 638 | "os": [ 639 | "darwin" 640 | ], 641 | "engines": { 642 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0" 643 | } 644 | }, 645 | "node_modules/locate-path": { 646 | "version": "5.0.0", 647 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", 648 | "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", 649 | "dependencies": { 650 | "p-locate": "^4.1.0" 651 | }, 652 | "engines": { 653 | "node": ">=8" 654 | } 655 | }, 656 | "node_modules/magic-string": { 657 | "version": "0.30.17", 658 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", 659 | "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", 660 | "dependencies": { 661 | "@jridgewell/sourcemap-codec": "^1.5.0" 662 | } 663 | }, 664 | "node_modules/make-dir": { 665 | "version": "3.1.0", 666 | "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", 667 | "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", 668 | "dependencies": { 669 | "semver": "^6.0.0" 670 | }, 671 | "engines": { 672 | "node": ">=8" 673 | }, 674 | "funding": { 675 | "url": "https://github.com/sponsors/sindresorhus" 676 | } 677 | }, 678 | "node_modules/ms": { 679 | "version": "2.1.3", 680 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 681 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" 682 | }, 683 | "node_modules/nanoid": { 684 | "version": "3.3.8", 685 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", 686 | "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", 687 | "funding": [ 688 | { 689 | "type": "github", 690 | "url": "https://github.com/sponsors/ai" 691 | } 692 | ], 693 | "bin": { 694 | "nanoid": "bin/nanoid.cjs" 695 | }, 696 | "engines": { 697 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 698 | } 699 | }, 700 | "node_modules/p-limit": { 701 | "version": "2.3.0", 702 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", 703 | "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", 704 | "dependencies": { 705 | "p-try": "^2.0.0" 706 | }, 707 | "engines": { 708 | "node": ">=6" 709 | }, 710 | "funding": { 711 | "url": "https://github.com/sponsors/sindresorhus" 712 | } 713 | }, 714 | "node_modules/p-locate": { 715 | "version": "4.1.0", 716 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", 717 | "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", 718 | "dependencies": { 719 | "p-limit": "^2.2.0" 720 | }, 721 | "engines": { 722 | "node": ">=8" 723 | } 724 | }, 725 | "node_modules/p-try": { 726 | "version": "2.2.0", 727 | "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", 728 | "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", 729 | "engines": { 730 | "node": ">=6" 731 | } 732 | }, 733 | "node_modules/path-exists": { 734 | "version": "4.0.0", 735 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 736 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 737 | "engines": { 738 | "node": ">=8" 739 | } 740 | }, 741 | "node_modules/picocolors": { 742 | "version": "1.1.1", 743 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 744 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" 745 | }, 746 | "node_modules/pkg-dir": { 747 | "version": "4.2.0", 748 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", 749 | "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", 750 | "dependencies": { 751 | "find-up": "^4.0.0" 752 | }, 753 | "engines": { 754 | "node": ">=8" 755 | } 756 | }, 757 | "node_modules/postcss": { 758 | "version": "8.4.49", 759 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", 760 | "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", 761 | "funding": [ 762 | { 763 | "type": "opencollective", 764 | "url": "https://opencollective.com/postcss/" 765 | }, 766 | { 767 | "type": "tidelift", 768 | "url": "https://tidelift.com/funding/github/npm/postcss" 769 | }, 770 | { 771 | "type": "github", 772 | "url": "https://github.com/sponsors/ai" 773 | } 774 | ], 775 | "dependencies": { 776 | "nanoid": "^3.3.7", 777 | "picocolors": "^1.1.1", 778 | "source-map-js": "^1.2.1" 779 | }, 780 | "engines": { 781 | "node": "^10 || ^12 || >=14" 782 | } 783 | }, 784 | "node_modules/rollup": { 785 | "version": "3.29.5", 786 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.29.5.tgz", 787 | "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", 788 | "bin": { 789 | "rollup": "dist/bin/rollup" 790 | }, 791 | "engines": { 792 | "node": ">=14.18.0", 793 | "npm": ">=8.0.0" 794 | }, 795 | "optionalDependencies": { 796 | "fsevents": "~2.3.2" 797 | } 798 | }, 799 | "node_modules/semver": { 800 | "version": "6.3.1", 801 | "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", 802 | "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", 803 | "bin": { 804 | "semver": "bin/semver.js" 805 | } 806 | }, 807 | "node_modules/source-map-js": { 808 | "version": "1.2.1", 809 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 810 | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 811 | "engines": { 812 | "node": ">=0.10.0" 813 | } 814 | }, 815 | "node_modules/upath": { 816 | "version": "2.0.1", 817 | "resolved": "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz", 818 | "integrity": "sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w==", 819 | "engines": { 820 | "node": ">=4", 821 | "yarn": "*" 822 | } 823 | }, 824 | "node_modules/vite": { 825 | "version": "4.5.9", 826 | "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.9.tgz", 827 | "integrity": "sha512-qK9W4xjgD3gXbC0NmdNFFnVFLMWSNiR3swj957yutwzzN16xF/E7nmtAyp1rT9hviDroQANjE4HK3H4WqWdFtw==", 828 | "license": "MIT", 829 | "dependencies": { 830 | "esbuild": "^0.18.10", 831 | "postcss": "^8.4.27", 832 | "rollup": "^3.27.1" 833 | }, 834 | "bin": { 835 | "vite": "bin/vite.js" 836 | }, 837 | "engines": { 838 | "node": "^14.18.0 || >=16.0.0" 839 | }, 840 | "funding": { 841 | "url": "https://github.com/vitejs/vite?sponsor=1" 842 | }, 843 | "optionalDependencies": { 844 | "fsevents": "~2.3.2" 845 | }, 846 | "peerDependencies": { 847 | "@types/node": ">= 14", 848 | "less": "*", 849 | "lightningcss": "^1.21.0", 850 | "sass": "*", 851 | "stylus": "*", 852 | "sugarss": "*", 853 | "terser": "^5.4.0" 854 | }, 855 | "peerDependenciesMeta": { 856 | "@types/node": { 857 | "optional": true 858 | }, 859 | "less": { 860 | "optional": true 861 | }, 862 | "lightningcss": { 863 | "optional": true 864 | }, 865 | "sass": { 866 | "optional": true 867 | }, 868 | "stylus": { 869 | "optional": true 870 | }, 871 | "sugarss": { 872 | "optional": true 873 | }, 874 | "terser": { 875 | "optional": true 876 | } 877 | } 878 | }, 879 | "node_modules/vite-plugin-vuetify": { 880 | "version": "1.0.1", 881 | "resolved": "https://registry.npmjs.org/vite-plugin-vuetify/-/vite-plugin-vuetify-1.0.1.tgz", 882 | "integrity": "sha512-/xHsIDuHxq7f6fDqCBYxNascLhDi+X8dV3RzTwmo4mGPrSnGq9pHv8wJsXBIQIT3nY8s16V0lmd6sXMjm0F8wg==", 883 | "dependencies": { 884 | "@vuetify/loader-shared": "^1.7.0", 885 | "debug": "^4.3.3", 886 | "upath": "^2.0.1" 887 | }, 888 | "engines": { 889 | "node": ">=12" 890 | }, 891 | "peerDependencies": { 892 | "vite": "^2.7.0 || ^3.0.0 || ^4.0.0", 893 | "vuetify": "^3.0.0-beta.4" 894 | } 895 | }, 896 | "node_modules/vue": { 897 | "version": "3.5.13", 898 | "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz", 899 | "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==", 900 | "dependencies": { 901 | "@vue/compiler-dom": "3.5.13", 902 | "@vue/compiler-sfc": "3.5.13", 903 | "@vue/runtime-dom": "3.5.13", 904 | "@vue/server-renderer": "3.5.13", 905 | "@vue/shared": "3.5.13" 906 | }, 907 | "peerDependencies": { 908 | "typescript": "*" 909 | }, 910 | "peerDependenciesMeta": { 911 | "typescript": { 912 | "optional": true 913 | } 914 | } 915 | }, 916 | "node_modules/vue-router": { 917 | "version": "4.5.0", 918 | "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz", 919 | "integrity": "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==", 920 | "dependencies": { 921 | "@vue/devtools-api": "^6.6.4" 922 | }, 923 | "funding": { 924 | "url": "https://github.com/sponsors/posva" 925 | }, 926 | "peerDependencies": { 927 | "vue": "^3.2.0" 928 | } 929 | }, 930 | "node_modules/vuetify": { 931 | "version": "3.7.5", 932 | "resolved": "https://registry.npmjs.org/vuetify/-/vuetify-3.7.5.tgz", 933 | "integrity": "sha512-5aiSz8WJyGzYe3yfgDbzxsFATwHvKtdvFAaUJEDTx7xRv55s3YiOho/MFhs5iTbmh2VT4ToRgP0imBUP660UOw==", 934 | "engines": { 935 | "node": "^12.20 || >=14.13" 936 | }, 937 | "funding": { 938 | "type": "github", 939 | "url": "https://github.com/sponsors/johnleider" 940 | }, 941 | "peerDependencies": { 942 | "typescript": ">=4.7", 943 | "vite-plugin-vuetify": ">=1.0.0", 944 | "vue": "^3.3.0", 945 | "webpack-plugin-vuetify": ">=2.0.0" 946 | }, 947 | "peerDependenciesMeta": { 948 | "typescript": { 949 | "optional": true 950 | }, 951 | "vite-plugin-vuetify": { 952 | "optional": true 953 | }, 954 | "webpack-plugin-vuetify": { 955 | "optional": true 956 | } 957 | } 958 | } 959 | } 960 | } 961 | --------------------------------------------------------------------------------