├── website
├── .dockerignore
├── public
│ ├── doge.png
│ ├── avatar.png
│ ├── elon.jpeg
│ ├── favicon.ico
│ ├── favicon.png
│ ├── dashboard.png
│ ├── admin-dashboard.png
│ ├── vercel.svg
│ ├── thirteen.svg
│ └── next.svg
├── src
│ ├── queryKeys.ts
│ ├── components
│ │ ├── Spinner.tsx
│ │ ├── ReactQueryProvider.tsx
│ │ ├── theme-provider.tsx
│ │ ├── AlertNotImplemented.tsx
│ │ ├── HeadText.tsx
│ │ ├── icons
│ │ │ ├── X.tsx
│ │ │ ├── Ansible.tsx
│ │ │ ├── Docker.tsx
│ │ │ ├── Google.tsx
│ │ │ ├── Github.tsx
│ │ │ ├── Nginx.tsx
│ │ │ ├── Discord.tsx
│ │ │ ├── Prometheus.tsx
│ │ │ ├── Next.tsx
│ │ │ ├── Go.tsx
│ │ │ └── Redis.tsx
│ │ ├── theme-toggle.tsx
│ │ ├── tailwind-indicator.tsx
│ │ ├── GetFileIcon.tsx
│ │ ├── Search.tsx
│ │ ├── ui
│ │ │ ├── label.tsx
│ │ │ ├── progress.tsx
│ │ │ ├── separator.tsx
│ │ │ ├── input.tsx
│ │ │ ├── popover.tsx
│ │ │ ├── avatar.tsx
│ │ │ ├── alert.tsx
│ │ │ ├── button.tsx
│ │ │ ├── SidebarButton.tsx
│ │ │ ├── card.tsx
│ │ │ └── table.tsx
│ │ ├── mockDevices.ts
│ │ ├── RowAction.tsx
│ │ ├── main-nav.tsx
│ │ ├── FolderCard.tsx
│ │ ├── mockFiles.ts
│ │ ├── FileCard.tsx
│ │ ├── Logo.tsx
│ │ ├── Avatar.tsx
│ │ └── DeleteDialog.tsx
│ ├── types
│ │ ├── nav.ts
│ │ └── index.ts
│ ├── pages
│ │ ├── _meta.json
│ │ ├── documentation
│ │ │ ├── _meta.json
│ │ │ ├── index.mdx
│ │ │ ├── conclusion.mdx
│ │ │ ├── locally.mdx
│ │ │ ├── vps.mdx
│ │ │ └── docker.mdx
│ │ └── _app.tsx
│ ├── config
│ │ ├── site.ts
│ │ └── meta.ts
│ ├── api
│ │ ├── deleteFile.ts
│ │ ├── adminFiles.ts
│ │ ├── adminUsers.ts
│ │ ├── login.ts
│ │ ├── deleteUser.ts
│ │ ├── deleteUserFiles.ts
│ │ ├── changePass.ts
│ │ ├── adminOverview.ts
│ │ ├── githubLogin.ts
│ │ ├── getData.ts
│ │ ├── newFolder.ts
│ │ ├── delete.ts
│ │ ├── rename.ts
│ │ └── makeRequest.ts
│ ├── lib
│ │ ├── fonts.ts
│ │ └── utils.ts
│ ├── app
│ │ ├── (dashboard)
│ │ │ └── dashboard
│ │ │ │ ├── team
│ │ │ │ ├── page.tsx
│ │ │ │ └── DataTable.tsx
│ │ │ │ ├── recent
│ │ │ │ ├── page.tsx
│ │ │ │ └── DataTable.tsx
│ │ │ │ ├── trash
│ │ │ │ ├── page.tsx
│ │ │ │ └── DataTable.tsx
│ │ │ │ ├── devices
│ │ │ │ ├── page.tsx
│ │ │ │ └── DataTable.tsx
│ │ │ │ ├── favorites
│ │ │ │ ├── page.tsx
│ │ │ │ └── DataTable.tsx
│ │ │ │ ├── albums
│ │ │ │ ├── page.tsx
│ │ │ │ ├── AlbumCard.tsx
│ │ │ │ └── DataTable.tsx
│ │ │ │ ├── videos
│ │ │ │ └── page.tsx
│ │ │ │ ├── all-media
│ │ │ │ └── page.tsx
│ │ │ │ ├── images
│ │ │ │ └── page.tsx
│ │ │ │ └── page.tsx
│ │ ├── (home)
│ │ │ ├── login
│ │ │ │ └── page.tsx
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── (admin)
│ │ │ ├── admin
│ │ │ │ ├── files
│ │ │ │ │ ├── RowAction.tsx
│ │ │ │ │ ├── page.tsx
│ │ │ │ │ ├── DeleteFileDialog.tsx
│ │ │ │ │ └── FilesTable.tsx
│ │ │ │ ├── AdminCard.tsx
│ │ │ │ └── users
│ │ │ │ │ ├── RowAction.tsx
│ │ │ │ │ ├── page.tsx
│ │ │ │ │ ├── DeleteUserDialog.tsx
│ │ │ │ │ ├── DeleteUserFiles.tsx
│ │ │ │ │ └── UsersTable.tsx
│ │ │ └── layout.tsx
│ │ └── (auth)
│ │ │ ├── auth
│ │ │ ├── github
│ │ │ │ └── page.tsx
│ │ │ ├── discord
│ │ │ │ └── page.tsx
│ │ │ └── google
│ │ │ │ └── page.tsx
│ │ │ └── layout.tsx
│ ├── session
│ │ └── SetSession.tsx
│ ├── config.ts
│ ├── layout
│ │ ├── sidebar-nav-admin.tsx
│ │ ├── SiteHeaderLoggedIn.tsx
│ │ ├── SiteHeader.tsx
│ │ └── sidebar-nav.tsx
│ ├── state
│ │ └── state.ts
│ └── styles
│ │ └── globals.css
├── postcss.config.js
├── .prettierignore
├── next-env.d.ts
├── .env.example
├── components.json
├── Dockerfile
├── README.md
├── .gitignore
├── tsconfig.json
├── prettier.config.js
├── next.config.js
├── package.json
└── tailwind.config.js
├── .gitignore
├── server
├── .gitignore
├── prisma
│ ├── db
│ │ └── .gitignore
│ ├── migrations
│ │ ├── 20230929154118_
│ │ │ └── migration.sql
│ │ ├── 20230922225200_
│ │ │ └── migration.sql
│ │ ├── 20230914105609_add_role
│ │ │ └── migration.sql
│ │ ├── migration_lock.toml
│ │ ├── 20230912102841_size_bigint
│ │ │ └── migration.sql
│ │ ├── 20230929154603_
│ │ │ └── migration.sql
│ │ ├── 20230905184155_size
│ │ │ └── migration.sql
│ │ ├── 20230908025236_del
│ │ │ └── migration.sql
│ │ ├── 20230905141316_path
│ │ │ └── migration.sql
│ │ ├── 20230906092845_rm_path
│ │ │ └── migration.sql
│ │ ├── 20230929130803_
│ │ │ └── migration.sql
│ │ ├── 20230906072639_user_path
│ │ │ └── migration.sql
│ │ └── 20230904184641_h
│ │ │ └── migration.sql
│ ├── prisma.go
│ └── schema.prisma
├── build-and-run.sh
├── Dockerfile
├── utils
│ └── randomPath.go
├── middleware
│ ├── dir.go
│ ├── cors.go
│ ├── rate_limit.go
│ ├── auth.go
│ └── auth_admin.go
├── .env.example
├── handler
│ ├── session.go
│ ├── files.go
│ ├── admin_files.go
│ ├── admin_overview.go
│ ├── update_file.go
│ ├── update_folder.go
│ ├── admin_users.go
│ ├── admin_delete_file.go
│ ├── admin_delete_user.go
│ ├── new_folder.go
│ ├── admin_delete_all_user_files.go
│ ├── user_settings.go
│ ├── login.go
│ ├── delete_folder.go
│ └── delete_file.go
├── config
│ └── config.go
├── go.mod
├── main.go
└── auth
│ └── user.go
├── ansible
├── vars.yml
├── roles
│ └── install_prerequisites
│ │ └── tasks
│ │ └── main.yml
└── playbook.yml
├── prometheus.yml
├── License
└── docker-compose.yml
/website/.dockerignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | .next/
3 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | uploads
3 | pg-data
4 | prometheus
5 | data
6 |
--------------------------------------------------------------------------------
/server/.gitignore:
--------------------------------------------------------------------------------
1 | .env
2 | uploads/*
3 | pg-data/*
4 | server
5 | server.log
6 |
--------------------------------------------------------------------------------
/server/prisma/db/.gitignore:
--------------------------------------------------------------------------------
1 | # gitignore generated by Prisma Client Go. DO NOT EDIT.
2 | *_gen.go
3 |
--------------------------------------------------------------------------------
/website/public/doge.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KryptXBSA/StorageBox/HEAD/website/public/doge.png
--------------------------------------------------------------------------------
/website/src/queryKeys.ts:
--------------------------------------------------------------------------------
1 | export const queryKeys={data:"data",overview:"overview",users:"users"}
2 |
--------------------------------------------------------------------------------
/server/prisma/migrations/20230929154118_/migration.sql:
--------------------------------------------------------------------------------
1 | -- DropIndex
2 | DROP INDEX "User_email_key";
3 |
--------------------------------------------------------------------------------
/website/public/avatar.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KryptXBSA/StorageBox/HEAD/website/public/avatar.png
--------------------------------------------------------------------------------
/website/public/elon.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KryptXBSA/StorageBox/HEAD/website/public/elon.jpeg
--------------------------------------------------------------------------------
/website/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KryptXBSA/StorageBox/HEAD/website/public/favicon.ico
--------------------------------------------------------------------------------
/website/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KryptXBSA/StorageBox/HEAD/website/public/favicon.png
--------------------------------------------------------------------------------
/website/public/dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KryptXBSA/StorageBox/HEAD/website/public/dashboard.png
--------------------------------------------------------------------------------
/website/public/admin-dashboard.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/KryptXBSA/StorageBox/HEAD/website/public/admin-dashboard.png
--------------------------------------------------------------------------------
/website/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/website/src/components/Spinner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | export function Spinner() {
3 | return
4 | }
5 |
--------------------------------------------------------------------------------
/server/prisma/migrations/20230922225200_/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "User" ADD COLUMN "avatar" TEXT NOT NULL DEFAULT '';
3 |
--------------------------------------------------------------------------------
/website/src/types/nav.ts:
--------------------------------------------------------------------------------
1 | export interface NavItem {
2 | title: string
3 | href?: string
4 | disabled?: boolean
5 | external?: boolean
6 | }
7 |
--------------------------------------------------------------------------------
/server/prisma/migrations/20230914105609_add_role/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "User" ADD COLUMN "role" TEXT NOT NULL DEFAULT 'user';
3 |
--------------------------------------------------------------------------------
/website/src/pages/_meta.json:
--------------------------------------------------------------------------------
1 | {
2 | "documentation": {
3 | "type": "page",
4 | "title": "Documentation",
5 | "display": "hidden"
6 | }
7 | }
8 |
--------------------------------------------------------------------------------
/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"
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/.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/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/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/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/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 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/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 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/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 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/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 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/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 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/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/src/components/HeadText.tsx:
--------------------------------------------------------------------------------
1 | export function HeadText({ text }: { text: string }) {
2 | return (
3 | <>
4 |
13 | >
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/src/components/icons/X.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { SVGProps } from "react"
3 | const XIcon = (props: SVGProps) => (
4 |
10 |
11 |
12 | )
13 | export default XIcon
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/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/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.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/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/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/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/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/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 | setTheme(theme === "light" ? "dark" : "light")}
17 | >
18 |
19 |
20 | Toggle theme
21 |
22 | )
23 | }
24 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/public/thirteen.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
33 |
34 | Download
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
22 | {items?.map(
23 | (item, index) =>
24 | item.href && (
25 |
33 | {item.title}
34 |
35 | )
36 | )}
37 |
38 | ) : null}
39 |
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/website/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/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/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/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/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/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/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/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/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/(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 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/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 |
42 | )
43 | }
44 |
--------------------------------------------------------------------------------
/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 |
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 |
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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
44 |
45 |
46 |
47 |
48 | >
49 | ))}
50 |
51 |
52 | )
53 | }
54 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 && */}
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 |
--------------------------------------------------------------------------------
/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/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/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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/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 | e.stopPropagation()}
45 | variant="ghost"
46 | className="flex justify-start gap-4"
47 | >
48 |
49 | Delete User
50 |
51 |
52 |
53 |
54 | Delete {p.username}
55 |
56 |
57 | setOpen(false)} variant="ghost">
58 | Cancel
59 |
60 |
61 | Delete
62 |
63 |
64 |
65 |
66 | )
67 | }
68 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | e.stopPropagation()}
46 | variant="ghost"
47 | className="flex justify-start gap-4"
48 | >
49 |
50 | Delete Files
51 |
52 |
53 |
54 |
55 | Delete {p.username} Files
56 |
57 |
58 | setOpen(false)} variant="ghost">
59 | Cancel
60 |
61 |
62 | Delete
63 |
64 |
65 |
66 |
67 | )
68 | }
69 |
--------------------------------------------------------------------------------
/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 | e.stopPropagation()}
47 | variant="ghost"
48 | className="flex justify-start gap-4"
49 | >
50 |
51 | Delete File
52 |
53 |
54 |
55 |
56 | Delete {p.name}
57 |
58 |
59 | setOpen(false)} variant="ghost">
60 | Cancel
61 |
62 |
63 | Delete
64 |
65 |
66 |
67 |
68 | )
69 | }
70 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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/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/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/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | router.push("/dashboard/settings")}
70 | variant="ghost"
71 | className="w-full gap-2 border-none"
72 | >
73 |
74 | Settings
75 |
76 |
81 | LogOut
82 |
83 |
84 |
85 | )
86 | }
87 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 | e.stopPropagation()}
80 | variant="ghost"
81 | className="flex justify-start gap-4"
82 | >
83 |
84 | Delete
85 |
86 |
87 |
88 |
89 | Delete {p.name}
90 |
91 | Are you sure you want to delete this {p.isFolder?"Folder":"File"}
92 |
93 | setOpen(false)} variant="ghost">Cancel
94 | Delete
95 |
96 |
97 |
98 | )
99 | }
100 |
--------------------------------------------------------------------------------
/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 |
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/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/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/(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 |
--------------------------------------------------------------------------------