mirror of https://github.com/JustKato/FreePad.git
Compare commits
No commits in common. "400fd23b3efa74d8c4589200c61f795963918be9" and "662dad90b70b3bbe423b681af11d1e35f148c60e" have entirely different histories.
400fd23b3e
...
662dad90b7
|
@ -21,8 +21,4 @@ CLEANUP_MAX_AGE=43200 # Default is a month
|
||||||
|
|
||||||
# Maximum pad file lenght, this is in characters, a character is one byte.
|
# Maximum pad file lenght, this is in characters, a character is one byte.
|
||||||
# Default: 524288 ( 500kb )
|
# Default: 524288 ( 500kb )
|
||||||
MAXIMUM_PAD_SIZE=524288
|
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
|
|
|
@ -1,62 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
|
@ -72,18 +72,3 @@ func GetCacheMapLimit() int {
|
||||||
|
|
||||||
return rez
|
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
|
|
||||||
}
|
|
||||||
|
|
|
@ -26,13 +26,6 @@ type Post struct {
|
||||||
Views uint32 `json:"views"`
|
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
|
// Get the path to the views JSON
|
||||||
func getViewsFilePath() (string, error) {
|
func getViewsFilePath() (string, error) {
|
||||||
// Get the path to the storage then append the const name for the storage file
|
// Get the path to the storage then append the const name for the storage file
|
||||||
|
@ -101,7 +94,7 @@ func LoadViewsCache() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func AddViewToPost(postName string, incrementViews bool) uint32 {
|
func AddViewToPost(postName string) uint32 {
|
||||||
// Lock the viewers mapping
|
// Lock the viewers mapping
|
||||||
viewersLock.Lock()
|
viewersLock.Lock()
|
||||||
|
|
||||||
|
@ -111,10 +104,8 @@ func AddViewToPost(postName string, incrementViews bool) uint32 {
|
||||||
ViewsCache[postName] = 0
|
ViewsCache[postName] = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
if incrementViews {
|
// Add to the counter
|
||||||
// Add to the counter
|
ViewsCache[postName]++
|
||||||
ViewsCache[postName]++
|
|
||||||
}
|
|
||||||
|
|
||||||
// Unlock
|
// Unlock
|
||||||
viewersLock.Unlock()
|
viewersLock.Unlock()
|
||||||
|
@ -184,7 +175,7 @@ func getStorageDirectory() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get a post from the file system
|
// Get a post from the file system
|
||||||
func GetPost(fileName string, incrementViews bool) Post {
|
func GetPost(fileName string) Post {
|
||||||
// Get the base storage directory and make sure it exists
|
// Get the base storage directory and make sure it exists
|
||||||
storageDir := getStorageDirectory()
|
storageDir := getStorageDirectory()
|
||||||
|
|
||||||
|
@ -192,7 +183,7 @@ func GetPost(fileName string, incrementViews bool) Post {
|
||||||
filePath := fmt.Sprintf("%s%s", storageDir, fileName)
|
filePath := fmt.Sprintf("%s%s", storageDir, fileName)
|
||||||
|
|
||||||
// Get the post views and add 1 to them
|
// Get the post views and add 1 to them
|
||||||
postViews := AddViewToPost(fileName, incrementViews)
|
postViews := AddViewToPost(fileName)
|
||||||
|
|
||||||
p := Post{
|
p := Post{
|
||||||
Name: fileName,
|
Name: fileName,
|
||||||
|
@ -304,30 +295,3 @@ 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
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,95 +0,0 @@
|
||||||
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(),
|
|
||||||
})
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
|
@ -41,7 +41,7 @@ func HomeRoutes(router *gin.Engine) {
|
||||||
}
|
}
|
||||||
postName = sanitize.XSS(sanitize.SingleLine(postName))
|
postName = sanitize.XSS(sanitize.SingleLine(postName))
|
||||||
|
|
||||||
post := objects.GetPost(postName, true)
|
post := objects.GetPost(postName)
|
||||||
|
|
||||||
c.HTML(200, "page.html", gin.H{
|
c.HTML(200, "page.html", gin.H{
|
||||||
"title": postName,
|
"title": postName,
|
||||||
|
|
3
main.go
3
main.go
|
@ -46,9 +46,6 @@ func main() {
|
||||||
// Implement the rate limiter
|
// Implement the rate limiter
|
||||||
controllers.DoRateLimit(router)
|
controllers.DoRateLimit(router)
|
||||||
|
|
||||||
// Admin Routing
|
|
||||||
routes.AdminRoutes(router.Group("/admin"))
|
|
||||||
|
|
||||||
// Add Routes
|
// Add Routes
|
||||||
routes.HomeRoutes(router)
|
routes.HomeRoutes(router)
|
||||||
|
|
||||||
|
|
|
@ -1,42 +0,0 @@
|
||||||
{{ template "inc/header.html" .}}
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<main id="main-card" class="container rounded mt-5 shadow-sm">
|
|
||||||
<div class="p-3">
|
|
||||||
|
|
||||||
<a href="/" class="logo-container w-100 d-flex mb-4">
|
|
||||||
<img src="/static/img/logo_transparent.png" alt="Logo" style="max-width: 50%; margin: 0 auto;" class="mx-auto">
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="form-group my-4">
|
|
||||||
<form class="search-action input-group" method="post" action="/admin/login">
|
|
||||||
<input autocomplete="off" type="password" class="form-control form-control-lg" name="admin-token" placeholder="Your Admin token" aria-label="Your Admin token" aria-describedby="admin-token-button" id="admin-token">
|
|
||||||
|
|
||||||
<button class="btn btn-primary" type="submit" id="admin-token-button">
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="24 " height="24 " fill="currentColor" class="bi bi-box-arrow-in-right" viewBox="0 0 16 16">
|
|
||||||
<path fill-rule="evenodd" d="M6 3.5a.5.5 0 0 1 .5-.5h8a.5.5 0 0 1 .5.5v9a.5.5 0 0 1-.5.5h-8a.5.5 0 0 1-.5-.5v-2a.5.5 0 0 0-1 0v2A1.5 1.5 0 0 0 6.5 14h8a1.5 1.5 0 0 0 1.5-1.5v-9A1.5 1.5 0 0 0 14.5 2h-8A1.5 1.5 0 0 0 5 3.5v2a.5.5 0 0 0 1 0v-2z"/>
|
|
||||||
<path fill-rule="evenodd" d="M11.854 8.354a.5.5 0 0 0 0-.708l-3-3a.5.5 0 1 0-.708.708L10.293 7.5H1.5a.5.5 0 0 0 0 1h8.793l-2.147 2.146a.5.5 0 0 0 .708.708l3-3z"/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
</form>
|
|
||||||
<small class="text-muted">Access the admin interface for FreePad, this can only be done through the Admin Token.</small>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="text-muted py-5 border-top text-center">
|
|
||||||
<p class="mb-1">
|
|
||||||
FreePad by <a href="https://justkato.me/">©Kato Twofold</a>
|
|
||||||
</p>
|
|
||||||
<p class="mb-0">
|
|
||||||
FreePad is freely available over on our <a href="https://github.com/JustKato/FreePad">GitHub</a>
|
|
||||||
</p>
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{{ template "inc/theme-toggle.html" .}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
{{ template "inc/footer.html" .}}
|
|
|
@ -1,94 +0,0 @@
|
||||||
{{ template "inc/header.html" .}}
|
|
||||||
|
|
||||||
<style>
|
|
||||||
|
|
||||||
.pad-instance {
|
|
||||||
display: flex;
|
|
||||||
flex-flow: row;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pad-list {
|
|
||||||
max-height: 30rem;
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pad-name {
|
|
||||||
max-width: 30%;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<main id="main-card" class="container rounded mt-5 shadow-sm">
|
|
||||||
<div class="p-3">
|
|
||||||
|
|
||||||
<a href="/" class="logo-container w-100 d-flex mb-4">
|
|
||||||
<img src="/static/img/logo_transparent.png" alt="Logo" style="max-width: 50%; margin: 0 auto;" class="mx-auto">
|
|
||||||
</a>
|
|
||||||
|
|
||||||
<div class="form-group my-4 border-top p-3 border">
|
|
||||||
|
|
||||||
<div class="pad-instance my-2 border-bottom">
|
|
||||||
<div class="pad-name col-5">
|
|
||||||
Pad Name
|
|
||||||
</div>
|
|
||||||
<div class="pad-last-view col-1">
|
|
||||||
Views
|
|
||||||
</div>
|
|
||||||
<div class="pad-last-modified col-4">
|
|
||||||
Create Date
|
|
||||||
</div>
|
|
||||||
<div class="col-2">
|
|
||||||
Actions
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="pad-list" >
|
|
||||||
{{ range $indx, $element := .padList }}
|
|
||||||
|
|
||||||
<div class="pad-instance my-2">
|
|
||||||
<div class="pad-name col-5">
|
|
||||||
<a href="/{{ $element.Name }}">
|
|
||||||
{{ $element.Name }}
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
<div class="pad-last-view col-1">
|
|
||||||
{{ $element.Views }}
|
|
||||||
</div>
|
|
||||||
<div class="pad-last-modified col-4">
|
|
||||||
{{ $element.LastModified }}
|
|
||||||
</div>
|
|
||||||
<div class="col-2">
|
|
||||||
<div onclick="doDelete({{ $element.Name }})" class="btn btn-danger">
|
|
||||||
Delete
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{{ end }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</main>
|
|
||||||
|
|
||||||
{{ template "inc/theme-toggle.html" .}}
|
|
||||||
</body>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
function doDelete(id) {
|
|
||||||
// Confirm deletion
|
|
||||||
if ( confirm("Confirm pad deletion?") ) {
|
|
||||||
// Do delete
|
|
||||||
window.location.href = `/admin/delete/${id}`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{{ template "inc/footer.html" .}}
|
|
Loading…
Reference in New Issue