Init
This commit is contained in:
commit
d796f9e08a
14 changed files with 411 additions and 0 deletions
21
.gitignore
vendored
Normal file
21
.gitignore
vendored
Normal file
|
@ -0,0 +1,21 @@
|
|||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
|
||||
# Executable
|
||||
hibiscus
|
7
README.md
Normal file
7
README.md
Normal file
|
@ -0,0 +1,7 @@
|
|||
# 🌺 Hibiscus.txt
|
||||
|
||||
Simple plaintext journaling server.
|
||||
|
||||
## Features:
|
||||
* Lack of features. No, really.
|
||||
* Does one thing and does it... decently?
|
37
auth.go
Normal file
37
auth.go
Normal file
|
@ -0,0 +1,37 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// This middleware handles authentication & authorization for the app.
|
||||
// It uses BasicAuth because I doubt there is a need for something sophisticated in a small hobby project
|
||||
// Originally taken from https://www.alexedwards.net/blog/basic-authentication-in-go (13.03.2024)
|
||||
// TODO: why did I have to convert Handler from HandlerFunc?
|
||||
func basicAuth(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
username, password, ok := r.BasicAuth()
|
||||
if ok {
|
||||
// Calculate SHA-256 hashes for equal length in ConstantTimeCompare
|
||||
usernameHash := sha256.Sum256([]byte(username))
|
||||
passwordHash := sha256.Sum256([]byte(password))
|
||||
expectedUsernameHash := sha256.Sum256([]byte("test")) // TODO: put user & pass outside
|
||||
expectedPasswordHash := sha256.Sum256([]byte("pass"))
|
||||
|
||||
usernameMatch := subtle.ConstantTimeCompare(usernameHash[:], expectedUsernameHash[:]) == 1
|
||||
passwordMatch := subtle.ConstantTimeCompare(passwordHash[:], expectedPasswordHash[:]) == 1
|
||||
|
||||
if usernameMatch && passwordMatch {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
// TODO: Note failed login attempt?
|
||||
}
|
||||
|
||||
// Unauthorized, inform client that we have auth and return 401
|
||||
w.Header().Set("WWW-Authenticate", `Basic realm="restricted", charset="UTF-8"`)
|
||||
http.Error(w, "Unauthorized", http.StatusUnauthorized)
|
||||
})
|
||||
}
|
95
files.go
Normal file
95
files.go
Normal file
|
@ -0,0 +1,95 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var DEFAULT_EXTENSION = "txt"
|
||||
|
||||
func GetFile(filename string, w http.ResponseWriter, r *http.Request) {
|
||||
filenames, err := filepath.Glob("data/" + filename + ".*") // .txt, .md, anything
|
||||
if err != nil {
|
||||
http.Error(w, "error finding file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
if len(filenames) == 0 {
|
||||
http.Error(w, "no matching files found", http.StatusNotFound)
|
||||
return
|
||||
} else if len(filenames) > 1 {
|
||||
http.Error(w, "several matching files found ("+strconv.Itoa(len(filenames))+")", http.StatusInternalServerError) // TODO: Better handling, duh
|
||||
return
|
||||
}
|
||||
|
||||
fileContents, err := os.ReadFile(filenames[0])
|
||||
if err != nil {
|
||||
http.Error(w, "error reading file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
_, err = w.Write(fileContents)
|
||||
if err != nil {
|
||||
http.Error(w, "error sending file", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// PostFile TODO: Save to trash to prevent malicious/accidental ovverrides?
|
||||
func PostFile(filename string, w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("error reading body"))
|
||||
return
|
||||
}
|
||||
|
||||
filenames, err := filepath.Glob("data/" + filename + ".*") // .txt, .md, anything
|
||||
if err != nil {
|
||||
http.Error(w, "error searching for file", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var filenameFinal string
|
||||
if len(filenames) == 0 {
|
||||
// Create new file and write
|
||||
filenameFinal = "data/" + filename + "." + DEFAULT_EXTENSION
|
||||
} else if len(filenames) > 1 {
|
||||
http.Error(w, "several matching files found ("+strconv.Itoa(len(filenames))+")", http.StatusInternalServerError) // TODO: Better handling, duh
|
||||
return
|
||||
} else {
|
||||
filenameFinal = filenames[0]
|
||||
fmt.Println(filenameFinal)
|
||||
fmt.Println(filenames)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(filenameFinal, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
fmt.Println("Error opening/making file")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := f.Write(body); err != nil {
|
||||
fmt.Println("Error writing to the file")
|
||||
}
|
||||
}
|
||||
|
||||
// ListFiles returns JSON of filenames in a directory without extensions or path
|
||||
func ListFiles(directory string, w http.ResponseWriter, r *http.Request) {
|
||||
filenames, err := filepath.Glob("data/" + directory + "/*") // .txt, .md, anything
|
||||
if err != nil {
|
||||
http.Error(w, "error searching for files", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
for i, file := range filenames {
|
||||
file, _ := strings.CutSuffix(filepath.Base(file), filepath.Ext(file))
|
||||
filenames[i] = file
|
||||
}
|
||||
filenamesJson, err := json.Marshal(filenames)
|
||||
w.Write(filenamesJson)
|
||||
}
|
5
go.mod
Normal file
5
go.mod
Normal file
|
@ -0,0 +1,5 @@
|
|||
module hibiscus
|
||||
|
||||
go 1.20
|
||||
|
||||
require github.com/go-chi/chi/v5 v5.0.12
|
2
go.sum
Normal file
2
go.sum
Normal file
|
@ -0,0 +1,2 @@
|
|||
github.com/go-chi/chi/v5 v5.0.12 h1:9euLV5sTrTNTRUU9POmDUvfxyj6LAABLUcEWO+JJb4s=
|
||||
github.com/go-chi/chi/v5 v5.0.12/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
47
log.go
Normal file
47
log.go
Normal file
|
@ -0,0 +1,47 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
func AppendLog(input string) error {
|
||||
t := time.Now().Format("02-01-2006 15:04") // dd-mm-yyyy HH:MM
|
||||
|
||||
f, err := os.OpenFile("./data/log.txt", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
fmt.Println("Error opening/making file")
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := f.Write([]byte(t + " | " + input + "\n")); err != nil {
|
||||
fmt.Println("Error appending to the file")
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
fmt.Println("Error closing the file")
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func PostLog(w http.ResponseWriter, r *http.Request) {
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("error reading body"))
|
||||
return
|
||||
}
|
||||
err = AppendLog(string(body))
|
||||
if err != nil {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
w.Write([]byte("error appending to log"))
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("appended to log"))
|
||||
}
|
1
log_curl.sh
Normal file
1
log_curl.sh
Normal file
|
@ -0,0 +1 @@
|
|||
curl -XPOST -d 'need to \n remove newlines \n' -u 'test:pass' 'localhost:7101/log'
|
5
main.go
Normal file
5
main.go
Normal file
|
@ -0,0 +1,5 @@
|
|||
package main
|
||||
|
||||
func main() {
|
||||
Serve()
|
||||
}
|
39
pages.go
Normal file
39
pages.go
Normal file
|
@ -0,0 +1,39 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"github.com/go-chi/chi/v5"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NotFound returns a user-friendly 404 error page
|
||||
func NotFound(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(404)
|
||||
http.ServeFile(w, r, "./pages/error/404.html")
|
||||
}
|
||||
|
||||
// GetDay gets... a day... page
|
||||
func GetDay(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: This will be *very* different, `today` func will be needed
|
||||
dayString := chi.URLParam(r, "day")
|
||||
if dayString == "" {
|
||||
dayString = time.Now().Format("02-01-2006") // By default, use today
|
||||
}
|
||||
GetFile("day/"+dayString, w, r)
|
||||
}
|
||||
|
||||
// GetNote gets... a day... page
|
||||
func GetNote(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: This will be *very* different, `today` func will be needed
|
||||
noteString := chi.URLParam(r, "note")
|
||||
if noteString == "" {
|
||||
w.WriteHeader(http.StatusNotFound) // TODO: maybe different status fits better?
|
||||
w.Write([]byte("note name not given"))
|
||||
return
|
||||
}
|
||||
GetFile("notes/"+noteString, w, r)
|
||||
}
|
||||
|
||||
func PostDayPage(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
}
|
17
pages/error/404.html
Normal file
17
pages/error/404.html
Normal file
|
@ -0,0 +1,17 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- <link rel="icon" type="image/x-icon" href="/public/img/favicon.ico">-->
|
||||
<!-- <link rel="stylesheet" href="/public/css/main.css">-->
|
||||
<title>Error 404 - Hibiscus</title>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>Error 404 - Not Found</h1>
|
||||
<p>The page you were looking for doesn't exist or was moved</p>
|
||||
<h3><a href="/">Go home?</a></h3>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
29
pages/index.html
Normal file
29
pages/index.html
Normal file
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<!-- <link rel="stylesheet" href="/public/css/main.css">-->
|
||||
<link rel="stylesheet" href="/public/css/pico.red.min.css">
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Hibiscus.txt</title>
|
||||
</head>
|
||||
<body>
|
||||
<header class="container">
|
||||
<h1 style="margin-bottom:0;">Hibiscus.txt</h1>
|
||||
<p id="status" style="margin-top:0;">Today is 14/03/2024</p>
|
||||
</header>
|
||||
<main class="container">
|
||||
<h2 style="margin-bottom:0;"><label for="day">Your day so far:</label></h2>
|
||||
<textarea id="day" name="Day entry" cols="40" rows="10" style="max-width: 640px; width: 100%"></textarea>
|
||||
<div class="grid"><button onclick="console.log('Test save')">Save</button></div>
|
||||
|
||||
<h2><label for="log">Log</label></h2>
|
||||
<input type="text" id="log" name="log" style="max-width: 640px; width: 100%"/>
|
||||
<div class="grid"><button onclick="console.log('Test log save')">Save</button></div>
|
||||
</main>
|
||||
<footer class="container">
|
||||
<p><a href="/page/sitemap" class="no-accent"></a> v0.0.1 | PoC using <a href="https://picocss.com">PicoCSS</a></p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
55
public/css/main.css
Normal file
55
public/css/main.css
Normal file
|
@ -0,0 +1,55 @@
|
|||
body {
|
||||
color: #454545;
|
||||
background-color: #f5f0e1; /*f6f4ee*/
|
||||
font-size: 16px;
|
||||
margin: 2em auto;
|
||||
max-width: 950px;
|
||||
padding: 1em;
|
||||
line-height: 1.4;
|
||||
text-align: justify;
|
||||
font-family: serif;
|
||||
min-height: 85vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
/* Links can be blue or look like plaintext */
|
||||
a, a:visited { color: #f85552; }
|
||||
a:hover, a:visited:hover { color: #e66868; }
|
||||
a.no-accent, a.no-accent:visited { color: #454545; }
|
||||
a.no-accent:hover, a.no-accent:visited:hover { color: #656565; }
|
||||
|
||||
textarea, input {
|
||||
background: #f5f2ee;
|
||||
}
|
||||
|
||||
footer { margin-top: auto; }
|
||||
|
||||
button {
|
||||
background-color: #f85552;
|
||||
border: none;
|
||||
color: #f5f2ee;
|
||||
padding: 10px;
|
||||
text-align: center;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
font-size: 16px;
|
||||
margin: 4px 2px;
|
||||
border-radius: 12px;
|
||||
max-width: 100px;
|
||||
cursor: pointer;
|
||||
}
|
||||
button:hover { background-color: #e66868; }
|
||||
|
||||
|
||||
/* Dark theme TODO*/
|
||||
/*@media (prefers-color-scheme: dark) {*/
|
||||
/* body {*/
|
||||
/* color: #f5f0e1;*/
|
||||
/* background-color: #2c2825;*/
|
||||
/* }*/
|
||||
/* a.no-accent, a.no-accent:visited { color: #f5f0e1; }*/
|
||||
/* a.no-accent:hover, a.no-accent:visited:hover { color: #a9a8a4; }*/
|
||||
/* a, a:visited { color: #24a5ea; }*/
|
||||
/* a:hover, a:visited:hover { color: #1b74cc; }*/
|
||||
/*}*/
|
51
serve.go
Normal file
51
serve.go
Normal file
|
@ -0,0 +1,51 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"net/http"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const PORT = 7101 // TODO: Obviously don't just declare port here
|
||||
|
||||
func Serve() {
|
||||
r := chi.NewRouter()
|
||||
r.Use(middleware.Logger, middleware.CleanPath, middleware.StripSlashes)
|
||||
r.Use(basicAuth) // TODO: ..duh!
|
||||
|
||||
// Home page
|
||||
r.Get("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
http.ServeFile(w, r, "./pages/index.html")
|
||||
})
|
||||
|
||||
// API =============
|
||||
apiRouter := chi.NewRouter()
|
||||
|
||||
apiRouter.Get("/readme", func(w http.ResponseWriter, r *http.Request) { GetFile("readme", w, r) })
|
||||
apiRouter.Get("/log", func(w http.ResponseWriter, r *http.Request) { GetFile("log", w, r) })
|
||||
apiRouter.Get("/agenda", func(w http.ResponseWriter, r *http.Request) { GetFile("agenda", w, r) })
|
||||
|
||||
apiRouter.Post("/readme", func(w http.ResponseWriter, r *http.Request) { PostFile("readme", w, r) })
|
||||
apiRouter.Post("/agenda", func(w http.ResponseWriter, r *http.Request) { PostFile("agenda", w, r) })
|
||||
|
||||
apiRouter.Get("/day", func(w http.ResponseWriter, r *http.Request) { ListFiles("day", w, r) })
|
||||
apiRouter.Get("/day/{day}", func(w http.ResponseWriter, r *http.Request) { GetDay(w, r) })
|
||||
|
||||
apiRouter.Get("/notes", func(w http.ResponseWriter, r *http.Request) { ListFiles("notes", w, r) })
|
||||
apiRouter.Get("/notes/{note}", func(w http.ResponseWriter, r *http.Request) { GetNote(w, r) })
|
||||
|
||||
apiRouter.Post("/log", func(w http.ResponseWriter, r *http.Request) { PostLog(w, r) })
|
||||
|
||||
r.Mount("/api", apiRouter)
|
||||
|
||||
r.NotFound(NotFound)
|
||||
|
||||
// Static files
|
||||
fs := http.FileServer(http.Dir("public"))
|
||||
r.Handle("/public/*", http.StripPrefix("/public/", fs))
|
||||
|
||||
fmt.Println("Website working on port: ", PORT)
|
||||
_ = http.ListenAndServe(":"+strconv.Itoa(PORT), r)
|
||||
}
|
Loading…
Reference in a new issue