diff --git a/README.md b/README.md index c19826f..8d62a73 100644 --- a/README.md +++ b/README.md @@ -20,5 +20,5 @@ DB_HOST=localhost:5432 DB_NAME=filething DB_USER=postgres STORAGE_PATH=data ./fi To run filething in dev mode with a hot reloading Ui server and auto rebuilding backend server, run ```BASH -DB_HOST=localhost:5432 DB_NAME=filething DB_USER=postgres STORAGE_PATH=data CompileDaemon --build="go build -tags netgo,dev -ldflags=-s" --command=./filething --exclude-dir=data/ --exclude-dir=ui. --graceful-kill +DB_HOST=localhost:5432 DB_NAME=filething DB_USER=postgres STORAGE_PATH=data CompileDaemon --build="go build -tags netgo,dev -ldflags=-s" --command=./filething --exclude-dir=data/ --exclude-dir=ui/ --graceful-kill ``` diff --git a/go.mod b/go.mod index 0865568..9502d2c 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.23.0 require github.com/google/uuid v1.6.0 require ( + github.com/dustin/go-humanize v1.0.1 github.com/golang-jwt/jwt v3.2.2+incompatible // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/labstack/echo/v4 v4.12.0 diff --git a/go.sum b/go.sum index 85cbdf6..ff881e2 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY= github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= diff --git a/main.go b/main.go index 1c6b7d7..063f99b 100644 --- a/main.go +++ b/main.go @@ -8,11 +8,10 @@ import ( "filething/middleware" "filething/models" "filething/routes" - "filething/ui" "fmt" "net/http" "os" - "strings" + "time" "github.com/labstack/echo/v4" echoMiddleware "github.com/labstack/echo/v4/middleware" @@ -84,61 +83,32 @@ func main() { api.GET("/files/get/*", routes.GetFiles) api.GET("/files/download*", routes.GetFile) api.POST("/files/delete*", routes.DeleteFiles) + + admin := api.Group("/admin") + { + admin.Use(middleware.AdminMiddleware()) + admin.GET("/system-status", routes.SystemStatus) + admin.GET("/get-users/:page", routes.GetUsers) + admin.GET("/get-total-users", routes.GetUsersCount) + } } // redirects to the proper pages if you are trying to access one that expects you have/dont have an api key // this isnt explicitly required, but it provides a better experience than doing this same thing clientside e.Use(middleware.AuthCheckMiddleware) - // calls out to a function set by either server.go server_dev.go based on the value of the dev tag, and hosts + // calls out to a function set by either server.go server_dev.go based on the presence of the dev tag, and hosts // either the static files that get embedded into the binary in ui/embed.go or proxies the dev server that gets // run in the provided function initUi(e) - e.HTTPErrorHandler = customHTTPErrorHandler + routes.AppStartTime = time.Now().UTC() if err := e.Start(":1323"); err != nil && err != http.ErrServerClosed { fmt.Println("Error starting HTTP server:", err) } } -// Custom Error handling since Nuxt relies on the 404 page for dynamic pages we still want api routes to use the default -// error handling built into echo -func customHTTPErrorHandler(err error, c echo.Context) { - if he, ok := err.(*echo.HTTPError); ok && he.Code == http.StatusNotFound { - path := c.Request().URL.Path - - if !strings.HasPrefix(path, "/api") { - file, err := ui.DistDirFS.Open("404.html") - if err != nil { - c.Logger().Error(err) - } - - fileInfo, err := file.Stat() - if err != nil { - c.Logger().Error(err) - } - - fileBuf := make([]byte, fileInfo.Size()) - _, err = file.Read(fileBuf) - defer func() { - if err := file.Close(); err != nil { - panic(err) - } - }() - if err != nil { - c.Logger().Error(err) - panic(err) - } - - c.HTML(http.StatusNotFound, string(fileBuf)) - return - } - } - - c.Echo().DefaultHTTPErrorHandler(err, c) -} - // creates tables in the db if they dont already exist func createSchema(db *bun.DB) error { models := []interface{}{ diff --git a/middleware/admin.go b/middleware/admin.go new file mode 100644 index 0000000..440c16d --- /dev/null +++ b/middleware/admin.go @@ -0,0 +1,22 @@ +package middleware + +import ( + "filething/models" + "net/http" + + "github.com/labstack/echo/v4" +) + +func AdminMiddleware() echo.MiddlewareFunc { + return func(next echo.HandlerFunc) echo.HandlerFunc { + return func(c echo.Context) error { + user := c.Get("user").(*models.User) + + if !user.Admin { + return echo.NewHTTPError(http.StatusForbidden, "You are not an administrator") + } + + return next(c) + } + } +} diff --git a/middleware/route.go b/middleware/route.go index ec1483c..48b7553 100644 --- a/middleware/route.go +++ b/middleware/route.go @@ -41,6 +41,10 @@ func AuthCheckMiddleware(next echo.HandlerFunc) echo.HandlerFunc { return c.Redirect(http.StatusFound, "/login") } + if strings.Contains(path, "/admin") && !authenticated { + return c.Redirect(http.StatusFound, "/login") + } + return next(c) } } diff --git a/models/user.go b/models/user.go index cd4dd10..5baa656 100644 --- a/models/user.go +++ b/models/user.go @@ -1,6 +1,8 @@ package models import ( + "time" + "github.com/google/uuid" "github.com/uptrace/bun" ) @@ -18,13 +20,15 @@ type SignupData struct { type User struct { bun.BaseModel `bun:"table:users"` - ID uuid.UUID `bun:"id,pk,type:uuid,default:uuid_generate_v4()" json:"id"` - Username string `bun:"username,notnull,unique" json:"username"` - Email string `bun:"email,notnull,unique" json:"email"` - PasswordHash string `bun:"passwordHash,notnull" json:"-"` + ID uuid.UUID `bun:",pk,type:uuid,default:uuid_generate_v4()" json:"id"` + Username string `bun:",notnull,unique" json:"username"` + Email string `bun:",notnull,unique" json:"email"` + PasswordHash string `bun:",notnull" json:"-"` PlanID int64 `bun:"plan_id,notnull" json:"-"` Plan Plan `bun:"rel:belongs-to,join:plan_id=id" json:"plan"` Usage int64 `bun:"-" json:"usage"` + CreatedAt time.Time `bun:",nullzero,notnull,default:current_timestamp" json:"created_at"` + Admin bool `bin:"admin,type:bool" json:"is_admin"` } type Session struct { diff --git a/routes/admin.go b/routes/admin.go new file mode 100644 index 0000000..41606cb --- /dev/null +++ b/routes/admin.go @@ -0,0 +1,172 @@ +package routes + +import ( + "context" + "filething/models" + "fmt" + "net/http" + "runtime" + "strconv" + "time" + + "github.com/dustin/go-humanize" + "github.com/labstack/echo/v4" + "github.com/uptrace/bun" +) + +func GetUsers(c echo.Context) error { + db := c.Get("db").(*bun.DB) + + pageStr := c.Param("page") + page, err := strconv.Atoi(pageStr) + if err != nil { + return echo.NewHTTPError(http.StatusBadRequest, "Invalid page number") + } + + offset := page * 30 + limit := 30 + + var users []models.User + err = db.NewSelect(). + Model(&users). + Limit(limit). + Offset(offset). + Scan(context.Background()) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to retrieve users") + } + + return c.JSON(http.StatusOK, users) +} + +func GetUsersCount(c echo.Context) error { + db := c.Get("db").(*bun.DB) + + count, err := db.NewSelect().Model(&models.User{}).Count(context.Background()) + + if err != nil { + fmt.Println(err) + return echo.NewHTTPError(http.StatusInternalServerError, "Failed to retrieve users") + } + + return c.JSON(http.StatusOK, map[string]int{"total_users": count}) +} + +// Stolen from Gitea https://github.com/go-gitea/gitea +func SystemStatus(c echo.Context) error { + updateSystemStatus() + return c.JSON(http.StatusOK, map[string]interface{}{ + "uptime": sysStatus.StartTime, + "num_goroutine": sysStatus.NumGoroutine, + "cur_mem_usage": sysStatus.MemAllocated, + "total_mem_usage": sysStatus.MemTotal, + "mem_obtained": sysStatus.MemSys, + "ptr_lookup_times": sysStatus.Lookups, + "mem_allocations": sysStatus.MemMallocs, + "mem_frees": sysStatus.MemFrees, + "cur_heap_usage": sysStatus.HeapAlloc, + "heap_mem_obtained": sysStatus.HeapSys, + "heap_mem_idle": sysStatus.HeapIdle, + "heap_mem_inuse": sysStatus.HeapInuse, + "heap_mem_release": sysStatus.HeapReleased, + "heap_objects": sysStatus.HeapObjects, + "bootstrap_stack_usage": sysStatus.StackInuse, + "stack_mem_obtained": sysStatus.StackSys, + "mspan_structures_usage": sysStatus.MSpanInuse, + "mspan_structures_obtained": sysStatus.MSpanSys, + "mcache_structures_usage": sysStatus.MSpanInuse, + "mcache_structures_obtained": sysStatus.MCacheSys, + "buck_hash_sys": sysStatus.BuckHashSys, + "gc_sys": sysStatus.GCSys, + "other_sys": sysStatus.OtherSys, + "next_gc": sysStatus.NextGC, + "last_gc_time": sysStatus.LastGCTime, + "pause_total_ns": sysStatus.PauseTotalNs, + "pause_ns": sysStatus.PauseNs, + "num_gc": sysStatus.NumGC, + }) +} + +var AppStartTime time.Time +var sysStatus struct { + StartTime string + NumGoroutine int + + // General statistics. + MemAllocated string // bytes allocated and still in use + MemTotal string // bytes allocated (even if freed) + MemSys string // bytes obtained from system (sum of XxxSys below) + Lookups uint64 // number of pointer lookups + MemMallocs uint64 // number of mallocs + MemFrees uint64 // number of frees + + // Main allocation heap statistics. + HeapAlloc string // bytes allocated and still in use + HeapSys string // bytes obtained from system + HeapIdle string // bytes in idle spans + HeapInuse string // bytes in non-idle span + HeapReleased string // bytes released to the OS + HeapObjects uint64 // total number of allocated objects + + // Low-level fixed-size structure allocator statistics. + // Inuse is bytes used now. + // Sys is bytes obtained from system. + StackInuse string // bootstrap stacks + StackSys string + MSpanInuse string // mspan structures + MSpanSys string + MCacheInuse string // mcache structures + MCacheSys string + BuckHashSys string // profiling bucket hash table + GCSys string // GC metadata + OtherSys string // other system allocations + + // Garbage collector statistics. + NextGC string // next run in HeapAlloc time (bytes) + LastGCTime string // last run time + PauseTotalNs string + PauseNs string // circular buffer of recent GC pause times, most recent at [(NumGC+255)%256] + NumGC uint32 +} + +func updateSystemStatus() { + sysStatus.StartTime = AppStartTime.Format(time.RFC3339) + + m := new(runtime.MemStats) + runtime.ReadMemStats(m) + sysStatus.NumGoroutine = runtime.NumGoroutine() + + sysStatus.MemAllocated = FileSize(int64(m.Alloc)) + sysStatus.MemTotal = FileSize(int64(m.TotalAlloc)) + sysStatus.MemSys = FileSize(int64(m.Sys)) + sysStatus.Lookups = m.Lookups + sysStatus.MemMallocs = m.Mallocs + sysStatus.MemFrees = m.Frees + + sysStatus.HeapAlloc = FileSize(int64(m.HeapAlloc)) + sysStatus.HeapSys = FileSize(int64(m.HeapSys)) + sysStatus.HeapIdle = FileSize(int64(m.HeapIdle)) + sysStatus.HeapInuse = FileSize(int64(m.HeapInuse)) + sysStatus.HeapReleased = FileSize(int64(m.HeapReleased)) + sysStatus.HeapObjects = m.HeapObjects + + sysStatus.StackInuse = FileSize(int64(m.StackInuse)) + sysStatus.StackSys = FileSize(int64(m.StackSys)) + sysStatus.MSpanInuse = FileSize(int64(m.MSpanInuse)) + sysStatus.MSpanSys = FileSize(int64(m.MSpanSys)) + sysStatus.MCacheInuse = FileSize(int64(m.MCacheInuse)) + sysStatus.MCacheSys = FileSize(int64(m.MCacheSys)) + sysStatus.BuckHashSys = FileSize(int64(m.BuckHashSys)) + sysStatus.GCSys = FileSize(int64(m.GCSys)) + sysStatus.OtherSys = FileSize(int64(m.OtherSys)) + + sysStatus.NextGC = FileSize(int64(m.NextGC)) + sysStatus.LastGCTime = time.Unix(0, int64(m.LastGC)).Format(time.RFC3339) + sysStatus.PauseTotalNs = fmt.Sprintf("%.1fs", float64(m.PauseTotalNs)/1000/1000/1000) + sysStatus.PauseNs = fmt.Sprintf("%.3fs", float64(m.PauseNs[(m.NumGC+255)%256])/1000/1000/1000) + sysStatus.NumGC = m.NumGC +} + +func FileSize(s int64) string { + return humanize.IBytes(uint64(s)) +} diff --git a/routes/auth.go b/routes/auth.go index cda6654..e704f4b 100644 --- a/routes/auth.go +++ b/routes/auth.go @@ -64,6 +64,8 @@ func LoginHandler(c echo.Context) error { return c.JSON(http.StatusOK, user) } +var firstUserCreated *bool + func SignupHandler(c echo.Context) error { var signupData models.SignupData @@ -87,11 +89,22 @@ func SignupHandler(c echo.Context) error { return c.JSON(http.StatusInternalServerError, map[string]string{"message": "An unknown error occoured!"}) } + if firstUserCreated == nil { + count, err := db.NewSelect().Model((*models.User)(nil)).Count(context.Background()) + if err != nil { + return fmt.Errorf("failed to count plans: %w", err) + } + + firstUserCreated = new(bool) + *firstUserCreated = count != 0 + } + user := &models.User{ Username: signupData.Username, Email: signupData.Email, PasswordHash: string(hash), PlanID: 1, // basic 10GB plan + Admin: !*firstUserCreated, } _, err = db.NewInsert().Model(user).Exec(context.Background()) @@ -99,6 +112,10 @@ func SignupHandler(c echo.Context) error { return c.JSON(http.StatusConflict, map[string]string{"message": "A user with that email or username already exists!"}) } + if !*firstUserCreated { + *firstUserCreated = true + } + err = db.NewSelect().Model(user).WherePK().Relation("Plan").Scan(context.Background()) if err != nil { return c.JSON(http.StatusNotFound, map[string]string{"message": "An unknown error occoured!"}) diff --git a/server.go b/server.go index 37c3bde..ba25e6c 100644 --- a/server.go +++ b/server.go @@ -5,6 +5,8 @@ package main import ( "filething/ui" + "net/http" + "strings" "github.com/labstack/echo/v4" ) @@ -12,5 +14,44 @@ import ( func init() { initUi = func(e *echo.Echo) { e.GET("/*", echo.StaticDirectoryHandler(ui.DistDirFS, false)) + + e.HTTPErrorHandler = customHTTPErrorHandler } } + +// Custom Error handling since Nuxt relies on the 404 page for dynamic pages we still want api routes to use the default +// error handling built into echo +func customHTTPErrorHandler(err error, c echo.Context) { + if he, ok := err.(*echo.HTTPError); ok && he.Code == http.StatusNotFound { + path := c.Request().URL.Path + + if !strings.HasPrefix(path, "/api") { + file, err := ui.DistDirFS.Open("404.html") + if err != nil { + c.Logger().Error(err) + } + + fileInfo, err := file.Stat() + if err != nil { + c.Logger().Error(err) + } + + fileBuf := make([]byte, fileInfo.Size()) + _, err = file.Read(fileBuf) + defer func() { + if err := file.Close(); err != nil { + panic(err) + } + }() + if err != nil { + c.Logger().Error(err) + panic(err) + } + + c.HTML(http.StatusNotFound, string(fileBuf)) + return + } + } + + c.Echo().DefaultHTTPErrorHandler(err, c) +} diff --git a/ui/app.vue b/ui/app.vue index cc25a49..548ef22 100644 --- a/ui/app.vue +++ b/ui/app.vue @@ -1,5 +1,7 @@ - + + + diff --git a/ui/assets/css/main.css b/ui/assets/css/main.css index 6a997b6..63b63d9 100644 --- a/ui/assets/css/main.css +++ b/ui/assets/css/main.css @@ -48,8 +48,10 @@ } html, body { - overflow: hidden !important; + min-height: 100vh; + overflow-x: hidden !important; background-color: rgb(var(--color-base)); + overflow-wrap: break-word; color: rgb(var(--color-text)); } diff --git a/ui/bun.lockb b/ui/bun.lockb index c2c912f..5a7c897 100755 Binary files a/ui/bun.lockb and b/ui/bun.lockb differ diff --git a/ui/components/Breadcrumbs.vue b/ui/components/Breadcrumbs.vue index 340cfc6..439449f 100644 --- a/ui/components/Breadcrumbs.vue +++ b/ui/components/Breadcrumbs.vue @@ -25,7 +25,7 @@ const crumbs = computed(() => { - {{ crumb.name }} diff --git a/ui/components/Checkbox.vue b/ui/components/Checkbox.vue index 9d6d918..311535d 100644 --- a/ui/components/Checkbox.vue +++ b/ui/components/Checkbox.vue @@ -1,6 +1,6 @@ diff --git a/ui/components/FileNav.vue b/ui/components/FileNav.vue index 7ce72f4..f4ce507 100644 --- a/ui/components/FileNav.vue +++ b/ui/components/FileNav.vue @@ -3,7 +3,10 @@ import { useUser } from '~/composables/useUser' const { getUser } = useUser() const props = defineProps({ - usageBytes: Number, + usageBytes: { + type: Number, + required: true, + } }) const user = await getUser() @@ -47,7 +50,7 @@ const isInFolder = computed(() => route.path.startsWith('/home/') && route.path -let user = await useUser().getUser() - defineEmits(["update:filenav"]) -defineProps(["filenav"]) +defineProps(["filenav", "user"]) + +let colorMode = useColorMode(); + +const changeTheme = () => { + if (colorMode.preference === "dark") { + // from dark => light + colorMode.preference = "light" + } else if (colorMode.preference === "light") { + // from light => system + colorMode.preference = "system"; + } else { + // from system => dark + colorMode.preference = "dark"; + } + + return; +} - + filething - + - + + class="flex items-center px-3 h-8 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"> - + - - + + + + + + + + + + + + + + + + + + class="flex items-center px-3 h-8 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"> @@ -50,21 +89,21 @@ defineProps(["filenav"]) Link + class="px-2.5 py-1.5 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">Link Link + class="px-2.5 py-1.5 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">Link Link + class="px-2.5 py-1.5 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">Link - + + class="flex items-center px-3 h-8 text-[15px] font-semibold transition-bg duration-300 hover:bg-muted/10 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"> - + + + + + + + + + + + + + + + + diff --git a/ui/components/Nav/userDropdown.vue b/ui/components/Nav/userDropdown.vue index b84b727..0977943 100644 --- a/ui/components/Nav/userDropdown.vue +++ b/ui/components/Nav/userDropdown.vue @@ -1,21 +1,4 @@ @@ -38,18 +25,19 @@ defineProps({ - + {{ user.username }} {{ user.email }} you have {{ formatBytes(user.plan.max_storage) }} of storage - - + + + - + class="flex items-center hover:bg-muted/10 active:bg-muted/20 transition-bg w-full px-2 py-1 rounded-md focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"> + - + + + + + + + + + Site Administration + + + + + + + + + + + + Logout diff --git a/ui/components/Popup.vue b/ui/components/Popup.vue index 7bd6260..8cfb047 100644 --- a/ui/components/Popup.vue +++ b/ui/components/Popup.vue @@ -16,7 +16,7 @@ const emit = defineEmits(['update:modelValue']) {{ header }} + class="p-1 border h-fit rounded-md hover:bg-muted/10 active:bg-muted/20 transition-bg focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"> diff --git a/ui/components/UploadPane.vue b/ui/components/UploadPane.vue index 55b18f0..97b30b8 100644 --- a/ui/components/UploadPane.vue +++ b/ui/components/UploadPane.vue @@ -114,14 +114,14 @@ let uploadFailed = computed(() => props.uploadingFiles.filter(x => x.status.erro Upload + class="p-1 border h-fit rounded-md hover:bg-muted/10 active:bg-muted/20 transition-bg focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"> + class="p-1 border h-fit rounded-md hover:bg-muted/10 active:bg-muted/20 transition-bg focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"> @@ -197,7 +197,7 @@ let uploadFailed = computed(() => props.uploadingFiles.filter(x => x.status.erro + class="h-fit p-1 border rounded-md hover:bg-love/10 active:bg-love/20 hover:text-love focus-visible:text-love focus-visible:bg-love/10 transition-[background-color,color] text-sm py-1 px-2 focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"> Cancel diff --git a/ui/components/vlAccordion/content.vue b/ui/components/vlAccordion/content.vue new file mode 100755 index 0000000..1b8e867 --- /dev/null +++ b/ui/components/vlAccordion/content.vue @@ -0,0 +1,39 @@ + + + + + + + + + \ No newline at end of file diff --git a/ui/components/vlAccordion/index.vue b/ui/components/vlAccordion/index.vue new file mode 100755 index 0000000..9d3bf9f --- /dev/null +++ b/ui/components/vlAccordion/index.vue @@ -0,0 +1,139 @@ + + + + + + + + + \ No newline at end of file diff --git a/ui/components/vlAccordion/item.vue b/ui/components/vlAccordion/item.vue new file mode 100755 index 0000000..b18c0fb --- /dev/null +++ b/ui/components/vlAccordion/item.vue @@ -0,0 +1,19 @@ + + + + + + + \ No newline at end of file diff --git a/ui/components/vlAccordion/trigger.vue b/ui/components/vlAccordion/trigger.vue new file mode 100755 index 0000000..6242762 --- /dev/null +++ b/ui/components/vlAccordion/trigger.vue @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/layouts/admin.vue b/ui/layouts/admin.vue new file mode 100644 index 0000000..1d6e1bf --- /dev/null +++ b/ui/layouts/admin.vue @@ -0,0 +1,82 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/ui/middleware/admin.ts b/ui/middleware/admin.ts new file mode 100644 index 0000000..eea1a5d --- /dev/null +++ b/ui/middleware/admin.ts @@ -0,0 +1,15 @@ +import { useUser } from '~/composables/useUser' + +// We have server side things that does effectively this, but that wont stop SPA navigation +export default defineNuxtRouteMiddleware(async (to, from) => { + if (import.meta.server) { + return + } + + const { getUser } = useUser() + const user = await getUser() + + if (!user.is_admin) { + return navigateTo('/home') + } +}) diff --git a/ui/nuxt.config.ts b/ui/nuxt.config.ts index d1e8697..248ff24 100644 --- a/ui/nuxt.config.ts +++ b/ui/nuxt.config.ts @@ -11,14 +11,5 @@ export default defineNuxtConfig({ devtools: { enabled: true }, - modules: [ - '@nuxtjs/color-mode', - ], - - postcss: { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, - }, -}) + modules: ['@nuxtjs/color-mode', '@nuxtjs/tailwindcss'] +}) \ No newline at end of file diff --git a/ui/package.json b/ui/package.json index 686f920..ea3b177 100644 --- a/ui/package.json +++ b/ui/package.json @@ -11,12 +11,8 @@ }, "dependencies": { "@nuxtjs/color-mode": "^3.4.4", + "@nuxtjs/tailwindcss": "^6.12.1", "nuxt": "^3.13.1", "vue": "latest" - }, - "devDependencies": { - "autoprefixer": "^10.4.20", - "postcss": "^8.4.45", - "tailwindcss": "^3.4.10" } } \ No newline at end of file diff --git a/ui/pages/admin/config/settings.vue b/ui/pages/admin/config/settings.vue new file mode 100644 index 0000000..c3dcfcd --- /dev/null +++ b/ui/pages/admin/config/settings.vue @@ -0,0 +1,10 @@ + + + + Hey + \ No newline at end of file diff --git a/ui/pages/admin/index.vue b/ui/pages/admin/index.vue new file mode 100644 index 0000000..11125e0 --- /dev/null +++ b/ui/pages/admin/index.vue @@ -0,0 +1,156 @@ + + + + + System Status + + + Server Uptime + {{ uptime }} + Current Goroutine + {{ systemStatusData.num_goroutine }} + + Current Memory Usage + {{ systemStatusData.cur_mem_usage }} + Total Memory Allocated + {{ systemStatusData.total_mem_usage }} + Memory Obtained + {{ systemStatusData.mem_obtained }} + Pointer Lookup Times + {{ systemStatusData.ptr_lookup_times }} + Memory Allocations + {{ systemStatusData.mem_allocations }} + Memory Frees + {{ systemStatusData.mem_frees }} + + Current Heap Usage + {{ systemStatusData.cur_heap_usage }} + Heap Memory Obtained + {{ systemStatusData.heap_mem_obtained }} + Heap Memory Idle + {{ systemStatusData.heap_mem_idle }} + Heap Memory In Use + {{ systemStatusData.heap_mem_inuse }} + Heap Memory Released + {{ systemStatusData.heap_mem_release }} + Heap Objects + {{ systemStatusData.heap_objects }} + + Bootstrap Stack Usage + {{ systemStatusData.bootstrap_stack_usage }} + Stack Memory Obtained + {{ systemStatusData.stack_mem_obtained }} + MSpan Structures Usage + {{ systemStatusData.mspan_structures_usage }} + MSpan Structures Obtained + {{ systemStatusData.mspan_structures_obtained }} + MCache Structures Usage + {{ systemStatusData.mcache_structures_usage }} + MCache Structures Obtained + {{ systemStatusData.mcache_structures_obtained }} + Profiling Bucket Hash Table Obtained + {{ systemStatusData.buck_hash_sys }} + GC Metadata Obtained + {{ systemStatusData.gc_sys }} + Other System Allocation Obtained + {{ systemStatusData.other_sys }} + + Next GC Recycle + {{ systemStatusData.next_gc }} + Since Last GC Time + {{ lastGcTime }} + Total GC Pause + {{ systemStatusData.pause_total_ns }} + Last GC Pause + {{ systemStatusData.pause_ns }} + GC Times + {{ systemStatusData.num_gc }} + + + + + + \ No newline at end of file diff --git a/ui/pages/admin/users/index.vue b/ui/pages/admin/users/index.vue new file mode 100644 index 0000000..86d355f --- /dev/null +++ b/ui/pages/admin/users/index.vue @@ -0,0 +1,92 @@ + + + + + + User Account Management (Total: {{ usersCount.total_users }}) + + + + + + ID + Username + Email Address + Restricted + Created + Actions + + + + + {{ user.id }} + + {{ user.username }} + Admin + + {{ user.email }} + + + + + + + + + {{ new Date(user.created_at).toLocaleDateString('en-US', { + year: + 'numeric', month: 'short', day: 'numeric' + }) }} + + + + + + + + + + + + + + + + + + + + Load + More + + + \ No newline at end of file diff --git a/ui/pages/home/[...name].vue b/ui/pages/home/[...name].vue index a1c5c27..14e4692 100644 --- a/ui/pages/home/[...name].vue +++ b/ui/pages/home/[...name].vue @@ -8,7 +8,7 @@ definePageMeta({ middleware: "auth" }); -const user = await getUser() +const user = await getUser(); const route = useRoute(); let { data: files } = await useFetch('/api/files/get/' + route.path.replace(/^\/home/, '')) @@ -33,7 +33,7 @@ const sortedFiles = computed(() => { let selectAll: Ref<"unchecked" | "some" | "checked"> = ref('unchecked'); let selectedFiles = computed(() => sortedFiles.value?.filter(file => file.toggled === 'checked')) -watch(sortedFiles, (newVal, oldVal) => { +watch(sortedFiles, (newVal) => { let checkedFilesLength = newVal?.filter(file => file.toggled === 'checked').length; if (newVal !== undefined && checkedFilesLength !== undefined && checkedFilesLength > 0) { if (checkedFilesLength < newVal.length) { @@ -46,7 +46,7 @@ watch(sortedFiles, (newVal, oldVal) => { } }) -watch(selectAll, (newVal, oldVal) => { +watch(selectAll, (newVal) => { if (newVal === 'some') { return } @@ -231,8 +231,6 @@ const createFolder = async () => { }) ) - console.log(error.value) - if (data.value != null) { user.usage = data.value.usage files.value?.push(data.value.file) @@ -318,27 +316,28 @@ const downloadFiles = async () => { name + {{ folderError }} Close + class=" px-2 py-1 rounded-md text-sm border bg-muted/10 hover:bg-muted/15 active:bg-muted/25 transition-bg focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">Close Confirm + disabled:bg-highlight-med/50 bg-highlight-med not:hover:brightness-105 not:active:brightness-110 transition-[background-color,filter] text-surface disabled:cursor-not-allowed focus-visible:outline-none focus-visible:ring focus-visible:ring-inset">Confirm - + + class="rounded-xl border-2 border-surface flex flex-col gap-y-2 px-2 py-3 w-40 justify-center items-center hover:bg-muted/10 active:bg-muted/20 transition-bg focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"> @@ -349,7 +348,7 @@ const downloadFiles = async () => { Upload + class="rounded-xl border-2 border-surface flex flex-col gap-y-2 px-2 py-3 w-40 justify-center items-center hover:bg-muted/10 active:bg-muted/20 transition-bg focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"> @@ -371,7 +370,7 @@ const downloadFiles = async () => { + class="flex flex-row px-2 py-1 rounded-md transition-bg text-xs border hover:bg-muted/10 active:bg-muted/20 items-center focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"> { Download + class="flex flex-row px-2 py-1 rounded-md transition-bg text-xs border hover:bg-love/10 active:bg-love/20 hover:text-love active:text-love items-center focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"> { - - - + + + + class="pl-4 flex-grow min-w-40 text-start flex items-center h-full"> Name @@ -413,9 +415,12 @@ const downloadFiles = async () => { - + { + class="flex-grow text-start flex items-center h-full min-w-40 pl-4"> { + class="p-2 rounded hover:bg-muted/10 active:bg-muted/20 focus-visible:outline-none focus-visible:ring focus-visible:ring-inset"> { let { data, error: fetchError } = await useAsyncData>( () => $fetch('/api/login', { @@ -25,12 +26,16 @@ const submitForm = async () => { if (fetchError.value !== null && fetchError.value.data !== undefined) { error.value = fetchError.value.data.message - setTimeout(() => error.value = "", 15000) + timeout = setTimeout(() => error.value = "", 15000) } else if (data.value !== null) { setUser(data.value) await navigateTo('/home') } } + +onUnmounted(() => { + clearTimeout(timeout) +}) diff --git a/ui/pages/signup.vue b/ui/pages/signup.vue index 570242d..6e2700a 100644 --- a/ui/pages/signup.vue +++ b/ui/pages/signup.vue @@ -13,6 +13,7 @@ let password = ref('') let error = ref('') +let timeout; const submitForm = async () => { let { data, error: fetchError } = await useAsyncData>( () => $fetch('/api/signup', { @@ -27,12 +28,16 @@ const submitForm = async () => { if (fetchError.value != null && fetchError.value.data !== undefined) { error.value = fetchError.value.data.message - setTimeout(() => error.value = "", 15000) + timeout = setTimeout(() => error.value = "", 15000) } else if (data.value !== null) { setUser(data.value) await navigateTo('/home') } } + +onUnmounted(() => { + clearTimeout(timeout) +}) diff --git a/ui/types/user.ts b/ui/types/user.ts index ddc0ae7..d1ad8d2 100644 --- a/ui/types/user.ts +++ b/ui/types/user.ts @@ -1,5 +1,5 @@ export interface User { - id: number, + id: string, username: string, email: string, plan: { @@ -7,4 +7,6 @@ export interface User { max_storage: number }, usage: number, + created_at: string, + is_admin: boolean, }
+ filething -
{{ user.username }}
{{ user.email }}
you have {{ formatBytes(user.plan.max_storage) }} of storage
{{ folderError }}