diff --git a/.env.example b/.env.example index 1b7bad1..329fb7f 100644 --- a/.env.example +++ b/.env.example @@ -21,4 +21,8 @@ CLEANUP_MAX_AGE=43200 # Default is a month # Maximum pad file lenght, this is in characters, a character is one byte. # Default: 524288 ( 500kb ) -MAXIMUM_PAD_SIZE=524288 \ No newline at end of file +MAXIMUM_PAD_SIZE=524288 + +# Your admin access token +# If the value is not defined the admin interface will not be available +# ADMIN_TOKEN=SUPER_SECRET_ADMIN_TOKEN \ No newline at end of file diff --git a/lib/controllers/controllers_admin.go b/lib/controllers/controllers_admin.go new file mode 100644 index 0000000..d0818b6 --- /dev/null +++ b/lib/controllers/controllers_admin.go @@ -0,0 +1,62 @@ +package controllers + +import ( + "crypto/sha512" + "encoding/hex" + "fmt" + "net/http" + + "github.com/JustKato/FreePad/lib/helper" + "github.com/gin-gonic/gin" +) + +func AdminMiddleware(router *gin.RouterGroup) { + + // Handl + router.Use(func(ctx *gin.Context) { + + // Check which route we are accessing + fmt.Println(`Accesing: `, ctx.Request.RequestURI) + + // Check if the request is other than the login request + if ctx.Request.RequestURI != "/admin/login" { + // Check if the user is logged-in + + fmt.Println(`Checking if admin`) + + if !IsAdmin(ctx) { + // Not an admin, redirect to homepage + ctx.Redirect(http.StatusTemporaryRedirect, "/") + ctx.Abort() + + fmt.Println(`Not an admin!`) + return + } + + } + + }) + +} + +func IsAdmin(ctx *gin.Context) bool { + adminToken, err := ctx.Cookie("admin_token") + if err != nil { + return false + } + + // Encode the real token + sha512Hasher := sha512.New() + sha512Hasher.Write([]byte(helper.GetAdminToken())) + hashHexToken := sha512Hasher.Sum(nil) + trueToken := hex.EncodeToString(hashHexToken) + + // Check if the user's admin token matches the token + if adminToken != "" && adminToken == trueToken { + // Yep, it's the admin! + return true + } + + // Definitely not an admin + return false +} diff --git a/lib/helper/helper_main.go b/lib/helper/helper_main.go index ef5f905..7c1f158 100644 --- a/lib/helper/helper_main.go +++ b/lib/helper/helper_main.go @@ -72,3 +72,18 @@ func GetCacheMapLimit() int { return rez } + +// Get the admin token used to authenticate as an admin +func GetAdminToken() string { + // Get the admin login from the environment + adminToken, exists := os.LookupEnv("ADMIN_TOKEN") + + // Check if the admin token was defined + if !exists { + // The admin token was not defined, disable admin logins + return "" + } + + // Return the admin token + return adminToken +} diff --git a/lib/objects/objects_post.go b/lib/objects/objects_post.go index b66518b..bd9f571 100644 --- a/lib/objects/objects_post.go +++ b/lib/objects/objects_post.go @@ -26,6 +26,13 @@ type Post struct { Views uint32 `json:"views"` } +func (p *Post) Delete() error { + filePath := path.Join(getStorageDirectory(), p.Name) + + // Remove the file and return the result + return os.Remove(filePath) +} + // Get the path to the views JSON func getViewsFilePath() (string, error) { // Get the path to the storage then append the const name for the storage file @@ -94,7 +101,7 @@ func LoadViewsCache() error { return nil } -func AddViewToPost(postName string) uint32 { +func AddViewToPost(postName string, incrementViews bool) uint32 { // Lock the viewers mapping viewersLock.Lock() @@ -104,8 +111,10 @@ func AddViewToPost(postName string) uint32 { ViewsCache[postName] = 0 } - // Add to the counter - ViewsCache[postName]++ + if incrementViews { + // Add to the counter + ViewsCache[postName]++ + } // Unlock viewersLock.Unlock() @@ -175,7 +184,7 @@ func getStorageDirectory() string { } // Get a post from the file system -func GetPost(fileName string) Post { +func GetPost(fileName string, incrementViews bool) Post { // Get the base storage directory and make sure it exists storageDir := getStorageDirectory() @@ -183,7 +192,7 @@ func GetPost(fileName string) Post { filePath := fmt.Sprintf("%s%s", storageDir, fileName) // Get the post views and add 1 to them - postViews := AddViewToPost(fileName) + postViews := AddViewToPost(fileName, incrementViews) p := Post{ Name: fileName, @@ -295,3 +304,30 @@ func CleanupPosts(age int) { } } + +func GetAllPosts() []Post { + // Initialize the list of posts + postList := []Post{} + + // Get the posts storage directory + storageDir := getStorageDirectory() + + // Read the directory listing + files, err := os.ReadDir(storageDir) + // Check if thereh as been an issues with reading the directory contents + if err != nil { + // Log the error + fmt.Println("Error::GetAllPosts:", err) + // Return an empty list to have a clean fallback + return []Post{} + } + + // Go through all of the files + for _, v := range files { + // Process the file into a pad + postList = append(postList, GetPost(v.Name(), false)) + } + + // Return the post list + return postList +} diff --git a/lib/routes/routes_admin.go b/lib/routes/routes_admin.go new file mode 100644 index 0000000..e12aca3 --- /dev/null +++ b/lib/routes/routes_admin.go @@ -0,0 +1,95 @@ +package routes + +import ( + "encoding/hex" + "fmt" + "net/http" + + "github.com/JustKato/FreePad/lib/controllers" + "github.com/JustKato/FreePad/lib/helper" + "github.com/JustKato/FreePad/lib/objects" + "github.com/gin-gonic/gin" + + "crypto/sha512" +) + +var adminLoginToken string = "" + +func AdminRoutes(router *gin.RouterGroup) { + + adminLoginToken = helper.GetAdminToken() + + // Apply the admin middleware for identification + controllers.AdminMiddleware(router) + + // Admin login route + router.GET("/login", func(ctx *gin.Context) { + ctx.HTML(200, "admin_login.html", gin.H{ + "title": "Login Login", + "domain_base": helper.GetDomainBase(), + }) + }) + + router.POST("/login", func(ctx *gin.Context) { + + // Get the value of the admin token + adminToken := ctx.PostForm("admin-token") + + // Check if the input admin token matches our admin token + if adminLoginToken != "" && adminLoginToken == adminToken { + + sha512Hasher := sha512.New() + sha512Hasher.Write([]byte(adminToken)) + + // Set the cookie to be an admin + hashHexToken := sha512Hasher.Sum(nil) + hashToken := hex.EncodeToString(hashHexToken) + + // Set the cookie + ctx.SetCookie("admin_token", hashToken, 60*60, "/", helper.GetDomainBase(), true, true) + + ctx.Request.Method = "GET" + + // Redirect the user to the admin page + ctx.Redirect(http.StatusFound, "/admin/view") + return + } else { + ctx.Request.Method = "GET" + + // Redirect the user to the admin page + ctx.Redirect(http.StatusFound, "/admin/login?fail") + return + } + + }) + + router.GET("/delete/:padname", func(ctx *gin.Context) { + // Get the pad name that we bout' to delete + padName := ctx.Param("padname") + + // Try and get the pad, check if valid + pad := objects.GetPost(padName, false) + + // Delete the pad + err := pad.Delete() + fmt.Println(err) + + // Redirect the user to the admin page + ctx.Redirect(http.StatusFound, "/admin/view") + }) + + // Admin view route + router.GET("/view", func(ctx *gin.Context) { + + // Get all of the pads as a listing + padList := objects.GetAllPosts() + + ctx.HTML(200, "admin_view.html", gin.H{ + "title": "Admin", + "padList": padList, + "domain_base": helper.GetDomainBase(), + }) + + }) + +} diff --git a/lib/routes/routes_home.go b/lib/routes/routes_home.go index d512015..e7d8be0 100644 --- a/lib/routes/routes_home.go +++ b/lib/routes/routes_home.go @@ -41,7 +41,7 @@ func HomeRoutes(router *gin.Engine) { } postName = sanitize.XSS(sanitize.SingleLine(postName)) - post := objects.GetPost(postName) + post := objects.GetPost(postName, true) c.HTML(200, "page.html", gin.H{ "title": postName, diff --git a/main.go b/main.go index 534bff1..6549a86 100644 --- a/main.go +++ b/main.go @@ -46,6 +46,9 @@ func main() { // Implement the rate limiter controllers.DoRateLimit(router) + // Admin Routing + routes.AdminRoutes(router.Group("/admin")) + // Add Routes routes.HomeRoutes(router) diff --git a/templates/pages/admin_login.html b/templates/pages/admin_login.html new file mode 100644 index 0000000..c2b8c7f --- /dev/null +++ b/templates/pages/admin_login.html @@ -0,0 +1,42 @@ +{{ template "inc/header.html" .}} + + + +
+
+ + + Logo + + +
+
+ + + + +
+ Access the admin interface for FreePad, this can only be done through the Admin Token. +
+
+ + + +
+ + {{ template "inc/theme-toggle.html" .}} + + +{{ template "inc/footer.html" .}} \ No newline at end of file diff --git a/templates/pages/admin_view.html b/templates/pages/admin_view.html new file mode 100644 index 0000000..18df451 --- /dev/null +++ b/templates/pages/admin_view.html @@ -0,0 +1,94 @@ +{{ template "inc/header.html" .}} + + + + + +
+
+ + + Logo + + +
+ +
+
+ Pad Name +
+
+ Views +
+
+ Create Date +
+
+ Actions +
+
+ +
+ {{ range $indx, $element := .padList }} + +
+ +
+ {{ $element.Views }} +
+
+ {{ $element.LastModified }} +
+
+
+ Delete +
+
+
+ + {{ end }} +
+ +
+
+ +
+ + {{ template "inc/theme-toggle.html" .}} + + + + +{{ template "inc/footer.html" .}} \ No newline at end of file