Files
website/backend/api.go
T
2026-05-24 09:40:20 +02:00

146 lines
3.6 KiB
Go

package main
import (
"context"
"crypto/rand"
"encoding/hex"
"encoding/json"
"io"
"log"
"net/http"
"sync"
)
type ApiHandler struct {
db *Database
mutex sync.RWMutex
authToken *string
}
const authTokenCookieName = "auth-token"
const isAuthorizedContextKey = "is-authorized"
const contentTypeHeaderKey = "Content-Type"
const JsonMimeType = "application/json"
func (h *ApiHandler) ServeLoginPost(writer http.ResponseWriter, request *http.Request) {
if !HasContentType(request, JsonMimeType) {
WriteError(writer, http.StatusBadRequest, "expected json body", nil)
return
}
bodyReader := request.Body
body, err := io.ReadAll(bodyReader)
_ = bodyReader.Close()
if err != nil {
WriteError(writer, http.StatusBadRequest, "failed to read body", err)
return
}
type LoginBody struct {
Password string `json:"password"`
}
loginBody := LoginBody{}
err = json.Unmarshal(body, &loginBody)
if err != nil {
WriteError(writer, http.StatusBadRequest, "failed to read body", err)
return
}
success, err := h.db.ValidateRootPassword(loginBody.Password)
if err != nil {
WriteError(writer, http.StatusInternalServerError, "failed to read database", err)
return
}
if !success {
log.Printf("failed login from '%s'", request.RemoteAddr)
WriteError(writer, http.StatusUnauthorized, "invalid password", nil)
return
}
rawAuthToken := make([]byte, 128)
_, _ = rand.Read(rawAuthToken)
authToken := hex.EncodeToString(rawAuthToken)
h.mutex.Lock()
h.authToken = &authToken
h.mutex.Unlock()
cookie := http.Cookie{}
cookie.Name = authTokenCookieName
cookie.Value = authToken
cookie.Secure = true
cookie.HttpOnly = true
http.SetCookie(writer, &cookie)
WriteResponse(writer, http.StatusOK, map[string]interface{}{})
log.Printf("successful login from '%s'", request.RemoteAddr)
}
func (h *ApiHandler) ServeLogoutPost(writer http.ResponseWriter, request *http.Request) {
cookie, _ := request.Cookie(authTokenCookieName)
if cookie != nil {
cookie := http.Cookie{}
cookie.Name = authTokenCookieName
cookie.Value = ""
cookie.Secure = true
cookie.HttpOnly = true
http.SetCookie(writer, &cookie)
}
h.mutex.Lock()
h.authToken = nil
h.mutex.Unlock()
WriteResponse(writer, http.StatusOK, map[string]interface{}{})
log.Printf("successful logout from '%s'", request.RemoteAddr)
}
func (h *ApiHandler) ProcessAuth(next http.Handler, required bool) http.Handler {
return http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
isAuthorized := false
cookie, _ := request.Cookie(authTokenCookieName)
if cookie != nil {
h.mutex.RLock()
isAuthorized = h.authToken != nil && *h.authToken == cookie.Value
h.mutex.RUnlock()
}
if !isAuthorized && required {
WriteError(writer, http.StatusUnauthorized, "authentication required", nil)
return
}
next.ServeHTTP(writer, request.WithContext(context.WithValue(request.Context(), isAuthorizedContextKey, isAuthorized)))
})
}
func IsAuthorized(request *http.Request) bool {
value := request.Context().Value(isAuthorizedContextKey)
return value != nil && value.(bool)
}
func WriteResponse(writer http.ResponseWriter, code int, body any) {
writer.Header().Set(contentTypeHeaderKey, "application/json")
writer.WriteHeader(code)
_ = json.NewEncoder(writer).Encode(body)
}
func WriteError(writer http.ResponseWriter, code int, message string, err error) {
if err != nil {
log.Println(err)
}
WriteResponse(writer, code, map[string]interface{}{
"message": message,
})
}
func HasContentType(request *http.Request, mimeType string) bool {
contentType := request.Header.Get(contentTypeHeaderKey)
return contentType == mimeType
}