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 }