├── .gitignore ├── License ├── README.md ├── ansible ├── playbook.yml ├── roles │ └── install_prerequisites │ │ └── tasks │ │ └── main.yml └── vars.yml ├── docker-compose.yml ├── prometheus.yml ├── server ├── .env.example ├── .gitignore ├── Dockerfile ├── auth │ ├── discord.go │ ├── github.go │ ├── google.go │ └── user.go ├── build-and-run.sh ├── config │ └── config.go ├── go.mod ├── go.sum ├── handler │ ├── admin_delete_all_user_files.go │ ├── admin_delete_file.go │ ├── admin_delete_user.go │ ├── admin_files.go │ ├── admin_overview.go │ ├── admin_users.go │ ├── delete_file.go │ ├── delete_folder.go │ ├── files.go │ ├── login.go │ ├── new_folder.go │ ├── session.go │ ├── tus_handler.go │ ├── update_file.go │ ├── update_folder.go │ └── user_settings.go ├── main.go ├── middleware │ ├── auth.go │ ├── auth_admin.go │ ├── cors.go │ ├── dir.go │ └── rate_limit.go ├── prisma │ ├── db │ │ └── .gitignore │ ├── migrations │ │ ├── 20230904184641_h │ │ │ └── migration.sql │ │ ├── 20230905141316_path │ │ │ └── migration.sql │ │ ├── 20230905184155_size │ │ │ └── migration.sql │ │ ├── 20230906072639_user_path │ │ │ └── migration.sql │ │ ├── 20230906092845_rm_path │ │ │ └── migration.sql │ │ ├── 20230908025236_del │ │ │ └── migration.sql │ │ ├── 20230912102841_size_bigint │ │ │ └── migration.sql │ │ ├── 20230914105609_add_role │ │ │ └── migration.sql │ │ ├── 20230922225200_ │ │ │ └── migration.sql │ │ ├── 20230929130803_ │ │ │ └── migration.sql │ │ ├── 20230929154118_ │ │ │ └── migration.sql │ │ ├── 20230929154603_ │ │ │ └── migration.sql │ │ └── migration_lock.toml │ ├── prisma.go │ └── schema.prisma └── utils │ └── randomPath.go └── website ├── .dockerignore ├── .env.example ├── .gitignore ├── .prettierignore ├── Dockerfile ├── README.md ├── components.json ├── next-env.d.ts ├── next.config.js ├── package.json ├── pnpm-lock.yaml ├── postcss.config.js ├── prettier.config.js ├── public ├── admin-dashboard.png ├── avatar.png ├── dashboard.png ├── doge.png ├── elon.jpeg ├── favicon.ico ├── favicon.png ├── next.svg ├── thirteen.svg └── vercel.svg ├── src ├── api │ ├── adminFiles.ts │ ├── adminOverview.ts │ ├── adminUsers.ts │ ├── changePass.ts │ ├── delete.ts │ ├── deleteFile.ts │ ├── deleteUser.ts │ ├── deleteUserFiles.ts │ ├── getData.ts │ ├── githubLogin.ts │ ├── login.ts │ ├── makeRequest.ts │ ├── newFolder.ts │ └── rename.ts ├── app │ ├── (admin) │ │ ├── admin │ │ │ ├── AdminCard.tsx │ │ │ ├── files │ │ │ │ ├── DeleteFileDialog.tsx │ │ │ │ ├── FilesTable.tsx │ │ │ │ ├── RowAction.tsx │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ └── users │ │ │ │ ├── DeleteUserDialog.tsx │ │ │ │ ├── DeleteUserFiles.tsx │ │ │ │ ├── RowAction.tsx │ │ │ │ ├── UsersTable.tsx │ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (auth) │ │ ├── auth │ │ │ ├── discord │ │ │ │ └── page.tsx │ │ │ ├── github │ │ │ │ └── page.tsx │ │ │ └── google │ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (dashboard) │ │ ├── dashboard │ │ │ ├── albums │ │ │ │ ├── AlbumCard.tsx │ │ │ │ ├── DataTable.tsx │ │ │ │ └── page.tsx │ │ │ ├── all-media │ │ │ │ └── page.tsx │ │ │ ├── devices │ │ │ │ ├── DataTable.tsx │ │ │ │ └── page.tsx │ │ │ ├── favorites │ │ │ │ ├── DataTable.tsx │ │ │ │ └── page.tsx │ │ │ ├── images │ │ │ │ └── page.tsx │ │ │ ├── page.tsx │ │ │ ├── recent │ │ │ │ ├── DataTable.tsx │ │ │ │ └── page.tsx │ │ │ ├── settings │ │ │ │ └── page.tsx │ │ │ ├── team │ │ │ │ ├── DataTable.tsx │ │ │ │ └── page.tsx │ │ │ ├── trash │ │ │ │ ├── DataTable.tsx │ │ │ │ └── page.tsx │ │ │ └── videos │ │ │ │ └── page.tsx │ │ └── layout.tsx │ └── (home) │ │ ├── layout.tsx │ │ ├── login │ │ ├── LoginForm.tsx │ │ └── page.tsx │ │ └── page.tsx ├── components │ ├── AlertNotImplemented.tsx │ ├── Avatar.tsx │ ├── Breadcrumbs.tsx │ ├── DataTable.tsx │ ├── DeleteDialog.tsx │ ├── FileCard.tsx │ ├── FolderCard.tsx │ ├── GetFileIcon.tsx │ ├── HeadText.tsx │ ├── Logo.tsx │ ├── NewFolderDialog.tsx │ ├── PreviewFileDialog.tsx │ ├── ReactQueryProvider.tsx │ ├── RenameDialog.tsx │ ├── RowAction.tsx │ ├── Search.tsx │ ├── Spinner.tsx │ ├── icons.tsx │ ├── icons │ │ ├── Ansible.tsx │ │ ├── Discord.tsx │ │ ├── Docker.tsx │ │ ├── Github.tsx │ │ ├── Go.tsx │ │ ├── Google.tsx │ │ ├── Grafana.tsx │ │ ├── Next.tsx │ │ ├── Nginx.tsx │ │ ├── Postgres.tsx │ │ ├── Prisma.tsx │ │ ├── Prometheus.tsx │ │ ├── ReactQuery.tsx │ │ ├── Redis.tsx │ │ └── X.tsx │ ├── main-nav.tsx │ ├── mockDevices.ts │ ├── mockFiles.ts │ ├── tailwind-indicator.tsx │ ├── theme-provider.tsx │ ├── theme-toggle.tsx │ └── ui │ │ ├── SidebarButton.tsx │ │ ├── alert.tsx │ │ ├── avatar.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── popover.tsx │ │ ├── progress.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ └── table.tsx ├── config.ts ├── config │ ├── meta.ts │ └── site.ts ├── layout │ ├── Sidebar.tsx │ ├── SidebarAdmin.tsx │ ├── SiteHeader.tsx │ ├── SiteHeaderLoggedIn.tsx │ ├── sidebar-nav-admin.tsx │ └── sidebar-nav.tsx ├── lib │ ├── fonts.ts │ └── utils.ts ├── pages │ ├── _app.tsx │ ├── _meta.json │ └── documentation │ │ ├── _meta.json │ │ ├── ansible.mdx │ │ ├── conclusion.mdx │ │ ├── docker.mdx │ │ ├── index.mdx │ │ ├── locally.mdx │ │ └── vps.mdx ├── queryKeys.ts ├── session │ └── SetSession.tsx ├── state │ └── state.ts ├── styles │ └── globals.css └── types │ ├── index.ts │ └── nav.ts ├── tailwind.config.js ├── theme.config.tsx ├── tsconfig.json └── tsconfig.tsbuildinfo /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | uploads 3 | pg-data 4 | prometheus 5 | data 6 | -------------------------------------------------------------------------------- /License: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aland Sleman 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 | -------------------------------------------------------------------------------- /ansible/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Setup Local Environment 3 | hosts: localhost 4 | become: true 5 | 6 | roles: 7 | - install_prerequisites 8 | tasks: 9 | 10 | - name: Include Variables 11 | include_vars: 12 | file: vars.yml 13 | 14 | - name: Run Docker Compose Up 15 | ansible.builtin.shell: | 16 | docker compose up -d 17 | args: 18 | chdir: "{{ playbook_dir }}/.." 19 | 20 | 21 | - name: Update Nginx configuration 22 | ansible.builtin.copy: 23 | content: | 24 | server { 25 | listen 80; 26 | server_name {{domain.website}}; 27 | 28 | location / { 29 | proxy_pass http://localhost:4001; 30 | proxy_set_header Host $http_host; 31 | proxy_set_header X-Real-IP $remote_addr; 32 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 33 | proxy_set_header X-Forwarded-Proto $scheme; 34 | proxy_set_header X-Forwarded-Host $http_host; 35 | } 36 | } 37 | 38 | server { 39 | listen 80; 40 | server_name {{domain.server}}; 41 | 42 | location / { 43 | proxy_pass http://localhost:4000; 44 | proxy_set_header Host $http_host; 45 | proxy_set_header X-Real-IP $remote_addr; 46 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 47 | proxy_set_header X-Forwarded-Proto $scheme; 48 | proxy_set_header X-Forwarded-Host $http_host; 49 | } 50 | } 51 | 52 | server { 53 | listen 80; 54 | server_name {{domain.grafana}}; 55 | 56 | location / { 57 | proxy_pass http://localhost:3000; 58 | proxy_set_header Host $http_host; 59 | proxy_set_header X-Real-IP $remote_addr; 60 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; 61 | proxy_set_header X-Forwarded-Proto $scheme; 62 | proxy_set_header X-Forwarded-Host $http_host; 63 | } 64 | } 65 | dest: /etc/nginx/sites-available/{{domain.domain_name}}.conf 66 | 67 | - name: Create symlink 68 | ansible.builtin.command: ln -s /etc/nginx/sites-available/{{domain.domain_name}}.conf /etc/nginx/sites-enabled/ 69 | 70 | 71 | - name: Reload Nginx 72 | ansible.builtin.command: nginx -s reload 73 | -------------------------------------------------------------------------------- /ansible/roles/install_prerequisites/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Install Docker and docker compose 3 | ansible.builtin.shell: | 4 | mkdir -p /etc/apt/keyrings 5 | curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg 6 | echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" | tee /etc/apt/sources.list.d/docker.list > /dev/null 7 | apt update 8 | apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin 9 | 10 | - name: Install Nginx 11 | apt: 12 | name: nginx 13 | state: present 14 | -------------------------------------------------------------------------------- /ansible/vars.yml: -------------------------------------------------------------------------------- 1 | # vars.yml 2 | --- 3 | domain: 4 | domain_name: kurdmake 5 | website: storagebox.kurdmake.com 6 | server: storagebox-api.kurdmake.com 7 | grafana: grafana.kurdmake.com 8 | 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | postgres: 5 | image: postgres:latest 6 | container_name: storagebox-postgres 7 | environment: 8 | POSTGRES_USER: postgres 9 | POSTGRES_PASSWORD: 123 10 | POSTGRES_DB: postgres 11 | ports: 12 | - "5432:5432" 13 | volumes: 14 | - ./data/pg-data:/var/lib/postgresql/data 15 | 16 | redis: 17 | image: redis:latest 18 | container_name: storagebox-redis 19 | ports: 20 | - "6379:6379" 21 | 22 | server: 23 | container_name: storagebox-server 24 | build: 25 | context: ./server 26 | dockerfile: Dockerfile 27 | ports: 28 | - "4000:4000" 29 | volumes: 30 | - ./data/uploads:/app/uploads 31 | 32 | website: 33 | container_name: storagebox-website 34 | build: 35 | context: ./website 36 | dockerfile: Dockerfile 37 | ports: 38 | - "4001:4001" 39 | 40 | node_exporter: 41 | image: quay.io/prometheus/node-exporter:latest 42 | container_name: node_exporter 43 | ports: 44 | - "9100:9100" 45 | command: 46 | - '--path.rootfs=/host' 47 | pid: host 48 | restart: unless-stopped 49 | volumes: 50 | - './data/node_exporter-data:/host:ro,rslave' 51 | 52 | prometheus: 53 | image: prom/prometheus 54 | container_name: prometheus 55 | ports: 56 | - "9090:9090" 57 | volumes: 58 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 59 | - ./data/prometheus-data:/etc/prometheus 60 | 61 | grafana: 62 | image: grafana/grafana-oss:latest 63 | container_name: grafana 64 | environment: 65 | GF_SECURITY_ALLOW_EMBEDDING: true 66 | GF_AUTH_ANONYMOUS_ENABLED: true 67 | ports: 68 | - "3000:3000" 69 | volumes: 70 | - grafana-data:/var/lib/grafana 71 | restart: unless-stopped 72 | 73 | volumes: 74 | grafana-data: 75 | -------------------------------------------------------------------------------- /prometheus.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 5s # By default, scrape targets every 15 seconds. 3 | 4 | scrape_configs: 5 | - job_name: 'prometheus' 6 | scrape_interval: 5s 7 | static_configs: 8 | - targets: ['prometheus:9090'] 9 | 10 | - job_name: 'node_exporter' 11 | static_configs: 12 | - targets: ['node_exporter:9100'] 13 | 14 | -------------------------------------------------------------------------------- /server/.env.example: -------------------------------------------------------------------------------- 1 | JWT_SECRET=your-secret-key 2 | 3 | SERVER_URL=http://localhost:4000 4 | 5 | DATABASE_URL=postgresql://postgres:123@postgres:5432/postgres 6 | REDIS_URL=redis://redis:6379/0 7 | 8 | GITHUB_CLIENT_SECRET= 9 | GITHUB_CLIENT_ID= 10 | GITHUB_REDIRECT_URI=http://localhost:4001/auth/github 11 | 12 | DISCORD_CLIENT_SECRET= 13 | DISCORD_CLIENT_ID= 14 | DISCORD_REDIRECT_URI=http://localhost:4001/auth/discord 15 | 16 | GOOGLE_CLIENT_ID= 17 | GOOGLE_CLIENT_SECRET= 18 | GOOGLE_REDIRECT_URI=http://localhost:4001/auth/google 19 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | uploads/* 3 | pg-data/* 4 | server 5 | server.log 6 | -------------------------------------------------------------------------------- /server/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.21.1 2 | 3 | WORKDIR /app 4 | 5 | COPY . . 6 | 7 | RUN go mod download 8 | 9 | EXPOSE 4000 10 | 11 | COPY build-and-run.sh /app/build-and-run.sh 12 | RUN chmod +x /app/build-and-run.sh 13 | 14 | ENTRYPOINT ["/app/build-and-run.sh"] 15 | -------------------------------------------------------------------------------- /server/auth/user.go: -------------------------------------------------------------------------------- 1 | package auth 2 | 3 | import ( 4 | "os" 5 | "strings" 6 | "time" 7 | 8 | "github.com/AlandSleman/StorageBox/config" 9 | "github.com/AlandSleman/StorageBox/prisma" 10 | "github.com/AlandSleman/StorageBox/prisma/db" 11 | "github.com/golang-jwt/jwt/v5" 12 | "golang.org/x/crypto/bcrypt" 13 | ) 14 | 15 | func CreateUserPassword(username, password string) (*db.UserModel, error) { 16 | // trim username 17 | username = strings.ReplaceAll(username, " ", "") 18 | 19 | hashedPassword, err := HashPassword(password) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | user, err := prisma.Client().User.CreateOne( 25 | db.User.Username.Set(username), 26 | db.User.Provider.Set("password"), 27 | db.User.Password.Set(hashedPassword), 28 | ).Exec(prisma.Context()) 29 | 30 | if err != nil { 31 | return nil, err 32 | } 33 | 34 | // Create a folder for the user 35 | err = CreateFolderForUser(user.ID) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | return user, nil 41 | } 42 | 43 | func CreateUserProvider(id, username, provider string, email string) (*db.UserModel, error) { 44 | // trim username 45 | username = strings.ReplaceAll(username, " ", "") 46 | 47 | user, err := prisma.Client().User.CreateOne( 48 | db.User.Username.Set(username), 49 | db.User.Provider.Set(provider), 50 | db.User.ID.Set(id), 51 | db.User.Email.Set(email), 52 | ).Exec(prisma.Context()) 53 | 54 | if err != nil { 55 | return nil, err 56 | } 57 | 58 | // Create a folder for the user 59 | err = CreateFolderForUser(user.ID) 60 | if err != nil { 61 | return nil, err 62 | } 63 | 64 | return user, nil 65 | } 66 | 67 | func HashPassword(password string) (string, error) { 68 | hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 69 | if err != nil { 70 | return "", err 71 | } 72 | return string(hashedPassword), nil 73 | } 74 | 75 | func CheckPassword(hashedPassword, password string) error { 76 | return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password)) 77 | } 78 | 79 | func CreateFolderForUser(userID string) error { 80 | folderPath := "./uploads/" + userID // Specify the folder path 81 | 82 | _, err := prisma.Client().Folder.CreateOne( 83 | db.Folder.Name.Set("/"), 84 | db.Folder.User.Link(db.User.ID.Equals(userID)), 85 | ).Exec(prisma.Context()) 86 | if err != nil { 87 | return err 88 | } 89 | // Create the folder (including parent directories) with read-write permissions (os.ModePerm) 90 | if err := os.MkdirAll(folderPath, os.ModePerm); err != nil { 91 | return err 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func GenerateJWTToken(userID, role string) (string, error) { 98 | token := jwt.New(jwt.SigningMethodHS256) 99 | claims := token.Claims.(jwt.MapClaims) 100 | claims["id"] = userID 101 | claims["exp"] = time.Now().Add(time.Hour * 3000).Unix() // Token expiration time (1 hour) 102 | 103 | // Include the role in the JWT claims if provided 104 | claims["role"] = role 105 | 106 | // Sign the token with the secret key 107 | return token.SignedString([]byte(config.GetConfig().JWT_SECRET)) 108 | } 109 | -------------------------------------------------------------------------------- /server/build-and-run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Run Prisma Client Go to apply database migrations 3 | go run github.com/steebchen/prisma-client-go db push 4 | 5 | # Build the Go application 6 | go build -o server 7 | 8 | # Start the application 9 | ./server 10 | -------------------------------------------------------------------------------- /server/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "os" 5 | ) 6 | 7 | type ServerConfig struct { 8 | JWT_SECRET string 9 | REDIS_URL string 10 | MAX_SIZE int64 11 | GITHUB_CLIENT_ID string 12 | SERVER_URL string 13 | GITHUB_CLIENT_SECRET string 14 | GITHUB_REDIRECT_URI string 15 | 16 | DISCORD_CLIENT_ID string 17 | DISCORD_CLIENT_SECRET string 18 | DISCORD_REDIRECT_URI string 19 | 20 | GOOGLE_CLIENT_ID string 21 | GOOGLE_CLIENT_SECRET string 22 | GOOGLE_REDIRECT_URI string 23 | } 24 | 25 | func GetConfig() *ServerConfig { 26 | config := &ServerConfig{ 27 | JWT_SECRET: os.Getenv("JWT_SECRET"), 28 | REDIS_URL: os.Getenv("REDIS_URL"), 29 | // 500 MB max allowed storage per user 30 | MAX_SIZE: 500 * 1024 * 1024, 31 | GITHUB_CLIENT_ID: os.Getenv("GITHUB_CLIENT_ID"), 32 | SERVER_URL: os.Getenv("SERVER_URL"), 33 | GITHUB_CLIENT_SECRET: os.Getenv("GITHUB_CLIENT_SECRET"), 34 | GITHUB_REDIRECT_URI: os.Getenv("GITHUB_REDIRECT_URI"), 35 | 36 | DISCORD_CLIENT_ID: os.Getenv("DISCORD_CLIENT_ID"), 37 | DISCORD_CLIENT_SECRET: os.Getenv("DISCORD_CLIENT_SECRET"), 38 | DISCORD_REDIRECT_URI: os.Getenv("DISCORD_REDIRECT_URI"), 39 | 40 | // Load Google OAuth configuration variables here 41 | GOOGLE_CLIENT_ID: os.Getenv("GOOGLE_CLIENT_ID"), 42 | GOOGLE_CLIENT_SECRET: os.Getenv("GOOGLE_CLIENT_SECRET"), 43 | GOOGLE_REDIRECT_URI: os.Getenv("GOOGLE_REDIRECT_URI"), 44 | } 45 | 46 | return config 47 | } 48 | -------------------------------------------------------------------------------- /server/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/AlandSleman/StorageBox 2 | 3 | go 1.21.0 4 | 5 | require ( 6 | github.com/gin-contrib/cors v1.4.0 7 | github.com/gin-gonic/gin v1.9.1 8 | github.com/golang-jwt/jwt/v5 v5.0.0 9 | github.com/google/uuid v1.3.1 10 | github.com/iancoleman/strcase v0.2.0 11 | github.com/joho/godotenv v1.5.1 12 | github.com/redis/go-redis/v9 v9.1.0 13 | github.com/shopspring/decimal v1.3.1 14 | github.com/steebchen/prisma-client-go v0.21.0 15 | github.com/takuoki/gocase v1.0.0 16 | github.com/tus/tusd v1.12.1 17 | github.com/ulule/limiter/v3 v3.11.2 18 | golang.org/x/crypto v0.13.0 19 | golang.org/x/oauth2 v0.12.0 20 | golang.org/x/text v0.13.0 21 | ) 22 | 23 | require ( 24 | cloud.google.com/go/compute v1.20.1 // indirect 25 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 26 | github.com/bmizerany/pat v0.0.0-20170815010413-6226ea591a40 // indirect 27 | github.com/bytedance/sonic v1.9.1 // indirect 28 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 29 | github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect 30 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 31 | github.com/gabriel-vasile/mimetype v1.4.2 // indirect 32 | github.com/gin-contrib/sse v0.1.0 // indirect 33 | github.com/go-playground/locales v0.14.1 // indirect 34 | github.com/go-playground/universal-translator v0.18.1 // indirect 35 | github.com/go-playground/validator/v10 v10.14.0 // indirect 36 | github.com/goccy/go-json v0.10.2 // indirect 37 | github.com/golang/protobuf v1.5.3 // indirect 38 | github.com/json-iterator/go v1.1.12 // indirect 39 | github.com/klauspost/cpuid/v2 v2.2.4 // indirect 40 | github.com/leodido/go-urn v1.2.4 // indirect 41 | github.com/mattn/go-isatty v0.0.19 // indirect 42 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 43 | github.com/modern-go/reflect2 v1.0.2 // indirect 44 | github.com/pelletier/go-toml/v2 v2.0.8 // indirect 45 | github.com/pkg/errors v0.9.1 // indirect 46 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 47 | github.com/ugorji/go/codec v1.2.11 // indirect 48 | golang.org/x/arch v0.3.0 // indirect 49 | golang.org/x/net v0.15.0 // indirect 50 | golang.org/x/sys v0.12.0 // indirect 51 | google.golang.org/appengine v1.6.7 // indirect 52 | google.golang.org/protobuf v1.31.0 // indirect 53 | gopkg.in/yaml.v3 v3.0.1 // indirect 54 | ) 55 | -------------------------------------------------------------------------------- /server/handler/admin_delete_all_user_files.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "os" 7 | "path/filepath" 8 | 9 | "github.com/AlandSleman/StorageBox/prisma" 10 | "github.com/AlandSleman/StorageBox/prisma/db" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | func AdminDeleteAllUserFiles(c *gin.Context) { 15 | 16 | var Body struct { 17 | UserID string `json:"id" binding:"required"` 18 | } 19 | 20 | if err := c.ShouldBindJSON(&Body); err != nil { 21 | c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid input"}) 22 | return 23 | } 24 | 25 | user, err := prisma.Client().User.FindUnique( 26 | db.User.ID.Equals(Body.UserID), 27 | ).With(db.User.Files.Fetch()).With(db.User.Folders.Fetch()).Exec(prisma.Context()) 28 | 29 | if err != nil { 30 | println(err.Error()) 31 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to get user"}) 32 | return 33 | } 34 | 35 | for _, file := range user.Files() { 36 | mainFilePath := filepath.Join("./uploads/", user.ID, file.ID) 37 | infoFilePath := mainFilePath + ".info" 38 | 39 | err := os.Remove(mainFilePath) 40 | if err != nil { 41 | println(err.Error()) 42 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to delete file"}) 43 | return 44 | } 45 | 46 | // Delete the info file 47 | err = os.Remove(infoFilePath) 48 | if err != nil { 49 | println(err.Error()) 50 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to delete info file"}) 51 | return 52 | } 53 | 54 | _, err = prisma.Client().File.FindUnique( 55 | db.File.ID.Equals(file.ID), 56 | ).Delete().Exec(prisma.Context()) 57 | 58 | if err != nil { 59 | if errors.Is(err, db.ErrNotFound) { 60 | // success deleted file 61 | } else { 62 | println(err.Error()) 63 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to delete user"}) 64 | return 65 | } 66 | } 67 | 68 | } 69 | 70 | c.JSON(http.StatusOK, gin.H{"message": "success"}) 71 | return 72 | } 73 | -------------------------------------------------------------------------------- /server/handler/admin_delete_file.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/AlandSleman/StorageBox/prisma" 9 | "github.com/AlandSleman/StorageBox/prisma/db" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | func AdminDeleteFile(c *gin.Context) { 14 | 15 | var Body struct { 16 | FileID string `json:"id" binding:"required"` 17 | } 18 | 19 | if err := c.ShouldBindJSON(&Body); err != nil { 20 | c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid input"}) 21 | return 22 | } 23 | 24 | file, err := prisma.Client().File.FindUnique( 25 | db.File.ID.Equals(Body.FileID), 26 | ).Exec(prisma.Context()) 27 | 28 | if err != nil { 29 | println(err.Error()) 30 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to get file"}) 31 | return 32 | } 33 | 34 | mainFilePath := filepath.Join("./uploads/", file.UserID, file.ID) 35 | infoFilePath := mainFilePath + ".info" 36 | 37 | err = os.Remove(mainFilePath) 38 | if err != nil { 39 | println(err.Error()) 40 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to delete file"}) 41 | return 42 | } 43 | 44 | err = os.Remove(infoFilePath) 45 | if err != nil { 46 | println(err.Error()) 47 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to delete info file"}) 48 | return 49 | } 50 | 51 | _, err = prisma.Client().File.FindUnique( 52 | db.File.ID.Equals(file.ID), 53 | ).Delete().Exec(prisma.Context()) 54 | 55 | c.JSON(http.StatusOK, gin.H{"message": "success"}) 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /server/handler/admin_delete_user.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/AlandSleman/StorageBox/prisma" 9 | "github.com/AlandSleman/StorageBox/prisma/db" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | func AdminDeleteUser(c *gin.Context) { 14 | 15 | var Body struct { 16 | UserID string `json:"id" binding:"required"` 17 | } 18 | 19 | if err := c.ShouldBindJSON(&Body); err != nil { 20 | c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid input"}) 21 | return 22 | } 23 | 24 | user, err := prisma.Client().User.FindUnique( 25 | db.User.ID.Equals(Body.UserID), 26 | ).With(db.User.Files.Fetch()).With(db.User.Folders.Fetch()).Exec(prisma.Context()) 27 | 28 | if err != nil { 29 | println(err.Error()) 30 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to get user"}) 31 | return 32 | } 33 | 34 | if len(user.Files()) > 0 { 35 | for _, file := range user.Files() { 36 | mainFilePath := filepath.Join("./uploads/", user.ID, file.ID) 37 | infoFilePath := mainFilePath + ".info" 38 | 39 | err := os.Remove(mainFilePath) 40 | if err != nil { 41 | println(err.Error()) 42 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to delete file"}) 43 | return 44 | } 45 | 46 | // Delete the info file 47 | err = os.Remove(infoFilePath) 48 | if err != nil { 49 | println(err.Error()) 50 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to delete info file"}) 51 | return 52 | } 53 | 54 | } 55 | } 56 | 57 | user, err = prisma.Client().User.FindUnique( 58 | db.User.ID.Equals(Body.UserID), 59 | ).Delete().Exec(prisma.Context()) 60 | 61 | if err != nil { 62 | println(err.Error()) 63 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to delete user"}) 64 | } 65 | 66 | c.JSON(http.StatusOK, gin.H{"message": "success"}) 67 | return 68 | } 69 | -------------------------------------------------------------------------------- /server/handler/admin_files.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/AlandSleman/StorageBox/prisma" 7 | "github.com/AlandSleman/StorageBox/prisma/db" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func AdminFiles(c *gin.Context) { 12 | // Fetch the authenticated user 13 | userID := c.GetString("id") 14 | authUser, err := prisma.Client().User.FindUnique( 15 | db.User.ID.Equals(userID), 16 | ).Exec(prisma.Context()) 17 | if err != nil { 18 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch authenticated user"}) 19 | return 20 | } 21 | 22 | files, err := prisma.Client().File.FindMany().Exec(prisma.Context()) 23 | if err != nil { 24 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"}) 25 | return 26 | } 27 | 28 | // Obfuscate user data for privacy reasons, for non admin users 29 | if len(files) > 0 { 30 | if authUser.Role != "admin" { 31 | for i := range files { 32 | files[i].Name = "********" 33 | } 34 | } 35 | } 36 | 37 | // Return the list of users (with email addresses obfuscated and sensitive data modified if necessary) 38 | c.JSON(http.StatusOK, files) 39 | } 40 | -------------------------------------------------------------------------------- /server/handler/admin_overview.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/AlandSleman/StorageBox/prisma" 5 | "github.com/AlandSleman/StorageBox/prisma/db" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | func AdminOverview(c *gin.Context) { 11 | userID := c.GetString("id") 12 | 13 | _, err := prisma.Client().User.FindUnique( 14 | db.User.ID.Equals(userID), 15 | ).Exec(prisma.Context()) 16 | 17 | // Fetch the total storage size and file count 18 | var totalStorage int64 19 | var fileCount int 20 | 21 | // Fetch all users 22 | users, err := prisma.Client().User.FindMany().With(db.User.Files.Fetch()).Exec(prisma.Context()) 23 | if err != nil { 24 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"}) 25 | return 26 | } 27 | 28 | // Calculate total storage size and file count 29 | for _, user := range users { 30 | for _, file := range user.Files() { 31 | totalStorage += int64(file.Size) 32 | fileCount++ 33 | } 34 | } 35 | 36 | // Return the statistics 37 | c.JSON(http.StatusOK, gin.H{ 38 | "userCount": len(users), // Use the length of the users slice as the user count 39 | "totalStorage": totalStorage, // Total storage size in bytes 40 | "fileCount": fileCount, 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /server/handler/admin_users.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/AlandSleman/StorageBox/prisma" 7 | "github.com/AlandSleman/StorageBox/prisma/db" 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func AdminUsers(c *gin.Context) { 12 | // Fetch the authenticated user 13 | userID := c.GetString("id") 14 | authUser, err := prisma.Client().User.FindUnique( 15 | db.User.ID.Equals(userID), 16 | ).Exec(prisma.Context()) 17 | if err != nil { 18 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch authenticated user"}) 19 | return 20 | } 21 | 22 | users, err := prisma.Client().User.FindMany().With(db.User.Files.Fetch()).With(db.User.Folders.Fetch()).Exec(prisma.Context()) 23 | if err != nil { 24 | c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch users"}) 25 | return 26 | } 27 | 28 | // Obfuscate user data for privacy reasons, for non admin users 29 | if len(users) > 0 { 30 | if authUser.Role != "admin" { 31 | for i, user := range users { 32 | users[i].Email = "*******@example.com" 33 | users[i].Password = "********" 34 | for j := range user.Folders() { 35 | users[i].Folders()[j].Name = "***" 36 | } 37 | for j := range user.Files() { 38 | users[i].Files()[j].Name = "***" 39 | } 40 | } 41 | 42 | } 43 | 44 | } 45 | 46 | // Return the list of users (with email addresses obfuscated and sensitive data modified if necessary) 47 | c.JSON(http.StatusOK, users) 48 | } 49 | -------------------------------------------------------------------------------- /server/handler/delete_file.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/AlandSleman/StorageBox/prisma" 9 | "github.com/AlandSleman/StorageBox/prisma/db" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | func DeleteFile(c *gin.Context) { 14 | 15 | var Body struct { 16 | FileId string `json:"id" binding:"required"` 17 | } 18 | 19 | if err := c.ShouldBindJSON(&Body); err != nil { 20 | c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid input"}) 21 | return 22 | } 23 | 24 | userID := c.GetString("id") 25 | 26 | file, err := prisma.Client().File.FindFirst( 27 | db.File.And(db.File.UserID.Equals(userID), db.File.ID.Equals(Body.FileId)), 28 | ).Exec(prisma.Context()) 29 | 30 | if err != nil { 31 | println(err.Error()) 32 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to find file"}) 33 | return 34 | } 35 | 36 | // decrement the total Storage for user 37 | _, err = prisma.Client().User.FindUnique( 38 | db.User.ID.Equals(userID), 39 | ).Update(db.User.Storage.Decrement(file.Size)).Exec(prisma.Context()) 40 | 41 | if err != nil { 42 | println(err.Error()) 43 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to decrement Storage for user"}) 44 | return 45 | } 46 | _, err = prisma.Client().File.FindMany( 47 | db.File.And(db.File.UserID.Equals(userID), db.File.ID.Equals(Body.FileId)), 48 | ).Delete().Exec(prisma.Context()) 49 | 50 | if err != nil { 51 | println(err.Error()) 52 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to delete file"}) 53 | return 54 | } 55 | 56 | mainFilePath := filepath.Join("./uploads/", userID, Body.FileId) 57 | infoFilePath := mainFilePath + ".info" 58 | 59 | err = os.Remove(mainFilePath) 60 | if err != nil { 61 | println(err.Error()) 62 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to delete file"}) 63 | return 64 | } 65 | 66 | err = os.Remove(infoFilePath) 67 | if err != nil { 68 | println(err.Error()) 69 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to delete info file"}) 70 | return 71 | } 72 | 73 | folders, err := prisma.Client().Folder.FindMany( 74 | db.Folder.UserID.Equals(userID), 75 | ).Exec(prisma.Context()) 76 | files, err := prisma.Client().File.FindMany( 77 | db.File.UserID.Equals(userID), 78 | ).Exec(prisma.Context()) 79 | 80 | c.JSON(http.StatusOK, gin.H{"folders": folders, "files": files}) 81 | return 82 | } 83 | -------------------------------------------------------------------------------- /server/handler/delete_folder.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "net/http" 5 | "os" 6 | "path/filepath" 7 | 8 | "github.com/AlandSleman/StorageBox/prisma" 9 | "github.com/AlandSleman/StorageBox/prisma/db" 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | func DeleteFolder(c *gin.Context) { 14 | 15 | var Body struct { 16 | FolderID string `json:"id" binding:"required"` 17 | } 18 | 19 | if err := c.ShouldBindJSON(&Body); err != nil { 20 | c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid input"}) 21 | return 22 | } 23 | 24 | userID := c.GetString("id") 25 | 26 | folder, err := prisma.Client().Folder.FindFirst( 27 | db.Folder.And(db.Folder.UserID.Equals(userID), db.Folder.ID.Equals(Body.FolderID)), 28 | ).With(db.Folder.Files.Fetch()).Exec(prisma.Context()) 29 | 30 | if err != nil { 31 | println(err.Error()) 32 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to get folder"}) 33 | return 34 | } 35 | 36 | // delete all files associated with this folder 37 | for _, file := range folder.Files() { 38 | mainFilePath := filepath.Join("./uploads/", userID, file.ID) 39 | infoFilePath := mainFilePath + ".info" 40 | 41 | err := os.Remove(mainFilePath) 42 | 43 | if err != nil { 44 | println(err.Error()) 45 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to delete file"}) 46 | return 47 | } 48 | 49 | err = os.Remove(infoFilePath) 50 | if err != nil { 51 | println(err.Error()) 52 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to delete info file"}) 53 | return 54 | } 55 | } 56 | 57 | _, err = prisma.Client().Folder.FindMany( 58 | db.Folder.And(db.Folder.UserID.Equals(userID), db.Folder.ID.Equals(Body.FolderID)), 59 | ).Delete().Exec(prisma.Context()) 60 | 61 | if err != nil { 62 | println(err.Error()) 63 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to delete folder"}) 64 | return 65 | } 66 | 67 | // get latest folders and files 68 | folders, err := prisma.Client().Folder.FindMany( 69 | db.Folder.UserID.Equals(userID), 70 | ).Exec(prisma.Context()) 71 | 72 | files, err := prisma.Client().File.FindMany( 73 | db.File.UserID.Equals(userID), 74 | ).Exec(prisma.Context()) 75 | 76 | c.JSON(http.StatusOK, gin.H{"folders": folders, "files": files}) 77 | return 78 | } 79 | -------------------------------------------------------------------------------- /server/handler/files.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/AlandSleman/StorageBox/prisma" 5 | "github.com/AlandSleman/StorageBox/prisma/db" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | func UserData(c *gin.Context) { 11 | 12 | userID := c.GetString("id") 13 | 14 | user, err := prisma.Client().User.FindUnique( 15 | db.User.ID.Equals(userID), 16 | ).Exec(prisma.Context()) 17 | 18 | folders, err := prisma.Client().Folder.FindMany( 19 | db.Folder.UserID.Equals(userID), 20 | ).Exec(prisma.Context()) 21 | 22 | files, err := prisma.Client().File.FindMany( 23 | db.File.UserID.Equals(userID), 24 | ).Exec(prisma.Context()) 25 | 26 | if err != nil { 27 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to get user data"}) 28 | return 29 | } 30 | c.JSON(http.StatusOK, gin.H{"folders": folders, "files": files, "storage": user.Storage}) 31 | } 32 | -------------------------------------------------------------------------------- /server/handler/login.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/AlandSleman/StorageBox/auth" 9 | "github.com/AlandSleman/StorageBox/prisma" 10 | "github.com/AlandSleman/StorageBox/prisma/db" 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type LoginRequestBody struct { 15 | Username string `json:"username" binding:"required"` 16 | Password string `json:"password" binding:"required"` 17 | } 18 | 19 | func Login(c *gin.Context) { 20 | // Parse the request body 21 | var body LoginRequestBody 22 | if err := c.ShouldBindJSON(&body); err != nil { 23 | c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid input"}) 24 | return 25 | } 26 | 27 | username := strings.ReplaceAll(body.Username, " ", "") 28 | // Attempt to find the user by username 29 | user, err := prisma.Client().User.FindFirst( 30 | db.User.Username.Equals(username), 31 | ).Exec(prisma.Context()) 32 | 33 | if err != nil { 34 | if errors.Is(err, db.ErrNotFound) { 35 | // User not found, create a new user 36 | user, err = auth.CreateUserPassword(username, body.Password) 37 | if err != nil { 38 | println(err.Error()) 39 | c.JSON(http.StatusInternalServerError, gin.H{"message": "Server error"}) 40 | return 41 | } 42 | } else { 43 | println(err.Error()) 44 | c.JSON(http.StatusInternalServerError, gin.H{"message": "Error while finding user"}) 45 | return 46 | } 47 | } 48 | if user.Provider != "password" { 49 | c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid login"}) 50 | return 51 | } 52 | 53 | // Check if the provided password matches the stored hash 54 | if err := auth.CheckPassword(user.Password, body.Password); err != nil { 55 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid password"}) 56 | return 57 | } 58 | 59 | // Generate a JWT token upon successful authentication 60 | token, err := auth.GenerateJWTToken(user.ID,user.Role) 61 | if err != nil { 62 | c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to generate token"}) 63 | return 64 | } 65 | 66 | // Return the token to the client 67 | c.JSON(http.StatusOK, gin.H{"token": token}) 68 | } 69 | -------------------------------------------------------------------------------- /server/handler/new_folder.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/AlandSleman/StorageBox/prisma" 5 | "github.com/AlandSleman/StorageBox/prisma/db" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | var NewFolderBody struct { 11 | Name string `json:"name" binding:"required"` 12 | ParentID string `json:"parentId" binding:"required"` 13 | } 14 | 15 | func NewFolder(c *gin.Context) { 16 | 17 | if err := c.ShouldBindJSON(&NewFolderBody); err != nil { 18 | c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid input"}) 19 | return 20 | } 21 | 22 | userID := c.GetString("id") 23 | 24 | user, err := prisma.Client().User.FindFirst( 25 | db.User.ID.Equals(userID), 26 | ).With(db.User.Folders.Fetch()).Exec(prisma.Context()) 27 | if err != nil { 28 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to get user"}) 29 | return 30 | } 31 | 32 | userOwnsFolder := false 33 | for _, folder := range user.Folders() { 34 | if folder.ID == NewFolderBody.ParentID { 35 | userOwnsFolder = true 36 | break 37 | } 38 | } 39 | 40 | if userOwnsFolder == false { 41 | c.JSON(http.StatusBadRequest, gin.H{"message": "User doesn't own folder"}) 42 | return 43 | } 44 | 45 | folderName := NewFolderBody.Name 46 | 47 | _, err = prisma.Client().Folder.CreateOne( 48 | db.Folder.Name.Set(folderName), 49 | db.Folder.User.Link(db.User.ID.Equals(userID)), 50 | db.Folder.Parent.Link(db.Folder.ID.Equals(NewFolderBody.ParentID)), 51 | ).Exec(prisma.Context()) 52 | 53 | if err != nil { 54 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to create folder"}) 55 | return 56 | } 57 | 58 | folders, err := prisma.Client().Folder.FindMany( 59 | db.Folder.UserID.Equals(userID), 60 | ).Exec(prisma.Context()) 61 | 62 | files, err := prisma.Client().File.FindMany( 63 | db.File.UserID.Equals(userID), 64 | ).Exec(prisma.Context()) 65 | 66 | c.JSON(http.StatusOK, gin.H{"folders": folders, "files": files}) 67 | return 68 | } 69 | -------------------------------------------------------------------------------- /server/handler/session.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/AlandSleman/StorageBox/prisma" 5 | "github.com/AlandSleman/StorageBox/prisma/db" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | func Session(c *gin.Context) { 11 | 12 | userID := c.GetString("id") 13 | 14 | user, err := prisma.Client().User.FindFirst( 15 | db.User.ID.Equals(userID), 16 | ).Exec(prisma.Context()) 17 | 18 | if err != nil { 19 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to get user"}) 20 | return 21 | } 22 | 23 | c.JSON(http.StatusOK, user) 24 | } 25 | -------------------------------------------------------------------------------- /server/handler/update_file.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/AlandSleman/StorageBox/prisma" 5 | "github.com/AlandSleman/StorageBox/prisma/db" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | func UpdateFile(c *gin.Context) { 11 | 12 | var Body struct { 13 | Name string `json:"name" binding:"required"` 14 | FileId string `json:"id" binding:"required"` 15 | } 16 | 17 | if err := c.ShouldBindJSON(&Body); err != nil { 18 | c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid input"}) 19 | return 20 | } 21 | 22 | userID := c.GetString("id") 23 | 24 | _, err := prisma.Client().File.FindMany( 25 | db.File.And(db.File.UserID.Equals(userID), db.File.ID.Equals(Body.FileId)), 26 | ).Update(db.File.Name.Set(Body.Name)).Exec(prisma.Context()) 27 | 28 | if err != nil { 29 | println(err.Error()) 30 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to update file"}) 31 | return 32 | } 33 | 34 | folders, err := prisma.Client().Folder.FindMany( 35 | db.Folder.UserID.Equals(userID), 36 | ).Exec(prisma.Context()) 37 | files, err := prisma.Client().File.FindMany( 38 | db.File.UserID.Equals(userID), 39 | ).Exec(prisma.Context()) 40 | 41 | c.JSON(http.StatusOK, gin.H{"folders": folders, "files": files}) 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /server/handler/update_folder.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/AlandSleman/StorageBox/prisma" 5 | "github.com/AlandSleman/StorageBox/prisma/db" 6 | "github.com/gin-gonic/gin" 7 | "net/http" 8 | ) 9 | 10 | func UpdateFolder(c *gin.Context) { 11 | 12 | var Body struct { 13 | Name string `json:"name" binding:"required"` 14 | FileId string `json:"id" binding:"required"` 15 | } 16 | 17 | if err := c.ShouldBindJSON(&Body); err != nil { 18 | c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid input"}) 19 | return 20 | } 21 | 22 | userID := c.GetString("id") 23 | 24 | _, err := prisma.Client().Folder.FindMany( 25 | db.Folder.And(db.Folder.UserID.Equals(userID), db.Folder.ID.Equals(Body.FileId)), 26 | ).Update(db.Folder.Name.Set(Body.Name)).Exec(prisma.Context()) 27 | 28 | if err != nil { 29 | println(err.Error()) 30 | c.JSON(http.StatusBadRequest, gin.H{"message": "Failed to update file"}) 31 | return 32 | } 33 | 34 | folders, err := prisma.Client().Folder.FindMany( 35 | db.Folder.UserID.Equals(userID), 36 | ).Exec(prisma.Context()) 37 | files, err := prisma.Client().File.FindMany( 38 | db.File.UserID.Equals(userID), 39 | ).Exec(prisma.Context()) 40 | 41 | c.JSON(http.StatusOK, gin.H{"folders": folders, "files": files}) 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /server/handler/user_settings.go: -------------------------------------------------------------------------------- 1 | package handler 2 | 3 | import ( 4 | "github.com/AlandSleman/StorageBox/auth" 5 | "github.com/AlandSleman/StorageBox/prisma" 6 | "github.com/AlandSleman/StorageBox/prisma/db" 7 | "github.com/gin-gonic/gin" 8 | "golang.org/x/crypto/bcrypt" 9 | "net/http" 10 | ) 11 | 12 | type PasswordChangeRequest struct { 13 | CurrentPassword string `json:"currentPassword"` 14 | NewPassword string `json:"newPassword"` 15 | } 16 | 17 | func UserSettings(c *gin.Context) { 18 | var body PasswordChangeRequest 19 | 20 | if err := c.ShouldBindJSON(&body); err != nil { 21 | c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid input"}) 22 | return 23 | } 24 | 25 | userID := c.GetString("id") 26 | 27 | user, err := prisma.Client().User.FindUnique( 28 | db.User.ID.Equals(userID), 29 | ).Exec(prisma.Context()) 30 | 31 | if err != nil { 32 | c.JSON(http.StatusNotFound, gin.H{"message": "User not found"}) 33 | return 34 | } 35 | 36 | if user.Provider != "password" { 37 | c.JSON(http.StatusBadRequest, gin.H{"message": "Invalid request"}) 38 | return 39 | } 40 | 41 | 42 | // Verify if the current password matches the user's current hashed password 43 | if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(body.CurrentPassword)); err != nil { 44 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid password"}) 45 | return 46 | } 47 | 48 | // Hash the new password 49 | hashedPassword, err := auth.HashPassword(body.NewPassword) 50 | if err != nil { 51 | c.JSON(http.StatusInternalServerError, gin.H{"message": "Server error"}) 52 | return 53 | } 54 | 55 | // Save the updated user back to the database (implement this function) 56 | user, err = prisma.Client().User.FindUnique( 57 | db.User.ID.Equals(userID), 58 | ).Update(db.User.Password.Set(hashedPassword)).Exec(prisma.Context()) 59 | 60 | if err != nil { 61 | c.JSON(http.StatusInternalServerError, gin.H{"message": "Failed to update password"}) 62 | return 63 | } 64 | 65 | c.JSON(http.StatusOK, gin.H{"message": "Password updated successfully"}) 66 | } 67 | -------------------------------------------------------------------------------- /server/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | 8 | "github.com/AlandSleman/StorageBox/auth" 9 | "github.com/AlandSleman/StorageBox/handler" 10 | "github.com/AlandSleman/StorageBox/middleware" 11 | "github.com/AlandSleman/StorageBox/prisma" 12 | "github.com/AlandSleman/StorageBox/prisma/db" 13 | "github.com/gin-gonic/gin" 14 | "github.com/joho/godotenv" 15 | ) 16 | 17 | func loadEnvVariables() { 18 | err := godotenv.Load() 19 | if err != nil { 20 | fmt.Println("Error loading .env file") 21 | os.Exit(1) 22 | } 23 | } 24 | 25 | // create admin user 26 | func seedDb() { 27 | pass, _ := auth.HashPassword("admin") 28 | _, _ = prisma.Client().User.CreateOne(db.User.Username.Set("admin"), 29 | db.User.Provider.Set("password"), 30 | db.User.Role.Set("admin"), 31 | db.User.Password.Set(pass)).Exec(prisma.Context()) 32 | } 33 | 34 | func main() { 35 | loadEnvVariables() 36 | r := gin.Default() 37 | r.Use(middleware.Cors()) 38 | 39 | r.Use(middleware.RateLimit()) 40 | 41 | r.GET("/", func(c *gin.Context) { 42 | c.JSON(http.StatusOK, gin.H{ 43 | "message": "hello", 44 | }) 45 | }) 46 | 47 | prisma.Init() 48 | seedDb() 49 | 50 | r.POST("/login", handler.Login) 51 | 52 | r.GET("/auth/github/callback", auth.Github) 53 | r.GET("/auth/discord/callback", auth.Discord) 54 | r.GET("/auth/google/callback", auth.Google) 55 | 56 | r.Use(middleware.Auth) 57 | 58 | r.PUT("/user", handler.UserSettings) 59 | 60 | r.GET("/session", handler.Session) 61 | r.POST("/folder", handler.NewFolder) 62 | 63 | r.PATCH("/file", handler.UpdateFile) 64 | r.PATCH("/folder", handler.UpdateFolder) 65 | r.DELETE("/folder", handler.DeleteFolder) 66 | r.DELETE("/file", handler.DeleteFile) 67 | 68 | r.GET("/data", handler.UserData) 69 | 70 | r.HEAD("/files/:id", handler.HeadHandler) 71 | r.GET("/files/:id", handler.GetHandler) 72 | r.POST("/files/", middleware.DirExists, handler.PostHandler) 73 | r.PATCH("/files/:id", middleware.DirExists, handler.PatchHandler) 74 | 75 | //admin routes 76 | r.GET("/admin/overview", handler.AdminOverview) 77 | r.GET("/admin/users", handler.AdminUsers) 78 | r.GET("/admin/files", handler.AdminFiles) 79 | r.Use(middleware.AuthAdmin) 80 | r.DELETE("/admin/user", handler.AdminDeleteUser) 81 | // TODO after deleting a file decrement the user Storage limit 82 | r.DELETE("/admin/user-files", handler.AdminDeleteAllUserFiles) 83 | r.DELETE("/admin/file", handler.AdminDeleteFile) 84 | 85 | fmt.Println("Listening at :4000") 86 | if err := r.Run(":4000"); err != nil { 87 | panic(fmt.Errorf("Unable to start Gin server: %s", err)) 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /server/middleware/auth.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/AlandSleman/StorageBox/config" 9 | "github.com/gin-gonic/gin" 10 | "github.com/golang-jwt/jwt/v5" 11 | ) 12 | 13 | 14 | func Auth(c *gin.Context) { 15 | authHeader := c.GetHeader("Authorization") 16 | 17 | var tokenString string 18 | tokenQuery := c.Query("token") 19 | 20 | if tokenQuery != "" { 21 | tokenString = tokenQuery 22 | 23 | } else { 24 | if authHeader == "" { 25 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Missing Authorization header"}) 26 | return 27 | } 28 | // Check if the Authorization header has the "Bearer" prefix 29 | if !strings.HasPrefix(authHeader, "Bearer ") { 30 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid Authorization header format"}) 31 | c.Abort() 32 | return 33 | } 34 | tokenString = authHeader[7:] 35 | } 36 | 37 | // Parse the JWT token 38 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 39 | // Validate the signing method 40 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 41 | return nil, fmt.Errorf("Invalid signing method") 42 | } 43 | // Provide the secret key to validate the token 44 | return []byte(config.GetConfig().JWT_SECRET), nil 45 | }) 46 | 47 | if err != nil { 48 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid token"}) 49 | println(err.Error()) 50 | c.Abort() 51 | return 52 | } 53 | 54 | // Check if the token is valid 55 | if !token.Valid { 56 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Token is not valid"}) 57 | c.Abort() 58 | return 59 | } 60 | 61 | // set the user id and role in ctx 62 | if claims, ok := token.Claims.(jwt.MapClaims); ok { 63 | if id, exists := claims["id"].(string); exists { 64 | c.Set("id", id) 65 | } 66 | if role, exists := claims["role"].(string); exists { 67 | c.Set("role", role) 68 | } 69 | } 70 | 71 | // Continue with the request 72 | c.Next() 73 | } 74 | -------------------------------------------------------------------------------- /server/middleware/auth_admin.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strings" 7 | 8 | "github.com/AlandSleman/StorageBox/config" 9 | "github.com/AlandSleman/StorageBox/prisma" 10 | "github.com/AlandSleman/StorageBox/prisma/db" 11 | "github.com/gin-gonic/gin" 12 | "github.com/golang-jwt/jwt/v5" 13 | ) 14 | 15 | func AuthAdmin(c *gin.Context) { 16 | authHeader := c.GetHeader("Authorization") 17 | 18 | var tokenString string 19 | tokenQuery := c.Query("token") 20 | 21 | if tokenQuery != "" { 22 | tokenString = tokenQuery 23 | 24 | } else { 25 | if authHeader == "" { 26 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Missing Authorization header"}) 27 | return 28 | } 29 | // Check if the Authorization header has the "Bearer" prefix 30 | if !strings.HasPrefix(authHeader, "Bearer ") { 31 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid Authorization header format"}) 32 | c.Abort() 33 | return 34 | } 35 | tokenString = authHeader[7:] 36 | } 37 | 38 | // Parse the JWT token 39 | token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { 40 | // Validate the signing method 41 | if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok { 42 | return nil, fmt.Errorf("Invalid signing method") 43 | } 44 | // Provide the secret key to validate the token 45 | return []byte(config.GetConfig().JWT_SECRET), nil 46 | }) 47 | 48 | if err != nil { 49 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Invalid token"}) 50 | println(err.Error()) 51 | return 52 | } 53 | 54 | // Check if the token is valid 55 | if !token.Valid { 56 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Token is not valid"}) 57 | return 58 | } 59 | 60 | // set the user id and role in ctx 61 | if claims, ok := token.Claims.(jwt.MapClaims); ok { 62 | 63 | if id, exists := claims["id"].(string); exists { 64 | 65 | user, err := prisma.Client().User.FindUnique( 66 | db.User.ID.Equals(claims["id"].(string)), 67 | ).Exec(prisma.Context()) 68 | if user.Role != "admin" { 69 | 70 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"}) 71 | c.Abort() 72 | return 73 | } 74 | 75 | if err != nil { 76 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"}) 77 | c.Abort() 78 | return 79 | } 80 | 81 | c.Set("id", id) 82 | } else { 83 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Unauthorized"}) 84 | c.Abort() 85 | return 86 | } 87 | if role, exists := claims["role"].(string); exists { 88 | c.Set("role", role) 89 | } 90 | } 91 | 92 | // Continue with the request 93 | c.Next() 94 | } 95 | -------------------------------------------------------------------------------- /server/middleware/cors.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-contrib/cors" 5 | "github.com/gin-gonic/gin" 6 | ) 7 | 8 | func Cors() gin.HandlerFunc { 9 | return cors.New(cors.Config{ 10 | AllowAllOrigins: true, 11 | AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}, 12 | AllowHeaders: []string{ 13 | "Authorization", "X-Requested-With", "X-Request-ID", "X-HTTP-Method-Override","Content-Type", 14 | "Upload-Length", "Upload-Offset", "Tus-Resumable", "Upload-Metadata", "Upload-Defer-Length", 15 | "Upload-Concat", "User-Agent", "Referrer", "Origin", "Content-Type", "Content-Length", "dir", "id", "token", // Include "dir" header 16 | }, 17 | ExposeHeaders: []string{ 18 | "Upload-Offset", "Location", "Upload-Length", "Tus-Version", "Tus-Resumable", "Tus-Max-Size", 19 | "Tus-Extension", "Upload-Metadata", "Upload-Defer-Length", "Upload-Concat", "Location", "Upload-Offset", "Upload-Length", 20 | }, 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /server/middleware/dir.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | "net/http" 6 | ) 7 | 8 | func DirExists(c *gin.Context) { 9 | // ensuring that the dir header is present 10 | dir := c.GetHeader("dir") 11 | if dir == "" { 12 | c.JSON(http.StatusUnauthorized, gin.H{"message": "Missing dir header"}) 13 | return 14 | } 15 | c.Set("dir", dir) 16 | 17 | if dir == "/" { 18 | c.Set("avatar", "true") 19 | } 20 | 21 | c.Next() 22 | } 23 | -------------------------------------------------------------------------------- /server/middleware/rate_limit.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/AlandSleman/StorageBox/config" 8 | "github.com/gin-gonic/gin" 9 | "github.com/redis/go-redis/v9" 10 | limiter "github.com/ulule/limiter/v3" 11 | mgin "github.com/ulule/limiter/v3/drivers/middleware/gin" 12 | sredis "github.com/ulule/limiter/v3/drivers/store/redis" 13 | ) 14 | 15 | func RateLimit() gin.HandlerFunc { 16 | rate, err := limiter.NewRateFromFormatted("85-M") 17 | if err != nil { 18 | log.Fatal(err) 19 | os.Exit(1) 20 | } 21 | 22 | // Create a redis client. 23 | option, err := redis.ParseURL(config.GetConfig().REDIS_URL) 24 | if err != nil { 25 | log.Fatal("errris:", err) 26 | os.Exit(1) 27 | } 28 | client := redis.NewClient(option) 29 | 30 | // Create a store with the redis client. 31 | store, err := sredis.NewStoreWithOptions(client, limiter.StoreOptions{ 32 | Prefix: "limiter", 33 | MaxRetry: 3, 34 | }) 35 | if err != nil { 36 | log.Fatal(err) 37 | os.Exit(1) 38 | } 39 | 40 | // Create a new middleware with the limiter instance. 41 | return mgin.NewMiddleware(limiter.New(store, rate)) 42 | } 43 | -------------------------------------------------------------------------------- /server/prisma/db/.gitignore: -------------------------------------------------------------------------------- 1 | # gitignore generated by Prisma Client Go. DO NOT EDIT. 2 | *_gen.go 3 | -------------------------------------------------------------------------------- /server/prisma/migrations/20230904184641_h/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL, 4 | "username" TEXT NOT NULL, 5 | "email" TEXT, 6 | "password" TEXT, 7 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updated_at" TIMESTAMP(3) NOT NULL, 9 | 10 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 11 | ); 12 | 13 | -- CreateTable 14 | CREATE TABLE "Folder" ( 15 | "id" TEXT NOT NULL, 16 | "name" TEXT NOT NULL, 17 | "userId" TEXT NOT NULL, 18 | "parentId" TEXT, 19 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 20 | "updated_at" TIMESTAMP(3) NOT NULL, 21 | 22 | CONSTRAINT "Folder_pkey" PRIMARY KEY ("id") 23 | ); 24 | 25 | -- CreateTable 26 | CREATE TABLE "File" ( 27 | "id" TEXT NOT NULL, 28 | "name" TEXT NOT NULL, 29 | "type" TEXT NOT NULL, 30 | "userId" TEXT NOT NULL, 31 | "folderId" TEXT NOT NULL, 32 | "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 33 | "updated_at" TIMESTAMP(3) NOT NULL, 34 | 35 | CONSTRAINT "File_pkey" PRIMARY KEY ("id") 36 | ); 37 | 38 | -- CreateIndex 39 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username"); 40 | 41 | -- CreateIndex 42 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 43 | 44 | -- AddForeignKey 45 | ALTER TABLE "Folder" ADD CONSTRAINT "Folder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 46 | 47 | -- AddForeignKey 48 | ALTER TABLE "Folder" ADD CONSTRAINT "Folder_parentId_fkey" FOREIGN KEY ("parentId") REFERENCES "Folder"("id") ON DELETE SET NULL ON UPDATE CASCADE; 49 | 50 | -- AddForeignKey 51 | ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 52 | 53 | -- AddForeignKey 54 | ALTER TABLE "File" ADD CONSTRAINT "File_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 55 | -------------------------------------------------------------------------------- /server/prisma/migrations/20230905141316_path/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `path` to the `Folder` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Folder" ADD COLUMN "path" TEXT NOT NULL; 9 | -------------------------------------------------------------------------------- /server/prisma/migrations/20230905184155_size/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `size` to the `File` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "File" ADD COLUMN "size" INTEGER NOT NULL; 9 | -------------------------------------------------------------------------------- /server/prisma/migrations/20230906072639_user_path/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `path` on the `Folder` table. All the data in the column will be lost. 5 | - A unique constraint covering the columns `[path]` on the table `User` will be added. If there are existing duplicate values, this will fail. 6 | - Added the required column `path` to the `User` table without a default value. This is not possible if the table is not empty. 7 | - Added the required column `provider` to the `User` table without a default value. This is not possible if the table is not empty. 8 | 9 | */ 10 | -- AlterTable 11 | ALTER TABLE "Folder" DROP COLUMN "path"; 12 | 13 | -- AlterTable 14 | ALTER TABLE "User" ADD COLUMN "path" TEXT NOT NULL, 15 | ADD COLUMN "provider" TEXT NOT NULL; 16 | 17 | -- CreateIndex 18 | CREATE UNIQUE INDEX "User_path_key" ON "User"("path"); 19 | -------------------------------------------------------------------------------- /server/prisma/migrations/20230906092845_rm_path/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the column `path` on the `User` table. All the data in the column will be lost. 5 | 6 | */ 7 | -- DropIndex 8 | DROP INDEX "User_path_key"; 9 | 10 | -- AlterTable 11 | ALTER TABLE "User" DROP COLUMN "path"; 12 | -------------------------------------------------------------------------------- /server/prisma/migrations/20230908025236_del/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "File" DROP CONSTRAINT "File_folderId_fkey"; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "File" ADD CONSTRAINT "File_folderId_fkey" FOREIGN KEY ("folderId") REFERENCES "Folder"("id") ON DELETE CASCADE ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /server/prisma/migrations/20230912102841_size_bigint/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "File" ALTER COLUMN "size" SET DATA TYPE BIGINT; 3 | 4 | -- AlterTable 5 | ALTER TABLE "User" ADD COLUMN "storage" BIGINT NOT NULL DEFAULT 0; 6 | -------------------------------------------------------------------------------- /server/prisma/migrations/20230914105609_add_role/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "role" TEXT NOT NULL DEFAULT 'user'; 3 | -------------------------------------------------------------------------------- /server/prisma/migrations/20230922225200_/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "avatar" TEXT NOT NULL DEFAULT ''; 3 | -------------------------------------------------------------------------------- /server/prisma/migrations/20230929130803_/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Made the column `email` on table `User` required. This step will fail if there are existing NULL values in that column. 5 | - Made the column `password` on table `User` required. This step will fail if there are existing NULL values in that column. 6 | 7 | */ 8 | -- DropForeignKey 9 | ALTER TABLE "Folder" DROP CONSTRAINT "Folder_userId_fkey"; 10 | 11 | -- AlterTable 12 | ALTER TABLE "User" ALTER COLUMN "email" SET NOT NULL, 13 | ALTER COLUMN "email" SET DEFAULT '', 14 | ALTER COLUMN "password" SET NOT NULL, 15 | ALTER COLUMN "password" SET DEFAULT ''; 16 | 17 | -- AddForeignKey 18 | ALTER TABLE "Folder" ADD CONSTRAINT "Folder_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 19 | -------------------------------------------------------------------------------- /server/prisma/migrations/20230929154118_/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropIndex 2 | DROP INDEX "User_email_key"; 3 | -------------------------------------------------------------------------------- /server/prisma/migrations/20230929154603_/migration.sql: -------------------------------------------------------------------------------- 1 | -- DropForeignKey 2 | ALTER TABLE "File" DROP CONSTRAINT "File_userId_fkey"; 3 | 4 | -- AddForeignKey 5 | ALTER TABLE "File" ADD CONSTRAINT "File_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 6 | -------------------------------------------------------------------------------- /server/prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /server/prisma/prisma.go: -------------------------------------------------------------------------------- 1 | package prisma 2 | 3 | import ( 4 | "context" 5 | "sync" 6 | 7 | "github.com/AlandSleman/StorageBox/prisma/db" 8 | ) 9 | 10 | var ( 11 | once sync.Once 12 | prisma *db.PrismaClient 13 | ctx context.Context 14 | err error 15 | ) 16 | 17 | // Init initializes the Prisma client once. 18 | func Init() { 19 | once.Do(func() { 20 | prisma = db.NewClient() 21 | ctx = context.Background() 22 | 23 | if err = prisma.Prisma.Connect(); err != nil { 24 | panic(err) 25 | } 26 | }) 27 | } 28 | 29 | // Client returns the initialized Prisma client. 30 | func Client() *db.PrismaClient { 31 | return prisma 32 | } 33 | 34 | // Context returns the context used for Prisma queries. 35 | func Context() context.Context { 36 | return ctx 37 | } 38 | -------------------------------------------------------------------------------- /server/prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | datasource db { 2 | provider = "postgresql" 3 | url = env("DATABASE_URL") 4 | } 5 | 6 | generator db { 7 | provider = "go run github.com/steebchen/prisma-client-go" 8 | } 9 | 10 | model User { 11 | id String @id @default(uuid()) 12 | username String @unique 13 | avatar String @default("") 14 | role String @default("user") 15 | email String @default("") 16 | password String @default("") 17 | provider String 18 | storage BigInt @default(0) 19 | folders Folder[] 20 | files File[] 21 | createdAt DateTime @default(now()) @map("created_at") 22 | updatedAt DateTime @updatedAt @map("updated_at") 23 | } 24 | 25 | model Folder { 26 | id String @id @default(uuid()) 27 | name String 28 | userId String 29 | parentId String? 30 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 31 | parent Folder? @relation("SubFolders", fields: [parentId], references: [id]) 32 | subFolders Folder[] @relation("SubFolders") 33 | files File[] 34 | createdAt DateTime @default(now()) @map("created_at") 35 | updatedAt DateTime @updatedAt @map("updated_at") 36 | } 37 | 38 | model File { 39 | id String @id 40 | name String 41 | type String 42 | size BigInt 43 | userId String 44 | folderId String 45 | user User @relation(fields: [userId], references: [id], onDelete:Cascade) 46 | folder Folder @relation(fields: [folderId], references: [id], onDelete: Cascade) 47 | createdAt DateTime @default(now()) @map("created_at") 48 | updatedAt DateTime @updatedAt @map("updated_at") 49 | } 50 | -------------------------------------------------------------------------------- /server/utils/randomPath.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | "strings" 6 | ) 7 | 8 | func RandomPath() string { 9 | uuidObj, _ := uuid.NewRandom() 10 | path := uuidObj.String() 11 | path = removeHyphens(path) 12 | return path 13 | } 14 | func removeHyphens(input string) string { 15 | return strings.Replace(input, "-", "", -1) 16 | } 17 | -------------------------------------------------------------------------------- /website/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .next/ 3 | -------------------------------------------------------------------------------- /website/.env.example: -------------------------------------------------------------------------------- 1 | SERVER_URL=http://localhost:4000 2 | LOCAL_SERVER_URL=http://server:4000 3 | 4 | GRAFANA_URL=http://localhost:3000 5 | 6 | GITHUB_CLIENT_ID= 7 | GITHUB_REDIRECT_URI=http://localhost:4001/auth/github 8 | 9 | DISCORD_CLIENT_ID= 10 | DISCORD_REDIRECT_URI=http://localhost:4001/auth/discord 11 | 12 | GOOGLE_CLIENT_ID= 13 | GOOGLE_REDIRECT_URI=http://localhost:4001/auth/google 14 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | .env 4 | # dependencies 5 | node_modules 6 | .pnp 7 | .pnp.js 8 | 9 | # testing 10 | coverage 11 | 12 | # next.js 13 | .next/ 14 | out/ 15 | build 16 | 17 | # misc 18 | .DS_Store 19 | *.pem 20 | 21 | # debug 22 | npm-debug.log* 23 | yarn-debug.log* 24 | yarn-error.log* 25 | .pnpm-debug.log* 26 | 27 | # local env files 28 | .env.local 29 | .env.development.local 30 | .env.test.local 31 | .env.production.local 32 | 33 | # turbo 34 | .turbo 35 | 36 | .contentlayer 37 | .env 38 | -------------------------------------------------------------------------------- /website/.prettierignore: -------------------------------------------------------------------------------- 1 | cache 2 | .cache 3 | package.json 4 | package-lock.json 5 | public 6 | CHANGELOG.md 7 | .yarn 8 | dist 9 | node_modules 10 | .next 11 | build 12 | .contentlayer -------------------------------------------------------------------------------- /website/Dockerfile: -------------------------------------------------------------------------------- 1 | # Use an official Node.js runtime as a parent image 2 | FROM node:20 3 | 4 | # Set the working directory in the container 5 | WORKDIR /app 6 | 7 | # Copy package.json and package-lock.json to the working directory 8 | COPY . . 9 | 10 | # Install project dependencies 11 | RUN npm i -g pnpm 12 | RUN pnpm install 13 | 14 | 15 | # Build your Next.js app 16 | RUN pnpm build 17 | 18 | # Expose the port that your Next.js app will run on 19 | EXPOSE 4001 20 | 21 | # Define the command to run your Next.js app 22 | CMD ["pnpm", "start"] 23 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # next-template 2 | 3 | A Next.js 13 template for building apps with Radix UI and Tailwind CSS. 4 | 5 | ## Usage 6 | 7 | ```bash 8 | npx create-next-app -e https://github.com/shadcn/next-template 9 | ``` 10 | 11 | ## Features 12 | 13 | - Next.js 13 App Directory 14 | - Radix UI Primitives 15 | - Tailwind CSS 16 | - Icons from [Lucide](https://lucide.dev) 17 | - Dark mode with `next-themes` 18 | - Tailwind CSS class sorting, merging and linting. 19 | 20 | ## License 21 | 22 | Licensed under the [MIT license](https://github.com/shadcn/ui/blob/main/LICENSE.md). 23 | -------------------------------------------------------------------------------- /website/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "tailwind": { 5 | "config": "tailwind.config.js", 6 | "css": "src/app/globals.css", 7 | "baseColor": "slate", 8 | "cssVariables": true 9 | }, 10 | "rsc": false, 11 | "aliases": { 12 | "utils": "@/lib/utils", 13 | "components": "@/components" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /website/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/basic-features/typescript for more information. 7 | -------------------------------------------------------------------------------- /website/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | serverActions: true 5 | }, 6 | env: { 7 | SERVER_URL: process.env.SERVER_URL, 8 | LOCAL_SERVER_URL: process.env.LOCAL_SERVER_URL, 9 | GITHUB_CLIENT_ID: process.env.GITHUB_CLIENT_ID, 10 | GRAFANA_URL: process.env.GRAFANA_URL, 11 | GITHUB_REDIRECT_URI: process.env.GITHUB_REDIRECT_URI, 12 | DISCORD_CLIENT_ID: process.env.DISCORD_CLIENT_ID, 13 | DISCORD_REDIRECT_URI: process.env.DISCORD_REDIRECT_URI, 14 | GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID, 15 | GOOGLE_REDIRECT_URI: process.env.GOOGLE_REDIRECT_URI 16 | } 17 | }; 18 | 19 | const withNextra = require("nextra")({ 20 | theme: "nextra-theme-docs", 21 | themeConfig: "./theme.config.tsx", 22 | }); 23 | 24 | // module.exports = withNextra(nextConfig); 25 | 26 | const withBundleAnalyzer = require('@next/bundle-analyzer')({ 27 | enabled: process.env.ANALYZE === 'true', 28 | }) 29 | module.exports = withBundleAnalyzer(withNextra(nextConfig)) 30 | -------------------------------------------------------------------------------- /website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-template", 3 | "version": "0.0.2", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev -p 4001", 7 | "build": "next build", 8 | "start": "next start -p 4001", 9 | "lint": "next lint", 10 | "lint:fix": "next lint --fix", 11 | "preview": "next build && next start", 12 | "typecheck": "tsc --noEmit" 13 | }, 14 | "dependencies": { 15 | "@hookform/resolvers": "^3.3.1", 16 | "@nanostores/react": "^0.7.1", 17 | "@next/bundle-analyzer": "^13.5.2", 18 | "@radix-ui/react-avatar": "^1.0.3", 19 | "@radix-ui/react-dialog": "^1.0.4", 20 | "@radix-ui/react-dropdown-menu": "^2.0.5", 21 | "@radix-ui/react-label": "^2.0.2", 22 | "@radix-ui/react-popover": "^1.0.6", 23 | "@radix-ui/react-progress": "^1.0.3", 24 | "@radix-ui/react-select": "^1.2.2", 25 | "@radix-ui/react-separator": "^1.0.3", 26 | "@radix-ui/react-slot": "^1.0.2", 27 | "@tanstack/react-query": "^4.33.0", 28 | "@uppy/core": "^3.5.0", 29 | "@uppy/dashboard": "^3.5.2", 30 | "@uppy/drag-drop": "^3.0.3", 31 | "@uppy/file-input": "^3.0.3", 32 | "@uppy/progress-bar": "^3.0.3", 33 | "@uppy/react": "^3.1.3", 34 | "@uppy/tus": "^3.2.0", 35 | "@vidstack/react": "^0.6.13", 36 | "axios": "^1.5.0", 37 | "build": "^0.1.4", 38 | "class-variance-authority": "^0.4.0", 39 | "clsx": "^1.2.1", 40 | "js-cookie": "^3.0.5", 41 | "jsonwebtoken": "^9.0.2", 42 | "lucide-react": "0.279.0", 43 | "moment": "^2.29.4", 44 | "nanostores": "^0.9.3", 45 | "next": "^13.4.8", 46 | "nextra": "latest", 47 | "nextra-theme-docs": "latest", 48 | "next-themes": "^0.2.1", 49 | "pnpm": "^8.8.0", 50 | "react": "^18.2.0", 51 | "react-dom": "^18.2.0", 52 | "react-hook-form": "^7.46.1", 53 | "react-toastify": "^9.1.3", 54 | "sharp": "^0.31.3", 55 | "tailwind-merge": "^1.13.2", 56 | "tailwindcss-animate": "^1.0.6", 57 | "vidstack": "^0.6.13", 58 | "zod": "^3.21.4" 59 | }, 60 | "devDependencies": { 61 | "@ianvs/prettier-plugin-sort-imports": "4.1.0", 62 | "@types/js-cookie": "^3.0.3", 63 | "@types/jsonwebtoken": "^9.0.2", 64 | "@types/node": "^17.0.45", 65 | "@types/react": "^18.2.14", 66 | "@types/react-dom": "^18.2.6", 67 | "autoprefixer": "^10.4.14", 68 | "postcss": "^8.4.24", 69 | "prettier": "^3.0.3", 70 | "tailwindcss": "^3.3.2", 71 | "typescript": "^4.9.5" 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /website/postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /website/prettier.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('prettier').Config} */ 2 | module.exports = { 3 | endOfLine: "lf", 4 | semi: false, 5 | singleQuote: false, 6 | tabWidth: 2, 7 | trailingComma: "es5", 8 | importOrder: [ 9 | "^(react/(.*)$)|^(react$)", 10 | "^(next/(.*)$)|^(next$)", 11 | "", 12 | "", 13 | "^types$", 14 | "^@/types/(.*)$", 15 | "^@/config/(.*)$", 16 | "^@/lib/(.*)$", 17 | "^@/hooks/(.*)$", 18 | "^@/components/ui/(.*)$", 19 | "^@/components/(.*)$", 20 | "^@/styles/(.*)$", 21 | "^@/app/(.*)$", 22 | "", 23 | "^[./]", 24 | ], 25 | importOrderSeparation: false, 26 | importOrderSortSpecifiers: true, 27 | importOrderBuiltinModulesToTop: true, 28 | importOrderParserPlugins: ["typescript", "jsx", "decorators-legacy"], 29 | importOrderMergeDuplicateImports: true, 30 | importOrderCombineTypeAndValueImports: true, 31 | plugins: ["@ianvs/prettier-plugin-sort-imports"], 32 | } 33 | -------------------------------------------------------------------------------- /website/public/admin-dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/StorageBox/e3aa0702c0e4576858636fff6c9894c3030bcab4/website/public/admin-dashboard.png -------------------------------------------------------------------------------- /website/public/avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/StorageBox/e3aa0702c0e4576858636fff6c9894c3030bcab4/website/public/avatar.png -------------------------------------------------------------------------------- /website/public/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/StorageBox/e3aa0702c0e4576858636fff6c9894c3030bcab4/website/public/dashboard.png -------------------------------------------------------------------------------- /website/public/doge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/StorageBox/e3aa0702c0e4576858636fff6c9894c3030bcab4/website/public/doge.png -------------------------------------------------------------------------------- /website/public/elon.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/StorageBox/e3aa0702c0e4576858636fff6c9894c3030bcab4/website/public/elon.jpeg -------------------------------------------------------------------------------- /website/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/StorageBox/e3aa0702c0e4576858636fff6c9894c3030bcab4/website/public/favicon.ico -------------------------------------------------------------------------------- /website/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KryptXBSA/StorageBox/e3aa0702c0e4576858636fff6c9894c3030bcab4/website/public/favicon.png -------------------------------------------------------------------------------- /website/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/public/thirteen.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /website/src/api/adminFiles.ts: -------------------------------------------------------------------------------- 1 | import { File} from "@/types" 2 | 3 | import { makeRequest } from "./makeRequest" 4 | 5 | type Res = File[] 6 | export async function adminFiles() { 7 | let data: Res = await makeRequest("/admin/files", "get") 8 | return data 9 | } 10 | -------------------------------------------------------------------------------- /website/src/api/adminOverview.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest } from "./makeRequest" 2 | 3 | export interface Res { 4 | fileCount: number 5 | totalStorage: number 6 | userCount: number 7 | } 8 | export async function adminOverview() { 9 | let data: Res = await makeRequest("/admin/overview", "get") 10 | return data 11 | } 12 | -------------------------------------------------------------------------------- /website/src/api/adminUsers.ts: -------------------------------------------------------------------------------- 1 | import { User } from "@/types" 2 | 3 | import { makeRequest } from "./makeRequest" 4 | 5 | type Res = User[] 6 | export async function adminUsers() { 7 | let data: Res = await makeRequest("/admin/users", "get") 8 | return data 9 | } 10 | -------------------------------------------------------------------------------- /website/src/api/changePass.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest } from "./makeRequest" 2 | 3 | type Res = { 4 | token: string 5 | } 6 | export async function changePass(body: { 7 | currentPassword: string 8 | newPassword: string 9 | }) { 10 | let data: Res = await makeRequest("/user", "put", body) 11 | return data 12 | } 13 | -------------------------------------------------------------------------------- /website/src/api/delete.ts: -------------------------------------------------------------------------------- 1 | import { File, Folder } from "@/types" 2 | 3 | import { makeRequest } from "./makeRequest" 4 | 5 | type Res = { 6 | files: File[] 7 | folders: Folder[] 8 | } 9 | export async function deleteItem(body: { id: string; isFolder: boolean }) { 10 | let data: Res = await makeRequest( 11 | body.isFolder ? "/folder" : "/file", 12 | "delete", 13 | body 14 | ) 15 | return data 16 | } 17 | -------------------------------------------------------------------------------- /website/src/api/deleteFile.ts: -------------------------------------------------------------------------------- 1 | 2 | import { makeRequest } from "./makeRequest" 3 | 4 | type Res = { 5 | } 6 | export async function deleteFile(body: { id: string }) { 7 | let data: Res = await makeRequest("/admin/file", "delete", body) 8 | return data 9 | } 10 | -------------------------------------------------------------------------------- /website/src/api/deleteUser.ts: -------------------------------------------------------------------------------- 1 | import { File, Folder } from "@/types" 2 | 3 | import { makeRequest } from "./makeRequest" 4 | 5 | type Res = { 6 | } 7 | export async function deleteUser(body: { id: string }) { 8 | let data: Res = await makeRequest("/admin/user", "delete", body) 9 | return data 10 | } 11 | -------------------------------------------------------------------------------- /website/src/api/deleteUserFiles.ts: -------------------------------------------------------------------------------- 1 | import { File, Folder } from "@/types" 2 | 3 | import { makeRequest } from "./makeRequest" 4 | 5 | type Res = {} 6 | export async function deleteUserFiles(body: { id: string }) { 7 | let data: Res = await makeRequest("/admin/user-files", "delete", body) 8 | return data 9 | } 10 | -------------------------------------------------------------------------------- /website/src/api/getData.ts: -------------------------------------------------------------------------------- 1 | 2 | import { makeRequest } from "./makeRequest" 3 | 4 | import { File, Folder } from "@/types" 5 | type Res = { 6 | files: File[] 7 | folders: Folder[] 8 | storage: number 9 | } 10 | export async function getData() { 11 | let data: Res = await makeRequest("/data", "get") 12 | return data 13 | } 14 | -------------------------------------------------------------------------------- /website/src/api/githubLogin.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest } from "./makeRequest" 2 | 3 | type Res = { 4 | token: string 5 | } 6 | export async function githubLogin(p: { code: string }) { 7 | let data: Res = await makeRequest( 8 | "/auth/github/callback?" + "code=" + p.code, 9 | "get" 10 | ) 11 | return data 12 | } 13 | -------------------------------------------------------------------------------- /website/src/api/login.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest } from "./makeRequest" 2 | 3 | type Res = { 4 | token: string 5 | } 6 | export async function login(body: { username: string; password: string }) { 7 | let data: Res = await makeRequest("/login", "post", body) 8 | return data 9 | } 10 | -------------------------------------------------------------------------------- /website/src/api/makeRequest.ts: -------------------------------------------------------------------------------- 1 | import { serverUrl } from "@/config" 2 | import { $appState } from "@/state/state" 3 | import axios from "axios" 4 | 5 | export async function makeRequest(path: string, method: string, body?: any) { 6 | const token = $appState.get().session?.token 7 | const options = { 8 | method: method, 9 | path: path, 10 | data: body, 11 | headers: { 12 | "Content-Type": "application/json", 13 | Authorization: `Bearer ${token}`, 14 | }, 15 | } 16 | const { data } = await axios(serverUrl + options.path, { 17 | ...options, 18 | }) 19 | return data 20 | } 21 | -------------------------------------------------------------------------------- /website/src/api/newFolder.ts: -------------------------------------------------------------------------------- 1 | import { makeRequest } from "./makeRequest" 2 | 3 | import { File, Folder } from "@/types" 4 | type Res = { 5 | files: File[] 6 | folders: Folder[] 7 | } 8 | type Body = { parentId: string; name: string } 9 | export async function newFolder(body: Body) { 10 | let data: Res = await makeRequest("/folder", "post", body) 11 | return data 12 | } 13 | -------------------------------------------------------------------------------- /website/src/api/rename.ts: -------------------------------------------------------------------------------- 1 | 2 | import { makeRequest } from "./makeRequest" 3 | 4 | import { File, Folder } from "@/types" 5 | type Res = { 6 | files: File[] 7 | folders: Folder[] 8 | } 9 | export async function renameItem(body: { 10 | id: string 11 | name: string 12 | isFolder: boolean 13 | }) { 14 | let data: Res = await makeRequest( 15 | body.isFolder ? "/folder" : "/file", 16 | "patch", 17 | body 18 | ) 19 | return data 20 | } 21 | -------------------------------------------------------------------------------- /website/src/app/(admin)/admin/AdminCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { UsersIcon } from "lucide-react" 3 | 4 | export function AdminCard(p: { 5 | text1: string 6 | text2: string 7 | icon: JSX.Element 8 | }) { 9 | return ( 10 |
11 |
{p.icon}
12 |
13 |

14 | {p.text1} 15 |

16 |

17 | {p.text2} 18 |

19 |
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /website/src/app/(admin)/admin/files/DeleteFileDialog.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import { deleteUser } from "@/api/deleteUser" 3 | import { deleteUserFiles } from "@/api/deleteUserFiles" 4 | import { ErrorRes } from "@/types" 5 | import { useMutation } from "@tanstack/react-query" 6 | import { AxiosError } from "axios" 7 | import { Trash } from "lucide-react" 8 | import { toast } from "react-toastify" 9 | 10 | import { Button } from "@/components/ui/button" 11 | import { 12 | Dialog, 13 | DialogContent, 14 | DialogFooter, 15 | DialogHeader, 16 | DialogTitle, 17 | DialogTrigger, 18 | } from "@/components/ui/dialog" 19 | import { deleteFile } from "@/api/deleteFile" 20 | 21 | export function DeleteFileDialog(p: { id: string; name: string }) { 22 | const mutation = useMutation({ 23 | mutationFn: deleteFile, 24 | onError: (e: AxiosError) => { 25 | toast.error(e.response?.data.message) 26 | return e 27 | }, 28 | }) 29 | 30 | useEffect(() => { 31 | if (mutation.isSuccess) { 32 | toast.success("Success") 33 | } 34 | }, [mutation.isLoading]) 35 | 36 | const [open, setOpen] = useState() 37 | 38 | function del() { 39 | mutation.mutate({ id: p.id }) 40 | setOpen(false) 41 | } 42 | return ( 43 | 44 | 45 | 53 | 54 | 55 | 56 | Delete {p.name} 57 | 58 | 59 | 62 | 65 | 66 | 67 | 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /website/src/app/(admin)/admin/files/FilesTable.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React from "react" 4 | import { adminFiles } from "@/api/adminFiles" 5 | import { adminUsers } from "@/api/adminUsers" 6 | import { queryKeys } from "@/queryKeys" 7 | import { useQuery } from "@tanstack/react-query" 8 | import moment from "moment" 9 | 10 | import { bytesToMB } from "@/lib/utils" 11 | import { 12 | Table, 13 | TableBody, 14 | TableCell, 15 | TableHead, 16 | TableHeader, 17 | TableRow, 18 | } from "@/components/ui/table" 19 | import { GetFileIcon } from "@/components/GetFileIcon" 20 | 21 | import { RowAction } from "./RowAction" 22 | 23 | export function FilesTable() { 24 | const query = useQuery({ 25 | queryKey: [queryKeys.users], 26 | queryFn: adminFiles, 27 | }) 28 | if (query.isLoading) return <> 29 | return ( 30 | <> 31 | {/* */} 32 | 33 | 34 | 35 | Name 36 | Date 37 | type 38 | Size 39 | 40 | 41 | 42 | 43 | {query.data?.map((i) => ( 44 | toggle(i)} 46 | className="cursor-pointer" 47 | key={i.id} 48 | > 49 | 50 | 51 | {i.name} 52 | 53 | {moment(i.createdAt).fromNow()} 54 | {i.type} 55 | {bytesToMB(i.size || 0)} 56 | { 58 | e.stopPropagation() 59 | }} 60 | className="text-right" 61 | > 62 | 63 | 64 | 65 | ))} 66 | 67 |
68 | 69 | ) 70 | } 71 | -------------------------------------------------------------------------------- /website/src/app/(admin)/admin/files/RowAction.tsx: -------------------------------------------------------------------------------- 1 | import { MoreVertical } from "lucide-react" 2 | 3 | import { 4 | Popover, 5 | PopoverContent, 6 | PopoverTrigger, 7 | } from "@/components/ui/popover" 8 | 9 | import { DeleteFileDialog } from "./DeleteFileDialog" 10 | 11 | export function RowAction(p: { id: string; name: string }) { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /website/src/app/(admin)/admin/files/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { getData } from "@/api/getData" 4 | import { queryKeys } from "@/queryKeys" 5 | import { getAppState, updateAppState } from "@/state/state" 6 | import { ErrorRes } from "@/types" 7 | import { useQuery } from "@tanstack/react-query" 8 | import { AxiosError } from "axios" 9 | import { toast } from "react-toastify" 10 | 11 | import { HeadText } from "@/components/HeadText" 12 | import { Spinner } from "@/components/Spinner" 13 | 14 | import { FilesTable } from "./FilesTable" 15 | 16 | export default function Page() { 17 | const state = getAppState() 18 | const query = useQuery({ 19 | queryKey: [queryKeys.data], 20 | queryFn: getData, 21 | // don't refetch again if already fetched 22 | enabled: !state.initialDataFetched, 23 | onError: (e: AxiosError) => 24 | toast.error(e.response?.data.message || e.message), 25 | }) 26 | if (query.isSuccess && !state.initialDataFetched) { 27 | updateAppState({ initialDataFetched: true }) 28 | updateAppState({ folders: query.data.folders }) 29 | updateAppState({ files: query.data.files }) 30 | } 31 | if (query.isLoading) 32 | return ( 33 |
34 | 35 |
36 | ) 37 | return ( 38 |
39 | 40 | 41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /website/src/app/(admin)/admin/users/DeleteUserDialog.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import { deleteUser } from "@/api/deleteUser" 3 | import { ErrorRes } from "@/types" 4 | import { useMutation } from "@tanstack/react-query" 5 | import { AxiosError } from "axios" 6 | import { Trash } from "lucide-react" 7 | import { toast } from "react-toastify" 8 | 9 | import { Button } from "@/components/ui/button" 10 | import { 11 | Dialog, 12 | DialogContent, 13 | DialogFooter, 14 | DialogHeader, 15 | DialogTitle, 16 | DialogTrigger, 17 | } from "@/components/ui/dialog" 18 | 19 | export function DeleteUserDialog(p: { id: string; username: string }) { 20 | const mutation = useMutation({ 21 | mutationFn: deleteUser, 22 | onError: (e: AxiosError) => { 23 | toast.error(e.response?.data.message) 24 | return e 25 | }, 26 | }) 27 | 28 | useEffect(() => { 29 | if (mutation.isSuccess) { 30 | toast.success("Success") 31 | } 32 | }, [mutation.isLoading]) 33 | 34 | const [open, setOpen] = useState() 35 | 36 | function del() { 37 | mutation.mutate({ id: p.id }) 38 | setOpen(false) 39 | } 40 | return ( 41 | 42 | 43 | 51 | 52 | 53 | 54 | Delete {p.username} 55 | 56 | 57 | 60 | 63 | 64 | 65 | 66 | ) 67 | } 68 | -------------------------------------------------------------------------------- /website/src/app/(admin)/admin/users/DeleteUserFiles.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import { deleteUser } from "@/api/deleteUser" 3 | import { deleteUserFiles } from "@/api/deleteUserFiles" 4 | import { ErrorRes } from "@/types" 5 | import { useMutation } from "@tanstack/react-query" 6 | import { AxiosError } from "axios" 7 | import { Trash } from "lucide-react" 8 | import { toast } from "react-toastify" 9 | 10 | import { Button } from "@/components/ui/button" 11 | import { 12 | Dialog, 13 | DialogContent, 14 | DialogFooter, 15 | DialogHeader, 16 | DialogTitle, 17 | DialogTrigger, 18 | } from "@/components/ui/dialog" 19 | 20 | export function DeleteUserFilesDialog(p: { id: string; username: string }) { 21 | const mutation = useMutation({ 22 | mutationFn: deleteUserFiles, 23 | onError: (e: AxiosError) => { 24 | toast.error(e.response?.data.message) 25 | return e 26 | }, 27 | }) 28 | 29 | useEffect(() => { 30 | if (mutation.isSuccess) { 31 | toast.success("Success") 32 | } 33 | }, [mutation.isLoading]) 34 | 35 | const [open, setOpen] = useState() 36 | 37 | function del() { 38 | mutation.mutate({ id: p.id }) 39 | setOpen(false) 40 | } 41 | return ( 42 | 43 | 44 | 52 | 53 | 54 | 55 | Delete {p.username} Files 56 | 57 | 58 | 61 | 64 | 65 | 66 | 67 | ) 68 | } 69 | -------------------------------------------------------------------------------- /website/src/app/(admin)/admin/users/RowAction.tsx: -------------------------------------------------------------------------------- 1 | import { Download, MoreHorizontal, MoreVertical, Pencil } from "lucide-react" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { 5 | Popover, 6 | PopoverContent, 7 | PopoverTrigger, 8 | } from "@/components/ui/popover" 9 | 10 | import { DeleteUserDialog } from "./DeleteUserDialog" 11 | import { DeleteUserFilesDialog } from "./DeleteUserFiles" 12 | 13 | export function RowAction(p: { id: string; username: string }) { 14 | return ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /website/src/app/(admin)/admin/users/UsersTable.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useState } from "react" 4 | import { adminOverview } from "@/api/adminOverview" 5 | import { adminUsers } from "@/api/adminUsers" 6 | import { queryKeys } from "@/queryKeys" 7 | import { getAppState, updateAppState } from "@/state/state" 8 | import { File, Folder } from "@/types" 9 | import { useQuery } from "@tanstack/react-query" 10 | import { FolderClosed, FolderIcon } from "lucide-react" 11 | import moment from "moment" 12 | 13 | import { bytesToMB } from "@/lib/utils" 14 | import { 15 | Table, 16 | TableBody, 17 | TableCell, 18 | TableHead, 19 | TableHeader, 20 | TableRow, 21 | } from "@/components/ui/table" 22 | 23 | import { RowAction } from "./RowAction" 24 | 25 | export function UsersTable() { 26 | const query = useQuery({ 27 | queryKey: [queryKeys.users], 28 | queryFn: adminUsers, 29 | }) 30 | if (query.isLoading) return <> 31 | return ( 32 | <> 33 | {/* */} 34 | 35 | 36 | 37 | Username 38 | Signup Method 39 | Email 40 | Files 41 | Folders 42 | Total storage 43 | Signup Date 44 | 45 | 46 | 47 | 48 | {query.data?.map((i) => ( 49 | toggle(i)} 51 | className="cursor-pointer" 52 | key={i.id} 53 | > 54 | 55 | {/* */} 56 | {i.username} 57 | 58 | {i.provider} 59 | {i.email} 60 | {i.files?.length || 0} 61 | {i.folders?.length || 0} 62 | {bytesToMB(i.storage || 0)} 63 | {moment(i.createdAt).fromNow()} 64 | { 66 | e.stopPropagation() 67 | }} 68 | className="text-right" 69 | > 70 | 71 | 72 | 73 | ))} 74 | 75 |
76 | 77 | ) 78 | } 79 | -------------------------------------------------------------------------------- /website/src/app/(admin)/admin/users/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { getData } from "@/api/getData" 4 | import { serverUrl } from "@/config" 5 | import { queryKeys } from "@/queryKeys" 6 | import { ErrorRes } from "@/types" 7 | import { useQuery } from "@tanstack/react-query" 8 | import { AxiosError } from "axios" 9 | import { toast } from "react-toastify" 10 | 11 | import { Spinner } from "@/components/Spinner" 12 | import { Breadcrumbs } from "@/components/Breadcrumbs" 13 | import { getAppState, updateAppState } from "@/state/state" 14 | import { UsersTable } from "./UsersTable" 15 | import { HeadText } from "@/components/HeadText" 16 | 17 | 18 | export default function Page() { 19 | const state = getAppState() 20 | const query = useQuery({ 21 | queryKey: [queryKeys.data], 22 | queryFn: getData, 23 | // don't refetch again if already fetched 24 | enabled: !state.initialDataFetched, 25 | onError: (e: AxiosError) => 26 | toast.error(e.response?.data.message || e.message), 27 | }) 28 | if (query.isSuccess && !state.initialDataFetched) { 29 | updateAppState({ initialDataFetched: true }) 30 | updateAppState({ folders: query.data.folders }) 31 | updateAppState({ files: query.data.files }) 32 | } 33 | if (query.isLoading) 34 | return ( 35 |
36 | 37 |
38 | ) 39 | return ( 40 |
41 | 42 | 43 |
44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /website/src/app/(admin)/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css" 2 | import "vidstack/styles/defaults.css" 3 | import "vidstack/styles/community-skin/video.css" 4 | 5 | import { Metadata } from "next" 6 | import { cookies } from "next/headers" 7 | import { SidebarAdmin } from "@/layout/SidebarAdmin" 8 | import { Role, Session, UserData } from "@/types" 9 | import jwt from "jsonwebtoken" 10 | 11 | import "@uppy/core/dist/style.css" 12 | import "@uppy/dashboard/dist/style.css" 13 | import "@uppy/drag-drop/dist/style.css" 14 | import "@uppy/file-input/dist/style.css" 15 | import "@uppy/progress-bar/dist/style.css" 16 | 17 | import { redirect } from "next/navigation" 18 | import { SiteHeaderLoggedIn } from "@/layout/SiteHeaderLoggedIn" 19 | import { SetSession } from "@/session/SetSession" 20 | 21 | import "react-toastify/dist/ReactToastify.css" 22 | 23 | import { localServerUrl, serverUrl } from "@/config" 24 | import axios from "axios" 25 | import { ToastContainer } from "react-toastify" 26 | import { fontSans } from "@/lib/fonts" 27 | import { cn } from "@/lib/utils" 28 | import { ReactQueryProvider } from "@/components/ReactQueryProvider" 29 | import { TailwindIndicator } from "@/components/tailwind-indicator" 30 | import { ThemeProvider } from "@/components/theme-provider" 31 | 32 | 33 | import { meta } from "@/config/meta" 34 | export const metadata:Metadata=meta 35 | 36 | 37 | interface RootLayoutProps { 38 | children: React.ReactNode 39 | } 40 | 41 | export default async function RootLayout({ children }: RootLayoutProps) { 42 | const cookieStore = cookies() 43 | const token = cookieStore.get("token")?.value 44 | if (!token) redirect("/login") 45 | let decoded = jwt.decode(token) as { id: string; role: Role } 46 | // let data = getUserData(session?.token) 47 | const session: Session = { token, id: decoded.id, role: decoded.role } 48 | let userData: UserData = { 49 | id: decoded.id, 50 | avatar: "", 51 | role: decoded.role, 52 | provider: "password", 53 | storage: 0, 54 | } 55 | try { 56 | const { data } = await axios.get(localServerUrl + "/session", { 57 | headers: { Authorization: "Bearer " + session.token }, 58 | }) 59 | userData = { 60 | id: decoded.id, 61 | provider: data.provider, 62 | avatar: data.avatar, 63 | role: decoded.role, 64 | storage: parseInt(data.storage) || 0, 65 | } 66 | } catch (error) { 67 | console.error(error) 68 | } 69 | return ( 70 | <> 71 | 72 | 73 | 79 | 80 | 81 | 82 | 83 | 84 |
85 | 86 |
{children}
87 |
88 | 89 |
90 |
91 | 92 | 93 | 94 | ) 95 | } 96 | -------------------------------------------------------------------------------- /website/src/app/(auth)/auth/discord/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect } from "react" 4 | import { redirect, useRouter, useSearchParams } from "next/navigation" 5 | import { githubLogin } from "@/api/githubLogin" 6 | import { serverUrl } from "@/config" 7 | import axios from "axios" 8 | import Cookies from "js-cookie" 9 | import { toast } from "react-toastify" 10 | 11 | import { Spinner } from "@/components/Spinner" 12 | 13 | export default function Page(p: { params: any; searchParams: any }) { 14 | const code = p.searchParams.code 15 | 16 | let router = useRouter() 17 | useEffect(() => { 18 | if (code) { 19 | fetch(serverUrl + "/auth/discord/callback?" + "code=" + code) 20 | .then((res) => res.json()) 21 | .then((data) => { 22 | console.log("aasd",data) 23 | if (data.token) { 24 | toast.success("Logging in") 25 | Cookies.set("token", data.token, { secure: true }) 26 | router.push("/dashboard") // You need to implement the `redirect` function 27 | } 28 | }) 29 | .catch((error) => { 30 | console.error("Error:", error) 31 | // Handle errors as needed 32 | }) 33 | } 34 | }, [code]) 35 | 36 | return ( 37 |
38 | 39 | Logging in 40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /website/src/app/(auth)/auth/github/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect } from "react" 4 | import { redirect, useRouter, useSearchParams } from "next/navigation" 5 | import { githubLogin } from "@/api/githubLogin" 6 | import { serverUrl } from "@/config" 7 | import axios from "axios" 8 | import Cookies from "js-cookie" 9 | import { toast } from "react-toastify" 10 | 11 | import { Spinner } from "@/components/Spinner" 12 | 13 | export default function Page(p: { params: any; searchParams: any }) { 14 | const code = p.searchParams.code 15 | 16 | let router = useRouter() 17 | useEffect(() => { 18 | if (code) { 19 | fetch(serverUrl + "/auth/github/callback?" + "code=" + code) 20 | .then((res) => res.json()) 21 | .then((data) => { 22 | if (data.token) { 23 | toast.success("Logging in") 24 | Cookies.set("token", data.token, { secure: true }) 25 | router.push("/dashboard") // You need to implement the `redirect` function 26 | } 27 | }) 28 | .catch((error) => { 29 | console.error("Error:", error) 30 | // Handle errors as needed 31 | }) 32 | } 33 | }, [code]) 34 | 35 | return ( 36 |
37 | 38 | Logging in 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /website/src/app/(auth)/auth/google/page.tsx: -------------------------------------------------------------------------------- 1 | 2 | "use client" 3 | 4 | import { useEffect } from "react" 5 | import { redirect, useRouter, useSearchParams } from "next/navigation" 6 | import { githubLogin } from "@/api/githubLogin" 7 | import { serverUrl } from "@/config" 8 | import axios from "axios" 9 | import Cookies from "js-cookie" 10 | import { toast } from "react-toastify" 11 | 12 | import { Spinner } from "@/components/Spinner" 13 | 14 | export default function Page(p: { params: any; searchParams: any }) { 15 | const code = p.searchParams.code 16 | 17 | let router = useRouter() 18 | useEffect(() => { 19 | if (code) { 20 | fetch(serverUrl + "/auth/google/callback?" + "code=" + code) 21 | .then((res) => res.json()) 22 | .then((data) => { 23 | console.log("aasd",data) 24 | if (data.token) { 25 | toast.success("Logging in") 26 | Cookies.set("token", data.token, { secure: true }) 27 | router.push("/dashboard") // You need to implement the `redirect` function 28 | } 29 | }) 30 | .catch((error) => { 31 | console.error("Error:", error) 32 | // Handle errors as needed 33 | }) 34 | } 35 | }, [code]) 36 | 37 | return ( 38 |
39 | 40 | Logging in 41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /website/src/app/(auth)/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css" 2 | import { Metadata } from "next" 3 | import "@uppy/core/dist/style.css" 4 | import "@uppy/dashboard/dist/style.css" 5 | import "@uppy/drag-drop/dist/style.css" 6 | import "@uppy/file-input/dist/style.css" 7 | import "@uppy/progress-bar/dist/style.css" 8 | import "react-toastify/dist/ReactToastify.css" 9 | import { ToastContainer } from "react-toastify" 10 | 11 | import { fontSans } from "@/lib/fonts" 12 | import { cn } from "@/lib/utils" 13 | import { ReactQueryProvider } from "@/components/ReactQueryProvider" 14 | import { TailwindIndicator } from "@/components/tailwind-indicator" 15 | import { ThemeProvider } from "@/components/theme-provider" 16 | 17 | import { meta } from "@/config/meta" 18 | export const metadata:Metadata=meta 19 | 20 | interface RootLayoutProps { 21 | children: React.ReactNode 22 | } 23 | 24 | export default function RootLayout({ children }: RootLayoutProps) { 25 | return ( 26 | <> 27 | 28 | 29 | 35 | 36 | {/* */} 37 | 38 | 39 |
40 |
41 |
44 |
{children}
45 |
46 | 47 |
48 |
49 | 50 | 51 | 52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/albums/AlbumCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Folder } from "@/types" 3 | import { FolderIcon } from "lucide-react" 4 | 5 | import { Card, CardContent } from "@/components/ui/card" 6 | 7 | export function AlbumCard(p: { name: string }) { 8 | return ( 9 | 10 | 11 | 12 |

13 | {p.name} 14 |

15 |
16 |
17 | ) 18 | } 19 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/albums/DataTable.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useState } from "react" 4 | import { FolderIcon, HeartIcon } from "lucide-react" 5 | 6 | import { 7 | Table, 8 | TableBody, 9 | TableCell, 10 | TableHead, 11 | TableHeader, 12 | TableRow, 13 | } from "@/components/ui/table" 14 | 15 | 16 | export function DataTable() { 17 | return ( 18 | 19 | 20 | 21 | Name 22 | Date 23 | Type 24 | Size 25 | 26 | 27 | 28 | {[{id:1,name:"aa",createdAt:1}].map((f) => ( 29 | <> 30 | 34 | 35 | 36 | {f.name} 37 | 38 | {f.createdAt} 39 | Folder 40 | - 41 | 42 | 46 | 47 | 48 | {f.name} 49 | 50 | {f.createdAt} 51 | Folder 52 | - 53 | 54 | 55 | ))} 56 | 57 |
58 | ) 59 | } 60 | 61 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/albums/page.tsx: -------------------------------------------------------------------------------- 1 | import { AlertNotImplemented } from "@/components/AlertNotImplemented" 2 | import { HeadText } from "@/components/HeadText" 3 | 4 | import { AlbumCard } from "./AlbumCard" 5 | import { DataTable } from "./DataTable" 6 | 7 | export default function Page() { 8 | return ( 9 |
10 | 11 | 12 |
13 | {[1, 2, 3].map((i) => ( 14 | 15 | ))} 16 |
17 |
18 | ) 19 | } 20 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/all-media/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { getData } from "@/api/getData" 4 | import { serverUrl } from "@/config" 5 | import { queryKeys } from "@/queryKeys" 6 | import { getAppState, updateAppState } from "@/state/state" 7 | import { ErrorRes } from "@/types" 8 | import { useQuery } from "@tanstack/react-query" 9 | import { AxiosError } from "axios" 10 | import { toast } from "react-toastify" 11 | 12 | import { Breadcrumbs } from "@/components/Breadcrumbs" 13 | import { DataTable } from "@/components/DataTable" 14 | import { Spinner } from "@/components/Spinner" 15 | 16 | export default function Page() { 17 | const state = getAppState() 18 | const query = useQuery({ 19 | queryKey: [queryKeys.data], 20 | queryFn: getData, 21 | // don't refetch again if already fetched 22 | enabled: !state.initialDataFetched, 23 | onError: (e: AxiosError) => 24 | toast.error(e.response?.data.message || e.message), 25 | }) 26 | if (query.isSuccess && !state.initialDataFetched) { 27 | updateAppState({ initialDataFetched: true }) 28 | updateAppState({ folders: query.data.folders }) 29 | updateAppState({ files: query.data.files }) 30 | } 31 | if (query.isLoading) 32 | return ( 33 |
34 | 35 |
36 | ) 37 | return ( 38 |
39 | 40 | 41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/devices/DataTable.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useState } from "react" 4 | import { FolderIcon, HeartIcon, TabletSmartphone } from "lucide-react" 5 | 6 | import { 7 | Table, 8 | TableBody, 9 | TableCell, 10 | TableHead, 11 | TableHeader, 12 | TableRow, 13 | } from "@/components/ui/table" 14 | import mockDevices from "@/components/mockDevices" 15 | 16 | export function DataTable() { 17 | return ( 18 | 19 | 20 | 21 | Name 22 | Last Sync 23 | Files 24 | Total Storage 25 | 26 | 27 | 28 | {mockDevices.map((f) => ( 29 | <> 30 | 31 | 32 | 33 | {f.name} 34 | 35 | {f.lastSync} 36 | {f.filesCount} 37 | {f.totalStorage} 38 | 39 | 40 | ))} 41 | 42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/devices/page.tsx: -------------------------------------------------------------------------------- 1 | import { AlertNotImplemented } from "@/components/AlertNotImplemented" 2 | import { HeadText } from "@/components/HeadText" 3 | import { DataTable } from "./DataTable" 4 | 5 | export default function Page() { 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/favorites/DataTable.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useState } from "react" 4 | import { FolderIcon, HeartIcon } from "lucide-react" 5 | 6 | import { 7 | Table, 8 | TableBody, 9 | TableCell, 10 | TableHead, 11 | TableHeader, 12 | TableRow, 13 | } from "@/components/ui/table" 14 | import { GetFileIcon } from "@/components/GetFileIcon" 15 | import mockFiles from "@/components/mockFiles" 16 | 17 | export function DataTable() { 18 | return ( 19 | 20 | 21 | 22 | Name 23 | Date 24 | Type 25 | Size 26 | 27 | 28 | 29 | {mockFiles.map((f) => ( 30 | <> 31 | 32 | 33 | 34 | {f.name} 35 | 36 | 37 | {f.lastAccessed} 38 | {f.type} 39 | {f.fileSize} 40 | 41 | 42 | ))} 43 | 44 |
45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/favorites/page.tsx: -------------------------------------------------------------------------------- 1 | import { AlertNotImplemented } from "@/components/AlertNotImplemented" 2 | import { HeadText } from "@/components/HeadText" 3 | import { DataTable } from "./DataTable" 4 | 5 | export default function Page() { 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/images/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { getData } from "@/api/getData" 4 | import { serverUrl } from "@/config" 5 | import { queryKeys } from "@/queryKeys" 6 | import { ErrorRes } from "@/types" 7 | import { useQuery } from "@tanstack/react-query" 8 | import { AxiosError } from "axios" 9 | import { toast } from "react-toastify" 10 | 11 | import { DataTable } from "@/components/DataTable" 12 | import { Spinner } from "@/components/Spinner" 13 | import { Breadcrumbs } from "@/components/Breadcrumbs" 14 | import { getAppState, updateAppState } from "@/state/state" 15 | 16 | 17 | export default function Page() { 18 | const state = getAppState() 19 | const query = useQuery({ 20 | queryKey: [queryKeys.data], 21 | queryFn: getData, 22 | // don't refetch again if already fetched 23 | enabled: !state.initialDataFetched, 24 | onError: (e: AxiosError) => 25 | toast.error(e.response?.data.message || e.message), 26 | }) 27 | if (query.isSuccess && !state.initialDataFetched) { 28 | updateAppState({ initialDataFetched: true }) 29 | updateAppState({ folders: query.data.folders }) 30 | updateAppState({ files: query.data.files }) 31 | } 32 | if (query.isLoading) 33 | return ( 34 |
35 | 36 |
37 | ) 38 | return ( 39 |
40 | 41 | 42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { getData } from "@/api/getData" 4 | import { serverUrl } from "@/config" 5 | import { queryKeys } from "@/queryKeys" 6 | import { getAppState, updateAppState } from "@/state/state" 7 | import { ErrorRes } from "@/types" 8 | import { useQuery } from "@tanstack/react-query" 9 | import { AxiosError } from "axios" 10 | import { toast } from "react-toastify" 11 | 12 | import { Breadcrumbs } from "@/components/Breadcrumbs" 13 | import { DataTable } from "@/components/DataTable" 14 | import { Spinner } from "@/components/Spinner" 15 | 16 | export const dynamic = "force-dynamic" 17 | export default function Page() { 18 | const state = getAppState() 19 | const query = useQuery({ 20 | queryKey: [queryKeys.data], 21 | queryFn: getData, 22 | // don't refetch again if already fetched 23 | enabled: !state.initialDataFetched, 24 | onError: (e: AxiosError) => 25 | toast.error(e.response?.data.message || e.message), 26 | }) 27 | if (query.isSuccess && !state.initialDataFetched) { 28 | updateAppState({ initialDataFetched: true }) 29 | updateAppState({ folders: query.data.folders }) 30 | updateAppState({ files: query.data.files }) 31 | } 32 | if (query.isLoading) 33 | return ( 34 |
35 | 36 |
37 | ) 38 | return ( 39 |
40 | 41 | 42 |
43 | ) 44 | } 45 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/recent/DataTable.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useState } from "react" 4 | import { FileIcon, FolderIcon } from "lucide-react" 5 | 6 | import { 7 | Table, 8 | TableBody, 9 | TableCell, 10 | TableHead, 11 | TableHeader, 12 | TableRow, 13 | } from "@/components/ui/table" 14 | import { GetFileIcon } from "@/components/GetFileIcon" 15 | import mockFiles from "@/components/mockFiles" 16 | 17 | 18 | export function DataTable() { 19 | return ( 20 | 21 | 22 | 23 | Name 24 | Last accessed 25 | Type 26 | Size 27 | 28 | 29 | 30 | {mockFiles.map((f) => ( 31 | <> 32 | 36 | 37 | 38 | {f.name} 39 | 40 | {f.lastAccessed} 41 | {f.type} 42 | {f.fileSize} 43 | 44 | 45 | ))} 46 | 47 |
48 | ) 49 | } 50 | 51 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/recent/page.tsx: -------------------------------------------------------------------------------- 1 | import { AlertNotImplemented } from "@/components/AlertNotImplemented" 2 | import { HeadText } from "@/components/HeadText" 3 | import { DataTable } from "./DataTable" 4 | 5 | export default function Page() { 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/team/DataTable.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useState } from "react" 4 | import { FolderIcon, HeartIcon, TabletSmartphone } from "lucide-react" 5 | 6 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" 7 | import { 8 | Table, 9 | TableBody, 10 | TableCell, 11 | TableHead, 12 | TableHeader, 13 | TableRow, 14 | } from "@/components/ui/table" 15 | 16 | export function DataTable() { 17 | return ( 18 | 19 | 20 | 21 | Name 22 | 23 | 24 | 25 | {[1,2,3,4].map((f) => ( 26 | <> 27 | 28 | 29 |
30 | 31 | 32 | CN 33 | 34 | 35 | 36 | CN 37 | 38 | 39 | 40 | +99 41 | 42 |
43 | My Team 44 |
45 |
46 | 47 | ))} 48 |
49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/team/page.tsx: -------------------------------------------------------------------------------- 1 | import { AlertNotImplemented } from "@/components/AlertNotImplemented" 2 | import { HeadText } from "@/components/HeadText" 3 | import { DataTable } from "./DataTable" 4 | 5 | export default function Page() { 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/trash/DataTable.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import React, { useState } from "react" 4 | import { ArchiveRestore, FolderIcon, HeartIcon } from "lucide-react" 5 | 6 | import { Button } from "@/components/ui/button" 7 | import { 8 | Table, 9 | TableBody, 10 | TableCell, 11 | TableHead, 12 | TableHeader, 13 | TableRow, 14 | } from "@/components/ui/table" 15 | import { GetFileIcon } from "@/components/GetFileIcon" 16 | import mockFiles from "@/components/mockFiles" 17 | 18 | export function DataTable() { 19 | return ( 20 | 21 | 22 | 23 | Name 24 | Date 25 | Type 26 | Size 27 | Restore 28 | 29 | 30 | 31 | 32 | {mockFiles.map((f) => ( 33 | <> 34 | 35 | 36 | 37 | {f.name} 38 | 39 | {f.lastAccessed} 40 | {f.type} 41 | {f.fileSize} 42 | 43 | 46 | 47 | 48 | 49 | ))} 50 | 51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/trash/page.tsx: -------------------------------------------------------------------------------- 1 | import { AlertNotImplemented } from "@/components/AlertNotImplemented" 2 | import { HeadText } from "@/components/HeadText" 3 | import { DataTable } from "./DataTable" 4 | 5 | export default function Page() { 6 | return ( 7 |
8 | 9 | 10 | 11 |
12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /website/src/app/(dashboard)/dashboard/videos/page.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { getData } from "@/api/getData" 4 | import { serverUrl } from "@/config" 5 | import { queryKeys } from "@/queryKeys" 6 | import { getAppState, updateAppState } from "@/state/state" 7 | import { ErrorRes } from "@/types" 8 | import { useQuery } from "@tanstack/react-query" 9 | import { AxiosError } from "axios" 10 | import { toast } from "react-toastify" 11 | 12 | import { Breadcrumbs } from "@/components/Breadcrumbs" 13 | import { DataTable } from "@/components/DataTable" 14 | import { Spinner } from "@/components/Spinner" 15 | 16 | export default function Page() { 17 | const state = getAppState() 18 | const query = useQuery({ 19 | queryKey: [queryKeys.data], 20 | queryFn: getData, 21 | // don't refetch again if already fetched 22 | enabled: !state.initialDataFetched, 23 | onError: (e: AxiosError) => 24 | toast.error(e.response?.data.message || e.message), 25 | }) 26 | if (query.isSuccess && !state.initialDataFetched) { 27 | updateAppState({ initialDataFetched: true }) 28 | updateAppState({ folders: query.data.folders }) 29 | updateAppState({ files: query.data.files }) 30 | } 31 | if (query.isLoading) 32 | return ( 33 |
34 | 35 |
36 | ) 37 | return ( 38 |
39 | 40 | 41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /website/src/app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css" 2 | import { Metadata } from "next" 3 | import { SiteHeader } from "@/layout/SiteHeader" 4 | import { fontSans } from "@/lib/fonts" 5 | import { cn } from "@/lib/utils" 6 | import { ReactQueryProvider } from "@/components/ReactQueryProvider" 7 | import { TailwindIndicator } from "@/components/tailwind-indicator" 8 | import { ThemeProvider } from "@/components/theme-provider" 9 | import "react-toastify/dist/ReactToastify.css" 10 | import { ToastContainer } from "react-toastify" 11 | 12 | import { meta } from "@/config/meta" 13 | 14 | export const metadata:Metadata=meta 15 | 16 | interface RootLayoutProps { 17 | children: React.ReactNode 18 | } 19 | 20 | export default function RootLayout({ children }: RootLayoutProps) { 21 | return ( 22 | <> 23 | 24 | 25 | 31 | 32 | 33 | 34 |
35 | 36 |
{children}
37 |
38 | 39 |
40 |
41 | 42 | 43 | 44 | ) 45 | } 46 | -------------------------------------------------------------------------------- /website/src/app/(home)/login/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | import { cookies } from "next/headers" 3 | import { LoginForm } from "./LoginForm" 4 | 5 | export default function Page() { 6 | const cookieStore = cookies() 7 | const token = cookieStore.get("token")?.value 8 | if(token)redirect("/dashboard") 9 | return ( 10 |
11 |
12 | 13 |
14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /website/src/app/(home)/page.tsx: -------------------------------------------------------------------------------- 1 | import AnsibleIcon from "@/components/icons/Ansible" 2 | import DockerIcon from "@/components/icons/Docker" 3 | import GoIcon from "@/components/icons/Go" 4 | import GrafanaIcon from "@/components/icons/Grafana" 5 | import NextIcon from "@/components/icons/Next" 6 | import NginxIcon from "@/components/icons/Nginx" 7 | import PostgresIcon from "@/components/icons/Postgres" 8 | import PrismaIcon from "@/components/icons/Prisma" 9 | import PrometheusIcon from "@/components/icons/Prometheus" 10 | import ReactQueryIcon from "@/components/icons/ReactQuery" 11 | import RedisIcon from "@/components/icons/Redis" 12 | 13 | export default function Page() { 14 | const iconClass = "w-[30px] h-[30px] lg:w-[55px] lg:h-[55px]" 15 | return ( 16 |
17 |
18 |
19 |
20 |

21 | StorageBox 22 |

23 |

24 | A Simple File Storage Service 25 |

26 |
27 |
28 | 29 |
30 |
31 | 32 |
33 |

34 | Powered by a Powerful Tech Stack 35 |

36 |
37 |
38 | 39 | 40 | 41 | 42 | 43 | 44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 |
52 |
53 |
54 | 55 |
56 |
57 |

58 | Admin Dashboard 59 |

60 |

61 | Admin Dashboard with Useful Metrics and Statistics 62 |

63 |
64 |
65 | 66 |
67 |
68 |
69 |
70 | ) 71 | } 72 | -------------------------------------------------------------------------------- /website/src/components/AlertNotImplemented.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { AlertCircle } from "lucide-react" 3 | 4 | import { Alert, AlertTitle } from "@/components/ui/alert" 5 | 6 | export function AlertNotImplemented() { 7 | return ( 8 | 9 | 10 | This page is for demonstration purposes only. 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /website/src/components/Avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useState } from "react" 4 | import { useRouter } from "next/navigation" 5 | import { serverUrl } from "@/config" 6 | import { getAppState } from "@/state/state" 7 | import Cookies from "js-cookie" 8 | import { LogOut, Settings } from "lucide-react" 9 | import { toast } from "react-toastify" 10 | 11 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" 12 | import { 13 | Popover, 14 | PopoverContent, 15 | PopoverTrigger, 16 | } from "@/components/ui/popover" 17 | 18 | import { Button } from "./ui/button" 19 | 20 | export function UserAvatar() { 21 | let router = useRouter() 22 | function logout() { 23 | Cookies.remove("token") 24 | router.push("/") 25 | } 26 | 27 | const [avatar, setAvatar] = useState(null) 28 | 29 | let state = getAppState() 30 | useEffect(() => { 31 | state.userData?.avatar.length! > 0 && fetchAvatar() 32 | async function fetchAvatar() { 33 | try { 34 | const authToken = "Bearer " + state.session?.token // Replace with your authorization token 35 | 36 | const response = await fetch( 37 | serverUrl + "/files/" + state.userData?.avatar, 38 | { 39 | method: "GET", 40 | headers: { 41 | Authorization: authToken, 42 | }, 43 | } 44 | ) 45 | 46 | if (!response.ok) { 47 | throw new Error("Request failed") 48 | } 49 | 50 | const blob = await response.blob() 51 | const blobUrl = URL.createObjectURL(blob) 52 | setAvatar(blobUrl) 53 | } catch (error) { 54 | toast.error("Error fetching avatar:" + error) 55 | console.error("Error fetching file content:", error) 56 | } 57 | } 58 | }, [state.userData?.avatar]) 59 | 60 | return ( 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 76 | 83 | 84 | 85 | ) 86 | } 87 | -------------------------------------------------------------------------------- /website/src/components/DeleteDialog.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react" 2 | import { deleteItem } from "@/api/delete" 3 | import { getData } from "@/api/getData" 4 | import { renameItem } from "@/api/rename" 5 | import { ErrorRes } from "@/types" 6 | import { zodResolver } from "@hookform/resolvers/zod" 7 | import { useMutation } from "@tanstack/react-query" 8 | import { AxiosError } from "axios" 9 | import { Pencil, Trash } from "lucide-react" 10 | import { useForm } from "react-hook-form" 11 | import { toast } from "react-toastify" 12 | import { z } from "zod" 13 | 14 | import { Button } from "@/components/ui/button" 15 | import { 16 | Dialog, 17 | DialogContent, 18 | DialogDescription, 19 | DialogFooter, 20 | DialogHeader, 21 | DialogTitle, 22 | DialogTrigger, 23 | } from "@/components/ui/dialog" 24 | import { 25 | Form, 26 | FormControl, 27 | FormField, 28 | FormItem, 29 | FormLabel, 30 | FormMessage, 31 | } from "@/components/ui/form" 32 | import { Input } from "@/components/ui/input" 33 | import { Label } from "@/components/ui/label" 34 | import { getAppState, updateAppState } from "@/state/state" 35 | 36 | const formSchema = z.object({ 37 | name: z.string().min(1).max(255), 38 | }) 39 | 40 | export function DeleteDialog(p: { 41 | id: string 42 | name: string 43 | isFolder: boolean 44 | }) { 45 | let state = getAppState() 46 | const mutation = useMutation({ 47 | mutationFn: deleteItem, 48 | onError: (e: AxiosError) => { 49 | toast.error(e.response?.data.message) 50 | return e 51 | }, 52 | }) 53 | 54 | // let router = useRouter() 55 | // if (mutation.isSuccess) { 56 | // toast.success(mutation.data.token) 57 | // Cookies.set("token", mutation.data.token, { secure: true }) 58 | // router.push("/dashboard") 59 | // } 60 | 61 | const [open, setOpen] = useState() 62 | 63 | useEffect(() => { 64 | if (mutation.isSuccess) { 65 | 66 | updateAppState({ folders: mutation.data.folders }) 67 | updateAppState({ files: mutation.data.files }) 68 | toast.success("Success") 69 | } 70 | }, [mutation.isLoading]) 71 | function del() { 72 | mutation.mutate({ id: p.id, isFolder: p.isFolder }) 73 | setOpen(false) 74 | } 75 | return ( 76 | 77 | 78 | 86 | 87 | 88 | 89 | Delete {p.name} 90 | 91 |

Are you sure you want to delete this {p.isFolder?"Folder":"File"}

92 | 93 | 94 | 95 | 96 |
97 |
98 | ) 99 | } 100 | -------------------------------------------------------------------------------- /website/src/components/FileCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { serverUrl } from "@/config" 3 | import { getAppState } from "@/state/state" 4 | import { File } from "@/types" 5 | 6 | import { handleDownload } from "@/lib/utils" 7 | import { Card, CardContent } from "@/components/ui/card" 8 | 9 | import { GetFileIcon } from "./GetFileIcon" 10 | import { RowAction } from "./RowAction" 11 | 12 | export function FileCard(p: File & { onClick: any }) { 13 | const token = getAppState().session?.token 14 | 15 | return ( 16 | 20 | 21 | {/* {isImage ? ( */} 22 | {/* content && {p.name} */} 23 | {/* ) : ( */} 24 | {/*
{content}
*/} 25 | {/* )} */} 26 | {/* TODO change icon based on type */} 27 | 28 |

29 | {/* */} 36 | {p.name} 37 | {/* */} 38 |

39 | handleDownload({ ...p, token: token! })} 41 | horizontal 42 | isFolder={false} 43 | name={p.name} 44 | id={p.id} 45 | /> 46 |
47 |
48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /website/src/components/FolderCard.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Folder } from "@/types" 3 | import { FolderIcon } from "lucide-react" 4 | 5 | import { Button } from "@/components/ui/button" 6 | import { 7 | Card, 8 | CardContent, 9 | CardDescription, 10 | CardFooter, 11 | CardHeader, 12 | CardTitle, 13 | } from "@/components/ui/card" 14 | import { Input } from "@/components/ui/input" 15 | import { Label } from "@/components/ui/label" 16 | import { 17 | Select, 18 | SelectContent, 19 | SelectItem, 20 | SelectTrigger, 21 | SelectValue, 22 | } from "@/components/ui/select" 23 | 24 | import { RowAction } from "./RowAction" 25 | 26 | export function FolderCard(p: Folder & { selectFolder: any }) { 27 | return ( 28 | 31 | 32 | p.selectFolder(p.id)} 34 | className="w-20 h-20" /> 35 |

{p.name}

36 | 37 |
38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /website/src/components/GetFileIcon.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { ViewAs } from "@/types" 3 | import { FileIcon, ImageIcon, VideoIcon } from "lucide-react" 4 | 5 | export function GetFileIcon(p: { type: string; view: ViewAs }) { 6 | const type = p.type.toLowerCase() 7 | let className = "" 8 | if (p.view === "grid") className = "w-20 h-20" 9 | 10 | if (type.includes("img") || type.includes("png")) { 11 | return 12 | } else if (type.includes("text") || type.includes("pdf")) { 13 | return 14 | } else if (type.includes("video") || type.includes("mp4")) { 15 | return 16 | } else { 17 | return 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /website/src/components/HeadText.tsx: -------------------------------------------------------------------------------- 1 | export function HeadText({ text }: { text: string }) { 2 | return ( 3 | <> 4 |
5 |
    6 |
  1. 7 | 8 | {text} 9 | 10 |
  2. 11 |
12 |
13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /website/src/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export function Logo() { 4 | return ( 5 | 6 | 16 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Storage Box 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /website/src/components/ReactQueryProvider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { QueryClient, QueryClientProvider } from "@tanstack/react-query" 4 | 5 | const queryClient = new QueryClient() 6 | export function ReactQueryProvider({ children }: { children: any }) { 7 | return ( 8 | {children} 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /website/src/components/RowAction.tsx: -------------------------------------------------------------------------------- 1 | import { Download, MoreHorizontal, MoreVertical, Pencil } from "lucide-react" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { 5 | Popover, 6 | PopoverContent, 7 | PopoverTrigger, 8 | } from "@/components/ui/popover" 9 | 10 | import { DeleteDialog } from "./DeleteDialog" 11 | import { RenameDialog } from "./RenameDialog" 12 | 13 | export function RowAction(p: { 14 | id: string 15 | horizontal?: boolean 16 | isFolder: boolean 17 | name: string 18 | handleDownload?: any 19 | }) { 20 | return ( 21 | 22 | 23 | {p.horizontal ? : } 24 | 25 | 26 | 27 | 28 | 36 | 37 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /website/src/components/Search.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { ChangeEvent } from "react" 4 | import { updateAppState } from "@/state/state" 5 | import { SearchIcon } from "lucide-react" 6 | 7 | import { Input } from "@/components/ui/input" 8 | 9 | export function Search() { 10 | function handleInputChange(event: ChangeEvent) { 11 | const searchQuery = event.target.value 12 | updateAppState({ searchQuery }) 13 | } 14 | 15 | return ( 16 |
17 | 22 | 23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /website/src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | export function Spinner() { 3 | return
4 | } 5 | -------------------------------------------------------------------------------- /website/src/components/icons/Ansible.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react" 3 | import { SVGProps } from "react" 4 | const AnsibleIcon = (props: SVGProps) => ( 5 | 11 | 12 | 13 | 17 | 18 | ) 19 | export default AnsibleIcon 20 | -------------------------------------------------------------------------------- /website/src/components/icons/Discord.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { SVGProps } from "react"; 3 | 4 | const DiscordIcon = (props: SVGProps) => ( 5 | 10 | 14 | 15 | ); 16 | 17 | export default DiscordIcon; 18 | -------------------------------------------------------------------------------- /website/src/components/icons/Docker.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react" 3 | import { SVGProps } from "react" 4 | const DockerIcon = (props: SVGProps) => ( 5 | 11 | 12 | 17 | 21 | 22 | ) 23 | export default DockerIcon 24 | -------------------------------------------------------------------------------- /website/src/components/icons/Github.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import { SVGProps } from "react"; 3 | 4 | const GithubIcon = (props: SVGProps) => ( 5 | 10 | 13 | 14 | ); 15 | 16 | export default GithubIcon; 17 | -------------------------------------------------------------------------------- /website/src/components/icons/Go.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { SVGProps } from "react" 3 | 4 | const GoIcon = (props: SVGProps) => ( 5 | 12 | 13 | 17 | 18 | 22 | 26 | 27 | 28 | ) 29 | export default GoIcon 30 | -------------------------------------------------------------------------------- /website/src/components/icons/Google.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { SVGProps } from "react" 3 | 4 | const GoogleIcon = (props: SVGProps) => ( 5 | 10 | 11 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | ) 25 | 26 | export default GoogleIcon 27 | -------------------------------------------------------------------------------- /website/src/components/icons/Next.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { SVGProps } from "react" 3 | 4 | const NextIcon = (props: SVGProps) => ( 5 | 11 | 12 | 13 | ) 14 | export default NextIcon 15 | -------------------------------------------------------------------------------- /website/src/components/icons/Nginx.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react" 3 | import { SVGProps } from "react" 4 | const NginxIcon = (props: SVGProps) => ( 5 | 13 | 17 | 21 | 22 | ) 23 | export default NginxIcon 24 | -------------------------------------------------------------------------------- /website/src/components/icons/Prometheus.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { SVGProps } from "react" 3 | const PrometheusIcon = (props: SVGProps) => ( 4 | 12 | 16 | 17 | ) 18 | export default PrometheusIcon 19 | -------------------------------------------------------------------------------- /website/src/components/icons/Redis.tsx: -------------------------------------------------------------------------------- 1 | 2 | import * as React from "react" 3 | import { SVGProps } from "react" 4 | const RedisIcon = (props: SVGProps) => ( 5 | 13 | 17 | 21 | 25 | 29 | 33 | 37 | 41 | 45 | 49 | 50 | ) 51 | export default RedisIcon 52 | -------------------------------------------------------------------------------- /website/src/components/icons/X.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { SVGProps } from "react" 3 | const XIcon = (props: SVGProps) => ( 4 | 12 | ) 13 | export default XIcon 14 | -------------------------------------------------------------------------------- /website/src/components/main-nav.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import Link from "next/link" 3 | 4 | import { NavItem } from "@/types/nav" 5 | import { siteConfig } from "@/config/site" 6 | import { cn } from "@/lib/utils" 7 | import { Icons } from "@/components/icons" 8 | 9 | interface MainNavProps { 10 | items?: NavItem[] 11 | } 12 | 13 | export function MainNav({ items }: MainNavProps) { 14 | return ( 15 |
16 | 17 | 18 | {siteConfig.name} 19 | 20 | {items?.length ? ( 21 | 38 | ) : null} 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /website/src/components/mockDevices.ts: -------------------------------------------------------------------------------- 1 | type Device = { 2 | name: string; 3 | lastSync: string; // You can use a date string like "2 days ago" here 4 | filesCount: number; 5 | totalStorage: string; // You can use a string like "32 GB" here 6 | }; 7 | 8 | const mockDevices: Device[] = [ 9 | { 10 | name: "Phone", 11 | lastSync: "1 day ago", 12 | filesCount: 235, 13 | totalStorage: "337 MB", 14 | }, 15 | { 16 | name: "Tablet", 17 | lastSync: "3 days ago", 18 | filesCount: 123, 19 | totalStorage: "118 MB", 20 | }, 21 | { 22 | name: "Laptop", 23 | lastSync: "5 days ago", 24 | filesCount: 567, 25 | totalStorage: "1.5 GB", 26 | }, 27 | { 28 | name: "Desktop", 29 | lastSync: "2 weeks ago", 30 | filesCount: 789, 31 | totalStorage: "2.8 GB", 32 | }, 33 | ]; 34 | export default mockDevices 35 | -------------------------------------------------------------------------------- /website/src/components/mockFiles.ts: -------------------------------------------------------------------------------- 1 | const mockFiles: Array<{ 2 | name: string; 3 | lastAccessed: string; 4 | type: string; 5 | fileSize: string; 6 | }> = [ 7 | { 8 | name: "document1.docx", 9 | lastAccessed: "2 days ago", 10 | type: "application/msword", 11 | fileSize: "2.5 MB", 12 | }, 13 | { 14 | name: "image1.jpg", 15 | lastAccessed: "1 week ago", 16 | type: "image/jpeg", 17 | fileSize: "1.2 MB", 18 | }, 19 | { 20 | name: "spreadsheet.xlsx", 21 | lastAccessed: "3 days ago", 22 | type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", 23 | fileSize: "4.8 MB", 24 | }, 25 | { 26 | name: "code.js", 27 | lastAccessed: "2 days ago", 28 | type: "application/javascript", 29 | fileSize: "350 KB", 30 | }, 31 | { 32 | name: "presentation.pptx", 33 | lastAccessed: "5 days ago", 34 | type: "application/vnd.openxmlformats-officedocument.presentationml.presentation", 35 | fileSize: "3.3 MB", 36 | }, 37 | { 38 | name: "image2.png", 39 | lastAccessed: "2 weeks ago", 40 | type: "image/png", 41 | fileSize: "900 KB", 42 | }, 43 | { 44 | name: "text.txt", 45 | lastAccessed: "4 days ago", 46 | type: "text/plain", 47 | fileSize: "150 KB", 48 | }, 49 | { 50 | name: "video.mp4", 51 | lastAccessed: "1 week ago", 52 | type: "video/mp4", 53 | fileSize: "15.7 MB", 54 | }, 55 | { 56 | name: "presentation2.pptx", 57 | lastAccessed: "6 days ago", 58 | type: "application/vnd.openxmlformats-officedocument.presentationml.presentation", 59 | fileSize: "5.2 MB", 60 | }, 61 | { 62 | name: "code2.ts", 63 | lastAccessed: "3 days ago", 64 | type: "text/typescript", 65 | fileSize: "280 KB", 66 | }, 67 | ]; 68 | 69 | export default mockFiles; 70 | -------------------------------------------------------------------------------- /website/src/components/tailwind-indicator.tsx: -------------------------------------------------------------------------------- 1 | export function TailwindIndicator() { 2 | if (process.env.NODE_ENV === "production") return null 3 | return null 4 | 5 | return ( 6 |
7 |
xs
8 |
sm
9 |
md
10 |
lg
11 |
xl
12 |
2xl
13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /website/src/components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { ThemeProvider as NextThemesProvider } from "next-themes" 5 | import { type ThemeProviderProps } from "next-themes/dist/types" 6 | 7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) { 8 | return {children} 9 | } 10 | -------------------------------------------------------------------------------- /website/src/components/theme-toggle.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Moon, Sun } from "lucide-react" 5 | import { useTheme } from "next-themes" 6 | 7 | import { Button } from "@/components/ui/button" 8 | 9 | export function ThemeToggle() { 10 | const { setTheme, theme } = useTheme() 11 | 12 | return ( 13 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /website/src/components/ui/SidebarButton.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const sidebarButtonVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | active: 16 | "bg-blue-700 text-destructive-foreground hover:bg-blue-600/90", 17 | outline: 18 | "border border-input hover:bg-accent hover:text-accent-foreground", 19 | secondary: 20 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 21 | ghost: "hover:bg-accent bg-gray-800/60 hover:text-accent-foreground", 22 | link: "underline-offset-4 hover:underline text-primary", 23 | }, 24 | size: { 25 | default: "h-10 py-2 px-4", 26 | sm: "h-9 px-3 rounded-md", 27 | lg: "h-11 px-8 rounded-md", 28 | icon: "h-10 w-10", 29 | }, 30 | }, 31 | defaultVariants: { 32 | variant: "default", 33 | size: "default", 34 | }, 35 | } 36 | ) 37 | 38 | export interface ButtonProps 39 | extends React.ButtonHTMLAttributes, 40 | VariantProps { 41 | asChild?: boolean 42 | } 43 | 44 | const SidebarButton = React.forwardRef( 45 | ({ className, variant, size, asChild = false, ...props }, ref) => { 46 | const Comp = asChild ? Slot : "button" 47 | return ( 48 | 53 | ) 54 | } 55 | ) 56 | SidebarButton.displayName = "SidebarButton" 57 | 58 | export { SidebarButton, sidebarButtonVariants } 59 | -------------------------------------------------------------------------------- /website/src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | warning: 15 | "border-yellow-900/50 text-yellow-800 dark:text-yellow-500 dark:border-yellow-500 [&>svg]:dark:text-yellow-500 [&>svg]:text-yellow-800", 16 | }, 17 | }, 18 | defaultVariants: { 19 | variant: "default", 20 | }, 21 | } 22 | ) 23 | 24 | const Alert = React.forwardRef< 25 | HTMLDivElement, 26 | React.HTMLAttributes & VariantProps 27 | >(({ className, variant, ...props }, ref) => ( 28 |
34 | )) 35 | Alert.displayName = "Alert" 36 | 37 | const AlertTitle = React.forwardRef< 38 | HTMLParagraphElement, 39 | React.HTMLAttributes 40 | >(({ className, ...props }, ref) => ( 41 |
46 | )) 47 | AlertTitle.displayName = "AlertTitle" 48 | 49 | const AlertDescription = React.forwardRef< 50 | HTMLParagraphElement, 51 | React.HTMLAttributes 52 | >(({ className, ...props }, ref) => ( 53 |
58 | )) 59 | AlertDescription.displayName = "AlertDescription" 60 | 61 | export { Alert, AlertTitle, AlertDescription } 62 | -------------------------------------------------------------------------------- /website/src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /website/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90", 15 | outline: 16 | "border border-input hover:bg-accent hover:text-accent-foreground", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: "hover:bg-accent hover:text-accent-foreground", 20 | link: "underline-offset-4 hover:underline text-primary", 21 | }, 22 | size: { 23 | default: "h-10 py-2 px-4", 24 | sm: "h-9 px-3 rounded-md", 25 | lg: "h-11 px-8 rounded-md", 26 | icon: "h-10 w-10", 27 | }, 28 | }, 29 | defaultVariants: { 30 | variant: "default", 31 | size: "default", 32 | }, 33 | } 34 | ) 35 | 36 | export interface ButtonProps 37 | extends React.ButtonHTMLAttributes, 38 | VariantProps { 39 | asChild?: boolean 40 | } 41 | 42 | const Button = React.forwardRef( 43 | ({ className, variant, size, asChild = false, ...props }, ref) => { 44 | const Comp = asChild ? Slot : "button" 45 | return ( 46 | 51 | ) 52 | } 53 | ) 54 | Button.displayName = "Button" 55 | 56 | export { Button, buttonVariants } 57 | -------------------------------------------------------------------------------- /website/src/components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /website/src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /website/src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const labelVariants = cva( 8 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 9 | ) 10 | 11 | const Label = React.forwardRef< 12 | React.ElementRef, 13 | React.ComponentPropsWithoutRef & 14 | VariantProps 15 | >(({ className, ...props }, ref) => ( 16 | 21 | )) 22 | Label.displayName = LabelPrimitive.Root.displayName 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /website/src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as PopoverPrimitive from "@radix-ui/react-popover" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Popover = PopoverPrimitive.Root 7 | 8 | const PopoverTrigger = PopoverPrimitive.Trigger 9 | 10 | const PopoverContent = React.forwardRef< 11 | React.ElementRef, 12 | React.ComponentPropsWithoutRef 13 | >(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( 14 | 15 | 25 | 26 | )) 27 | PopoverContent.displayName = PopoverPrimitive.Content.displayName 28 | 29 | export { Popover, PopoverTrigger, PopoverContent } 30 | -------------------------------------------------------------------------------- /website/src/components/ui/progress.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as ProgressPrimitive from "@radix-ui/react-progress" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Progress = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >(({ className, value, ...props }, ref) => ( 10 | 18 | 22 | 23 | )) 24 | Progress.displayName = ProgressPrimitive.Root.displayName 25 | 26 | export { Progress } 27 | -------------------------------------------------------------------------------- /website/src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const Separator = React.forwardRef< 7 | React.ElementRef, 8 | React.ComponentPropsWithoutRef 9 | >( 10 | ( 11 | { className, orientation = "horizontal", decorative = true, ...props }, 12 | ref 13 | ) => ( 14 | 25 | ) 26 | ) 27 | Separator.displayName = SeparatorPrimitive.Root.displayName 28 | 29 | export { Separator } 30 | -------------------------------------------------------------------------------- /website/src/components/ui/table.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Table = React.forwardRef< 6 | HTMLTableElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
10 | 15 | 16 | )) 17 | Table.displayName = "Table" 18 | 19 | const TableHeader = React.forwardRef< 20 | HTMLTableSectionElement, 21 | React.HTMLAttributes 22 | >(({ className, ...props }, ref) => ( 23 | 24 | )) 25 | TableHeader.displayName = "TableHeader" 26 | 27 | const TableBody = React.forwardRef< 28 | HTMLTableSectionElement, 29 | React.HTMLAttributes 30 | >(({ className, ...props }, ref) => ( 31 | 36 | )) 37 | TableBody.displayName = "TableBody" 38 | 39 | const TableFooter = React.forwardRef< 40 | HTMLTableSectionElement, 41 | React.HTMLAttributes 42 | >(({ className, ...props }, ref) => ( 43 | 48 | )) 49 | TableFooter.displayName = "TableFooter" 50 | 51 | const TableRow = React.forwardRef< 52 | HTMLTableRowElement, 53 | React.HTMLAttributes 54 | >(({ className, ...props }, ref) => ( 55 | 63 | )) 64 | TableRow.displayName = "TableRow" 65 | 66 | const TableHead = React.forwardRef< 67 | HTMLTableCellElement, 68 | React.ThHTMLAttributes 69 | >(({ className, ...props }, ref) => ( 70 |
78 | )) 79 | TableHead.displayName = "TableHead" 80 | 81 | const TableCell = React.forwardRef< 82 | HTMLTableCellElement, 83 | React.TdHTMLAttributes 84 | >(({ className, ...props }, ref) => ( 85 | 90 | )) 91 | TableCell.displayName = "TableCell" 92 | 93 | const TableCaption = React.forwardRef< 94 | HTMLTableCaptionElement, 95 | React.HTMLAttributes 96 | >(({ className, ...props }, ref) => ( 97 |
102 | )) 103 | TableCaption.displayName = "TableCaption" 104 | 105 | export { 106 | Table, 107 | TableHeader, 108 | TableBody, 109 | TableFooter, 110 | TableHead, 111 | TableRow, 112 | TableCell, 113 | TableCaption, 114 | } 115 | -------------------------------------------------------------------------------- /website/src/config.ts: -------------------------------------------------------------------------------- 1 | export const serverUrl = process.env.SERVER_URL; 2 | export const grafanaUrl = process.env.GRAFANA_URL; 3 | export const localServerUrl = process.env.LOCAL_SERVER_URL; 4 | export const GITHUB_CLIENT_ID = process.env.GITHUB_CLIENT_ID; 5 | export const GITHUB_REDIRECT_URI = process.env.GITHUB_REDIRECT_URI; 6 | export const DISCORD_CLIENT_ID = process.env.DISCORD_CLIENT_ID; 7 | export const DISCORD_REDIRECT_URI = process.env.DISCORD_REDIRECT_URI; 8 | export const GOOGLE_CLIENT_ID = process.env.GOOGLE_CLIENT_ID; 9 | export const GOOGLE_REDIRECT_URI = process.env.GOOGLE_REDIRECT_URI; 10 | -------------------------------------------------------------------------------- /website/src/config/meta.ts: -------------------------------------------------------------------------------- 1 | import { Metadata } from "next"; 2 | import { siteConfig } from "./site"; 3 | 4 | export const meta: Metadata = { 5 | title: { 6 | default: siteConfig.name, 7 | template: `%s - ${siteConfig.name}`, 8 | }, 9 | description: siteConfig.description, 10 | themeColor: [ 11 | { media: "(prefers-color-scheme: light)", color: "white" }, 12 | { media: "(prefers-color-scheme: dark)", color: "black" }, 13 | ], 14 | icons: { 15 | icon: "/favicon.png", 16 | shortcut: "/favicon-16x16.png", 17 | apple: "/apple-touch-icon.png", 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /website/src/config/site.ts: -------------------------------------------------------------------------------- 1 | export type SiteConfig = typeof siteConfig 2 | 3 | export const siteConfig = { 4 | name: "Storage Box", 5 | description: 6 | "A Simple File Storage Service", 7 | } 8 | 9 | -------------------------------------------------------------------------------- /website/src/layout/SiteHeader.tsx: -------------------------------------------------------------------------------- 1 | import { cookies } from "next/headers" 2 | import Link from "next/link" 3 | 4 | import { Logo } from "@/components/Logo" 5 | 6 | // TODO get server session for the login text 7 | export function SiteHeader() { 8 | const cookieStore = cookies() 9 | const token = cookieStore.get("token")?.value 10 | // if(token)redirect("/dashboard") 11 | return ( 12 |
13 |
14 |
15 |
16 | 17 | Documentation 18 |
19 | {/*
*/} 20 | {/* */} 21 | {/*
*/} 22 | {/* */} 23 | {token ? ( 24 | Dashboard 25 | ) : ( 26 | Login 27 | )} 28 |
29 |
30 |
31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /website/src/layout/SiteHeaderLoggedIn.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { getAppState, updateAppState } from "@/state/state" 4 | import { MenuIcon } from "lucide-react" 5 | 6 | import { UserAvatar } from "@/components/Avatar" 7 | import { Search } from "@/components/Search" 8 | 9 | // TODO get server session for the login text 10 | export function SiteHeaderLoggedIn() { 11 | let state = getAppState() 12 | return ( 13 |
14 |
15 |
16 | updateAppState({ showSidebar: !state.showSidebar })} 18 | className="visible lg:hidden" 19 | /> 20 |
21 | 22 |
23 | 24 | 25 |
26 |
27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /website/src/layout/sidebar-nav-admin.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | AreaChart, 3 | FileIcon, 4 | HeartIcon, 5 | HistoryIcon, 6 | LayoutDashboardIcon, 7 | Trash2Icon, 8 | UsersIcon, 9 | } from "lucide-react" 10 | 11 | type Nav = { text: string; href: string; icon: JSX.Element }[] 12 | 13 | export const sidebarNavAdmin: Nav = [ 14 | { 15 | text: "Overview", 16 | href: "/admin", 17 | icon: , 18 | }, 19 | { 20 | text: "Users", 21 | href: "/admin/users", 22 | icon: , 23 | }, 24 | { 25 | text: "Files", 26 | href: "/admin/files", 27 | icon: , 28 | }, 29 | ] 30 | -------------------------------------------------------------------------------- /website/src/layout/sidebar-nav.tsx: -------------------------------------------------------------------------------- 1 | 2 | import { 3 | Book, 4 | CameraIcon, 5 | FoldersIcon, 6 | Github, 7 | HeartIcon, 8 | HistoryIcon, 9 | ImageIcon, 10 | LayoutDashboardIcon, 11 | TabletSmartphoneIcon, 12 | Trash2Icon, 13 | Users2, 14 | VideoIcon, 15 | } from "lucide-react" 16 | 17 | type Section = { 18 | title: string 19 | btns: { text: string; href: string; icon: JSX.Element }[] 20 | } 21 | 22 | export const sidebarNav: Section[] = [ 23 | { 24 | title: "Dashboard", 25 | btns: [ 26 | { 27 | text: "Dashboard", 28 | href: "/dashboard", 29 | icon: , 30 | }, 31 | { 32 | text: "Recent", 33 | href: "/dashboard/recent", 34 | icon: , 35 | }, 36 | { 37 | text: "Favorites", 38 | href: "/dashboard/favorites", 39 | icon: , 40 | }, 41 | { 42 | text: "Trash", 43 | href: "/dashboard/trash", 44 | icon: , 45 | }, 46 | ], 47 | }, 48 | { 49 | title: "Media", 50 | btns: [ 51 | { 52 | text: "All media", 53 | href: "/dashboard/all-media", 54 | icon: , 55 | }, 56 | { 57 | text: "Images", 58 | href: "/dashboard/images", 59 | icon: , 60 | }, 61 | { 62 | text: "Videos", 63 | href: "/dashboard/videos", 64 | icon: , 65 | }, 66 | { 67 | text: "Albums", 68 | href: "/dashboard/albums", 69 | icon: , 70 | }, 71 | ], 72 | }, 73 | { 74 | title: "Shared", 75 | btns: [ 76 | { 77 | text: "Devices", 78 | href: "/dashboard/devices", 79 | icon: , 80 | }, 81 | { 82 | text: "Team", 83 | href: "/dashboard/team", 84 | icon: , 85 | }, 86 | ], 87 | }, 88 | ] 89 | -------------------------------------------------------------------------------- /website/src/lib/fonts.ts: -------------------------------------------------------------------------------- 1 | import { JetBrains_Mono as FontMono, Inter as FontSans } from "next/font/google" 2 | 3 | export const fontSans = FontSans({ 4 | subsets: ["latin"], 5 | variable: "--font-sans", 6 | }) 7 | 8 | export const fontMono = FontMono({ 9 | subsets: ["latin"], 10 | variable: "--font-mono", 11 | }) 12 | -------------------------------------------------------------------------------- /website/src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { serverUrl } from "@/config" 2 | import { clsx, type ClassValue } from "clsx" 3 | import { twMerge } from "tailwind-merge" 4 | 5 | export function cn(...inputs: ClassValue[]) { 6 | return twMerge(clsx(inputs)) 7 | } 8 | 9 | export const imageTypes = ["image", "png"] 10 | export const videoTypes = ["video", "mp4"] 11 | export const textTypes = [ 12 | "textfile", 13 | "text", 14 | "pdf", 15 | "docx", 16 | "application/octet-stream", 17 | "json", 18 | ] 19 | 20 | export type FinalType = "image" | "video" | "text" | "unknown" 21 | 22 | export function getFileType(input?: string): FinalType { 23 | if (!input) return "unknown" 24 | if (imageTypes.some((type) => input.includes(type))) { 25 | return "image" 26 | } else if (videoTypes.some((type) => input.includes(type))) { 27 | return "video" 28 | } else if (textTypes.some((type) => input.includes(type))) { 29 | return "text" 30 | } else { 31 | return "unknown" 32 | } 33 | } 34 | 35 | export function handleDownload({ 36 | name, 37 | id, 38 | token, 39 | }: { 40 | token: string 41 | id: string 42 | name: string 43 | }) { 44 | fetch(serverUrl + "/files/" + id + "?token=" + token) 45 | .then((response) => response.blob()) 46 | .then((blob) => { 47 | const url = window.URL.createObjectURL(blob) 48 | const a = document.createElement("a") 49 | a.href = url 50 | a.download = name 51 | document.body.appendChild(a) 52 | a.click() 53 | window.URL.revokeObjectURL(url) 54 | document.body.removeChild(a) 55 | }) 56 | .catch((error) => { 57 | console.error("Error downloading the file:", error) 58 | }) 59 | } 60 | 61 | export function bytesToMB(bytes: number, decimalPlaces = 2) { 62 | bytes=parseInt(bytes.toString()) 63 | if (bytes === 0) return "0 MB" 64 | 65 | const k = 1024 66 | const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"] 67 | const i = Math.floor(Math.log(bytes) / Math.log(k)) 68 | const formattedValue = parseFloat( 69 | (bytes / Math.pow(k, i)).toFixed(decimalPlaces) 70 | ) 71 | 72 | return formattedValue + " " + sizes[i] 73 | } 74 | const totalSize = 500 * 1024 * 1024 75 | export function calculatePercentage( 76 | sizeInBytes: number, 77 | totalSizeInBytes = totalSize, 78 | decimalPlaces = 2 79 | ): number { 80 | if (totalSizeInBytes === 0) return 0 81 | 82 | const percentage = (sizeInBytes / totalSizeInBytes) * 100 83 | return parseFloat(percentage.toFixed(decimalPlaces)) 84 | } 85 | -------------------------------------------------------------------------------- /website/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css" 2 | import type { AppProps } from "next/app" 3 | 4 | function MyApp({ Component, pageProps }: AppProps) { 5 | return ( 6 | <> 7 | 8 | 9 | ) 10 | } 11 | 12 | export default MyApp 13 | -------------------------------------------------------------------------------- /website/src/pages/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "documentation": { 3 | "type": "page", 4 | "title": "Documentation", 5 | "display": "hidden" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /website/src/pages/documentation/_meta.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "Introduction", 3 | "locally": "Run locally", 4 | "vps": "Run on a VPS", 5 | "docker": "docker-compose.yml", 6 | "ansible": "Ansible", 7 | "conclusion": "Conclusion" 8 | } 9 | -------------------------------------------------------------------------------- /website/src/pages/documentation/conclusion.mdx: -------------------------------------------------------------------------------- 1 | # Conclusion 2 | 3 | In summary, working on this project has been a great learning experience. It allowed me to gain valuable insights into Go, Docker, Ansible, and other essential technologies for modern web development. However, there were some unique challenges along the way: 4 | 5 | - I encountered issues with running Postgres in Docker, as it repeatedly attempted to execute random commands within the container. As a solution, I had to run Postgres separately, outside of Docker. Meanwhile, the website and server containers had to utilize the host networking mode for seamless interaction. 6 | 7 | - When it came to data modeling and working with the Postgres database in Go, things got a bit tricky. I initially chose Prisma, but it proved to be more complicated than expected. It's worth noting that Prisma doesn't offer the best developer experience in Go because there is no official library maintained by the team, like their TypeScript library. 8 | 9 | In the future, I plan to explore GORM, a more developer-friendly option. 10 | -------------------------------------------------------------------------------- /website/src/pages/documentation/docker.mdx: -------------------------------------------------------------------------------- 1 | # docker-compose.yml 2 | 3 | The Docker Compose file defines services that use a shared network created by Docker, enabling seamless communication between these services. 4 | 5 | ### Postgres 6 | Running on port 5432 7 | ```yaml 8 | postgres: 9 | image: postgres:latest 10 | container_name: storagebox-postgres 11 | environment: 12 | POSTGRES_USER: postgres 13 | POSTGRES_PASSWORD: 123 14 | POSTGRES_DB: postgres 15 | ports: 16 | - "5432:5432" 17 | volumes: 18 | - ./data/pg-data:/var/lib/postgresql/data 19 | ``` 20 | ### Redis 21 | Running on port 6379 22 | ```yaml 23 | redis: 24 | image: redis:latest 25 | container_name: storagebox-redis 26 | ports: 27 | - "6379:6379" 28 | ``` 29 | ### Server 30 | The backend, port 4000 31 | ```yaml 32 | server: 33 | container_name: storagebox-server 34 | build: 35 | context: ./server 36 | dockerfile: Dockerfile 37 | ports: 38 | - "4000:4000" 39 | volumes: 40 | - ./data/uploads:/app/uploads 41 | ``` 42 | ### Website 43 | The frontend, port 4001 44 | ```yaml 45 | website: 46 | container_name: storagebox-website 47 | build: 48 | context: ./website 49 | dockerfile: Dockerfile 50 | ports: 51 | - "4001:4001" 52 | ``` 53 | ### Prometheus 54 | Prometheus for storing and monitoring metrics data, port 9090. 55 | ```yaml 56 | prometheus: 57 | image: prom/prometheus 58 | container_name: prometheus 59 | ports: 60 | - "9090:9090" 61 | volumes: 62 | - ./prometheus.yml:/etc/prometheus/prometheus.yml 63 | - ./data/prometheus-data:/etc/prometheus 64 | ``` 65 | 66 | ### Grafana 67 | Grafana for visualizing and analyzing metrics, port 3000. 68 | ```yaml 69 | grafana: 70 | image: grafana/grafana-oss:latest 71 | container_name: grafana 72 | environment: 73 | GF_SECURITY_ALLOW_EMBEDDING: true 74 | GF_AUTH_ANONYMOUS_ENABLED: true 75 | ports: 76 | - "3000:3000" 77 | volumes: 78 | - grafana-data:/var/lib/grafana 79 | restart: unless-stopped 80 | ``` 81 | ### Node Exporter 82 | Node Exporter for collecting system-level metrics, port 9100. 83 | ```yaml 84 | node_exporter: 85 | image: quay.io/prometheus/node-exporter:latest 86 | container_name: node_exporter 87 | ports: 88 | - "9100:9100" 89 | command: 90 | - '--path.rootfs=/host' 91 | pid: host 92 | restart: unless-stopped 93 | volumes: 94 | - './data/node_exporter-data:/host:ro,rslave' 95 | ``` 96 | 97 | -------------------------------------------------------------------------------- /website/src/pages/documentation/index.mdx: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | StorageBox a simple file storage service. 4 | 5 | ## Features 6 | 7 | - Authentication with username/password and social logins (Google, GitHub, and Discord). 8 | - Resumable file uploads using Tus. 9 | - Users can create folders to organize and store their files. 10 | 11 | ## Tech Stack 12 | 13 | - Next.js 14 | - React Query 15 | - Uppy with Tus Plugin for resumable file uploads 16 | - Shadcn-ui and Tailwind CSS for components and styling 17 | - Go for the backend with Tus for file uploads 18 | - Postgres (primary database) and Redis (rate limiting) 19 | - Prisma as an ORM 20 | - Grafana, Prometheus for server statistics with node_exporter 21 | - Ansible for automating server provisioning 22 | - Nginx as a reverse proxy 23 | - Docker and Docker Compose for containerizing all the 7 services required to run this project 24 | 25 | 26 | -------------------------------------------------------------------------------- /website/src/pages/documentation/locally.mdx: -------------------------------------------------------------------------------- 1 | ## How to Run Locally 2 | 3 | **Requirements** 4 | 5 | - Docker and Docker Compose: [Installation Instructions](https://docs.docker.com/get-docker/) 6 | 7 | To run the project locally, follow these steps: 8 | 9 | 1. Clone the project: `git clone https://github.com/AlandSleman/StorageBox` 10 | 11 | 2. Change to the `server/` directory. Copy the contents of the `.env.example` file into a new file named `.env`. Default values will work for running locally. 12 | 13 | 3. Change to the `website/` directory. Copy the contents of the `.env.example` file into a new file named `.env`. Default values will work for running locally. 14 | 15 | 4. Run the following command to spawn the Docker containers: `docker-compose up` 16 | 17 | Make sure to visit `localhost:4001` login with the default credentals`user:admin, pass:admin` this user has a special role which can delete files and users in the Admin Dashboard so make sure to change the password. 18 | 19 | Also Make sure to visit `localhost:3000` login with the default credentals`user:admin, pass:admin`, configure Grafana and Prometheus by following these steps: 20 | 21 | - Add the Prometheus data source `Home > Connections > Data sources` The URL would be `http://prometheus:9090`. 22 | 23 | - Import the Node Exporter dashboard `Home > Dashboards > Import dashboard` the dashboard ID would be `1860`. 24 | -------------------------------------------------------------------------------- /website/src/pages/documentation/vps.mdx: -------------------------------------------------------------------------------- 1 | ## How to Run on a VPS 2 | 3 | **Requirements** 4 | 5 | - Ansible: [Installation Instructions](https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html) 6 | 7 | To run the project on a VPS, follow these steps: 8 | 9 | 1. Clone the project: `git clone https://github.com/AlandSleman/StorageBox` 10 | 11 | 2. Change to the `server/` directory. Copy the contents of the `.env.example` file into a new file named `.env`. 12 | 13 | 3. Change to the `website/` directory. Copy the contents of the `.env.example` file into a new file named `.env`. 14 | 15 | 4. Change to the `ansible/` directory and edit `vars.yml` to your own values. This file contains the variables used for configuring Nginx with Ansible. 16 | 17 | 5. You'll also need to update the Nginx maximum upload limit. The default is 1MB. Refer to [this guide](https://stackoverflow.com/questions/26717013/how-to-edit-nginx-conf-to-increase-file-size-upload) to update the limit. 18 | 19 | 6. Change to the `ansible/` directory and run the following command to run the Ansible playbook: `ansible-playbook playbook.yml` 20 | 21 | This playbook will install the required software, spawn Docker containers, and configure Nginx for you. You may also need to configure your VPS firewall. Please note this playbook has only been tested on Linux(Ubuntu). 22 | 23 | Make sure to visit `website_url` which you configured, login with the default credentals `user:admin, pass:admin` this user has a special role which can delete files and users in the Admin Dashboard so make sure to change the password. 24 | 25 | Also Make sure to visit `grafana_url` which you configured, login with the default credentals `user:admin, pass:admin`, configure Grafana and Prometheus by following these steps: 26 | 27 | - Add the Prometheus data source `Home > Connections > Data sources` The URL would be `http://prometheus:9090`. 28 | 29 | - Import the Node Exporter dashboard `Home > Dashboards > Import dashboard` the dashboard ID would be `1860`. 30 | -------------------------------------------------------------------------------- /website/src/queryKeys.ts: -------------------------------------------------------------------------------- 1 | export const queryKeys={data:"data",overview:"overview",users:"users"} 2 | -------------------------------------------------------------------------------- /website/src/session/SetSession.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { getAppState, updateAppState } from "@/state/state" 4 | import { Session, UserData } from "@/types" 5 | 6 | export function SetSession(props: { 7 | session: Session | null 8 | userData: UserData | null 9 | }) { 10 | const state = getAppState() 11 | 12 | if (state.alreadySetSession) return <> 13 | 14 | if (!state.session) { 15 | updateAppState({ session: props.session }) 16 | updateAppState({ userData: props.userData }) 17 | } 18 | updateAppState({ alreadySetSession: true }) 19 | return <> 20 | } 21 | -------------------------------------------------------------------------------- /website/src/state/state.ts: -------------------------------------------------------------------------------- 1 | import { File, Folder, Session, UserData, ViewAs } from "@/types" 2 | import { useStore } from "@nanostores/react" 3 | import { atom } from "nanostores" 4 | 5 | export type State = { 6 | session: Session | null 7 | userData: UserData | null 8 | alreadySetSession: boolean 9 | viewAs: ViewAs 10 | initialDataFetched: boolean 11 | showSidebar: boolean 12 | selectedFolder: Folder | null 13 | parents: Folder[] 14 | folders: Folder[] 15 | files: File[] 16 | selectedFile: File | null 17 | searchQuery: string 18 | } 19 | 20 | export const $appState = atom({ 21 | session: null, 22 | userData: null, 23 | selectedFile: null, 24 | alreadySetSession: false, 25 | showSidebar: false, 26 | viewAs: "list", 27 | initialDataFetched: false, 28 | parents: [], 29 | folders: [], 30 | files: [], 31 | selectedFolder: null, 32 | searchQuery: "", 33 | }) 34 | 35 | export function getAppState() { 36 | return useStore($appState) 37 | } 38 | 39 | export function updateAppState(changes: Partial) { 40 | $appState.set({ ...$appState.get(), ...changes }) 41 | } 42 | -------------------------------------------------------------------------------- /website/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | .loader { 6 | width: 10em; 7 | height: 10em; 8 | display: block; 9 | position: relative; 10 | animation: spinRing 900ms linear infinite; 11 | } 12 | .loader::after{ 13 | content: ''; 14 | position: absolute; 15 | top: 0; 16 | left: 0; 17 | bottom: 0; 18 | right: 0; 19 | margin: auto; 20 | height: auto; 21 | width: auto; 22 | border: 4px solid #1E4D92; 23 | border-radius: 50%; 24 | clip-path: polygon(50% 50%, 50% 0%, 100% 0%,100% 12.5%); 25 | animation: spinRingInner 900ms cubic-bezier(0.770, 0.000, 0.175, 1.000) infinite; 26 | } 27 | @keyframes spinRing { 28 | 0% { transform: rotate(0deg); } 29 | 100% { transform: rotate(360deg); } 30 | } 31 | @keyframes spinRingInner { 32 | 0% { transform: rotate(-180deg); } 33 | 50% { transform: rotate(-160deg); } 34 | 100% { transform: rotate(180deg); } 35 | } 36 | @layer base { 37 | :root { 38 | --background: 0 0% 100%; 39 | --foreground: 222.2 47.4% 11.2%; 40 | 41 | --muted: 210 40% 96.1%; 42 | --muted-foreground: 215.4 16.3% 46.9%; 43 | 44 | --popover: 0 0% 100%; 45 | --popover-foreground: 222.2 47.4% 11.2%; 46 | --border: 215.294, 19%, 35%; 47 | --input: 214.3 31.8% 91.4%; 48 | 49 | --card: 0 0% 100%; 50 | --card-foreground: 222.2 47.4% 11.2%; 51 | 52 | --primary: 222.2 47.4% 11.2%; 53 | --primary-foreground: 210 40% 98%; 54 | 55 | --secondary: 210 40% 96.1%; 56 | --secondary-foreground: 222.2 47.4% 11.2%; 57 | 58 | --accent: 210 40% 96.1%; 59 | --accent-foreground: 222.2 47.4% 11.2%; 60 | 61 | --destructive: 0 100% 50%; 62 | --destructive-foreground: 210 40% 98%; 63 | 64 | --ring: 215 20.2% 65.1%; 65 | 66 | --radius: 0.5rem; 67 | } 68 | 69 | .dark { 70 | --background: 224 71% 4%; 71 | --foreground: 213 31% 91%; 72 | 73 | --muted: 223 47% 11%; 74 | --muted-foreground: 215.4 16.3% 56.9%; 75 | 76 | --accent: 216 34% 17%; 77 | --accent-foreground: 210 40% 98%; 78 | 79 | --popover: 224 71% 4%; 80 | --popover-foreground: 215 20.2% 65.1%; 81 | 82 | --border: 215.294, 19%, 35%; 83 | --input: 216 34% 17%; 84 | 85 | --card: 224 71% 4%; 86 | --card-foreground: 213 31% 91%; 87 | 88 | --primary: 210 40% 98%; 89 | --primary-foreground: 222.2 47.4% 1.2%; 90 | 91 | --secondary: 222.2 47.4% 11.2%; 92 | --secondary-foreground: 210 40% 98%; 93 | 94 | --destructive: 0 63% 31%; 95 | --destructive-foreground: 210 40% 98%; 96 | 97 | --ring: 216 34% 17%; 98 | 99 | --radius: 0.5rem; 100 | } 101 | } 102 | 103 | @layer base { 104 | * { 105 | @apply border-border; 106 | } 107 | body { 108 | @apply bg-background text-foreground; 109 | font-feature-settings: "rlig" 1, "calt" 1; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /website/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface ErrorResponse { 2 | statusCode: number 3 | code: number 4 | message: string 5 | } 6 | export type ViewAs = "list" | "grid" 7 | 8 | export type ErrorRes = { message: string } 9 | export interface File { 10 | id: string 11 | name: string 12 | type: string 13 | size: number 14 | userId: string 15 | folderId: string 16 | createdAt: string 17 | updatedAt: string 18 | } 19 | 20 | export interface Folder { 21 | id: string 22 | name: string 23 | userId: string 24 | createdAt: string 25 | updatedAt: string 26 | parentId?: string 27 | } 28 | 29 | export type Role = "user" | "admin" 30 | export type Session = { id: string; token: string; role: Role } 31 | export type UserData = { 32 | id: string 33 | avatar: string 34 | provider: "password" | "google" | "discord" | "github" 35 | storage: number 36 | role: Role 37 | } 38 | 39 | export interface User { 40 | id: string 41 | username: string 42 | avatar: string 43 | role: string 44 | password: string 45 | email: string 46 | provider: string 47 | storage: number 48 | createdAt: Date 49 | updatedAt: Date 50 | folders?: Folder[] 51 | files?: File[] 52 | } 53 | -------------------------------------------------------------------------------- /website/src/types/nav.ts: -------------------------------------------------------------------------------- 1 | export interface NavItem { 2 | title: string 3 | href?: string 4 | disabled?: boolean 5 | external?: boolean 6 | } 7 | -------------------------------------------------------------------------------- /website/tailwind.config.js: -------------------------------------------------------------------------------- 1 | const { fontFamily } = require("tailwindcss/defaultTheme") 2 | 3 | /** @type {import('tailwindcss').Config} */ 4 | module.exports = { 5 | darkMode: ["class"], 6 | content: ["src/**/*.{ts,tsx}"], 7 | theme: { 8 | container: { 9 | center: true, 10 | padding: "2rem", 11 | screens: { 12 | "2xl": "1400px", 13 | }, 14 | }, 15 | extend: { 16 | colors: { 17 | border: "hsl(var(--border))", 18 | input: "hsl(var(--input))", 19 | ring: "hsl(var(--ring))", 20 | background: "hsl(var(--background))", 21 | foreground: "hsl(var(--foreground))", 22 | primary: { 23 | DEFAULT: "hsl(var(--primary))", 24 | foreground: "hsl(var(--primary-foreground))", 25 | }, 26 | secondary: { 27 | DEFAULT: "hsl(var(--secondary))", 28 | foreground: "hsl(var(--secondary-foreground))", 29 | }, 30 | destructive: { 31 | DEFAULT: "hsl(var(--destructive))", 32 | foreground: "hsl(var(--destructive-foreground))", 33 | }, 34 | muted: { 35 | DEFAULT: "hsl(var(--muted))", 36 | foreground: "hsl(var(--muted-foreground))", 37 | }, 38 | accent: { 39 | DEFAULT: "hsl(var(--accent))", 40 | foreground: "hsl(var(--accent-foreground))", 41 | }, 42 | popover: { 43 | DEFAULT: "hsl(var(--popover))", 44 | foreground: "hsl(var(--popover-foreground))", 45 | }, 46 | card: { 47 | DEFAULT: "hsl(var(--card))", 48 | foreground: "hsl(var(--card-foreground))", 49 | }, 50 | }, 51 | borderRadius: { 52 | lg: `var(--radius)`, 53 | md: `calc(var(--radius) - 2px)`, 54 | sm: "calc(var(--radius) - 4px)", 55 | }, 56 | fontFamily: { 57 | sans: ["var(--font-sans)", ...fontFamily.sans], 58 | }, 59 | keyframes: { 60 | "accordion-down": { 61 | from: { height: 0 }, 62 | to: { height: "var(--radix-accordion-content-height)" }, 63 | }, 64 | "accordion-up": { 65 | from: { height: "var(--radix-accordion-content-height)" }, 66 | to: { height: 0 }, 67 | }, 68 | }, 69 | animation: { 70 | "accordion-down": "accordion-down 0.2s ease-out", 71 | "accordion-up": "accordion-up 0.2s ease-out", 72 | }, 73 | }, 74 | }, 75 | plugins: [require("tailwindcss-animate")], 76 | } 77 | -------------------------------------------------------------------------------- /website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "noEmit": true, 9 | "incremental": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "baseUrl": ".", 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | }, 20 | "plugins": [ 21 | { 22 | "name": "next" 23 | } 24 | ], 25 | "strictNullChecks": true 26 | }, 27 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 28 | "exclude": ["node_modules"] 29 | } 30 | --------------------------------------------------------------------------------