feat: deepseek is insanly good
This commit is contained in:
1 parent
be9d6b610f
commit
56da3bae61
5 files changed
+112
-51
No files matched your search
@@ -12,3 +12,4 @@ ADMIN_PASSWORD=change-me
|
|||||||
# MCP_TOKEN=legacy-token
|
# MCP_TOKEN=legacy-token
|
||||||
PORT=8080
|
PORT=8080
|
||||||
SQLITE_PATH=flashcard.db
|
SQLITE_PATH=flashcard.db
|
||||||
|
HMAC_KEY_FILE=.secret
|
||||||
@@ -9,6 +9,7 @@ result
|
|||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
||||||
|
.secret
|
||||||
|
|
||||||
# IDE
|
# IDE
|
||||||
.idea/
|
.idea/
|
||||||
|
|||||||
@@ -2,14 +2,17 @@ package main
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/hmac"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -74,6 +77,29 @@ func hashToken(token string) string {
|
|||||||
return hex.EncodeToString(h[:])
|
return hex.EncodeToString(h[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadHMACKey(path string, logger *zap.Logger) ([]byte, error) {
|
||||||
|
if data, err := os.ReadFile(path); err == nil {
|
||||||
|
key, err := base64.StdEncoding.DecodeString(strings.TrimSpace(string(data)))
|
||||||
|
if err == nil && len(key) >= 32 {
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
logger.Warn("invalid HMAC key file, regenerating", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
key := make([]byte, 32)
|
||||||
|
if _, err := rand.Read(key); err != nil {
|
||||||
|
return nil, fmt.Errorf("generate HMAC key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded := base64.StdEncoding.EncodeToString(key)
|
||||||
|
if err := os.WriteFile(path, []byte(encoded+"\n"), 0600); err != nil {
|
||||||
|
return nil, fmt.Errorf("write HMAC key: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("generated new HMAC key", zap.String("path", path))
|
||||||
|
return key, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (s *sqliteStore) countUsers(ctx context.Context) (int, error) {
|
func (s *sqliteStore) countUsers(ctx context.Context) (int, error) {
|
||||||
var n int
|
var n int
|
||||||
err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM users`).Scan(&n)
|
err := s.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM users`).Scan(&n)
|
||||||
@@ -276,44 +302,86 @@ func getUserFromContext(r *http.Request) *userRow {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func authMiddleware(store *sqliteStore, logger *zap.Logger, next http.Handler) http.HandlerFunc {
|
func signCookie(key []byte, userID int64, username string) string {
|
||||||
|
expiry := time.Now().Add(30 * 24 * time.Hour).Unix()
|
||||||
|
payload := fmt.Sprintf("%d|%s|%d", userID, username, expiry)
|
||||||
|
mac := hmac.New(sha256.New, key)
|
||||||
|
mac.Write([]byte(payload))
|
||||||
|
sig := hex.EncodeToString(mac.Sum(nil))
|
||||||
|
return base64.RawURLEncoding.EncodeToString([]byte(payload + "|" + sig))
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyCookie(key []byte, encoded string) (int64, string, bool) {
|
||||||
|
raw, err := base64.RawURLEncoding.DecodeString(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(string(raw), "|", 4)
|
||||||
|
if len(parts) != 4 {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, username, expiryStr, sig := parts[0], parts[1], parts[2], parts[3]
|
||||||
|
payload := userID + "|" + username + "|" + expiryStr
|
||||||
|
|
||||||
|
mac := hmac.New(sha256.New, key)
|
||||||
|
mac.Write([]byte(payload))
|
||||||
|
expected := hex.EncodeToString(mac.Sum(nil))
|
||||||
|
if !hmac.Equal([]byte(sig), []byte(expected)) {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
expiryUnix, err := strconv.ParseInt(expiryStr, 10, 64)
|
||||||
|
if err != nil || time.Now().Unix() > expiryUnix {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := strconv.ParseInt(userID, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
return id, username, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func authMiddleware(store *sqliteStore, hmacKey []byte, logger *zap.Logger, next http.Handler) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method == http.MethodOptions {
|
if r.Method == http.MethodOptions {
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token := ""
|
var user *userRow
|
||||||
|
|
||||||
cookie, err := r.Cookie("st_pierre_session")
|
cookie, err := r.Cookie("st_pierre_session")
|
||||||
if err == nil && cookie.Value != "" {
|
if err == nil && cookie.Value != "" {
|
||||||
token = cookie.Value
|
if id, username, ok := verifyCookie(hmacKey, cookie.Value); ok {
|
||||||
}
|
u, err := store.getUserByID(r.Context(), id)
|
||||||
|
if err == nil && u != nil && u.Active && u.Username == username {
|
||||||
if token == "" {
|
user = u
|
||||||
authHeader := r.Header.Get("Authorization")
|
}
|
||||||
if strings.HasPrefix(authHeader, "Bearer ") {
|
|
||||||
token = strings.TrimPrefix(authHeader, "Bearer ")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if token == "" {
|
if user == nil {
|
||||||
writeUnauthorized(w)
|
authHeader := r.Header.Get("Authorization")
|
||||||
return
|
if strings.HasPrefix(authHeader, "Bearer ") {
|
||||||
|
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
|
session, u, err := store.getSessionByToken(r.Context(), token)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("token lookup failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
if session != nil && session.Active && u != nil && u.Active {
|
||||||
|
store.updateSessionLastUsed(r.Context(), session.ID)
|
||||||
|
user = u
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
session, user, err := store.getSessionByToken(r.Context(), token)
|
if user == nil {
|
||||||
if err != nil {
|
|
||||||
logger.Error("auth lookup failed", zap.Error(err))
|
|
||||||
writeUnauthorized(w)
|
writeUnauthorized(w)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if session == nil || !session.Active || !user.Active {
|
|
||||||
writeUnauthorized(w)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
store.updateSessionLastUsed(r.Context(), session.ID)
|
|
||||||
|
|
||||||
ctx := context.WithValue(r.Context(), userContextKey, user)
|
ctx := context.WithValue(r.Context(), userContextKey, user)
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
@@ -376,7 +444,7 @@ func getenvDefault(key, fallback string) string {
|
|||||||
return fallback
|
return fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginHandler(store *sqliteStore, logger *zap.Logger) http.HandlerFunc {
|
func loginHandler(store *sqliteStore, hmacKey []byte, logger *zap.Logger) http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||||
@@ -405,31 +473,18 @@ func loginHandler(store *sqliteStore, logger *zap.Logger) http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
token, err := generateToken()
|
signed := signCookie(hmacKey, user.ID, user.Username)
|
||||||
if err != nil {
|
|
||||||
logger.Error("generate token failed", zap.Error(err))
|
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
session, err := store.createSession(r.Context(), user.ID, token, "UI login")
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("create session failed", zap.Error(err))
|
|
||||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "internal error"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: "st_pierre_session",
|
Name: "st_pierre_session",
|
||||||
Value: token,
|
Value: signed,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
SameSite: http.SameSiteLaxMode,
|
SameSite: http.SameSiteLaxMode,
|
||||||
Expires: time.Now().Add(365 * 24 * time.Hour),
|
Expires: time.Now().Add(30 * 24 * time.Hour),
|
||||||
})
|
})
|
||||||
|
|
||||||
writeJSON(w, http.StatusOK, map[string]any{
|
writeJSON(w, http.StatusOK, map[string]any{
|
||||||
"session": session,
|
|
||||||
"user": map[string]any{
|
"user": map[string]any{
|
||||||
"id": user.ID,
|
"id": user.ID,
|
||||||
"username": user.Username,
|
"username": user.Username,
|
||||||
@@ -438,21 +493,13 @@ func loginHandler(store *sqliteStore, logger *zap.Logger) http.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func logoutHandler(store *sqliteStore) http.HandlerFunc {
|
func logoutHandler() http.HandlerFunc {
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
return func(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPost {
|
if r.Method != http.MethodPost {
|
||||||
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
writeJSON(w, http.StatusMethodNotAllowed, map[string]string{"error": "method not allowed"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
cookie, err := r.Cookie("st_pierre_session")
|
|
||||||
if err == nil && cookie.Value != "" {
|
|
||||||
session, _, _ := store.getSessionByToken(r.Context(), cookie.Value)
|
|
||||||
if session != nil {
|
|
||||||
store.deactivateSession(r.Context(), session.ID, session.UserID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
Name: "st_pierre_session",
|
Name: "st_pierre_session",
|
||||||
Value: "",
|
Value: "",
|
||||||
@@ -731,4 +778,3 @@ func getUserHandler(store *sqliteStore, logger *zap.Logger) http.HandlerFunc {
|
|||||||
writeJSON(w, http.StatusOK, user)
|
writeJSON(w, http.StatusOK, user)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +124,12 @@
|
|||||||
default = "/var/lib/st-pierre/st-pierre.sqlite";
|
default = "/var/lib/st-pierre/st-pierre.sqlite";
|
||||||
description = "Path to the SQLite database file.";
|
description = "Path to the SQLite database file.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
hmacKeyFile = lib.mkOption {
|
||||||
|
type = lib.types.path;
|
||||||
|
default = "/var/lib/st-pierre/secret";
|
||||||
|
description = "Path to the HMAC key file for cookie signing. Will be auto-generated if missing.";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
config = lib.mkIf cfg.enable {
|
config = lib.mkIf cfg.enable {
|
||||||
@@ -170,6 +176,7 @@
|
|||||||
EMBEDDING_BASE_URL = cfg.embeddingBaseUrl;
|
EMBEDDING_BASE_URL = cfg.embeddingBaseUrl;
|
||||||
EMBEDDING_MODEL = cfg.embeddingModel;
|
EMBEDDING_MODEL = cfg.embeddingModel;
|
||||||
SQLITE_PATH = builtins.toString cfg.sqlitePath;
|
SQLITE_PATH = builtins.toString cfg.sqlitePath;
|
||||||
|
HMAC_KEY_FILE = builtins.toString cfg.hmacKeyFile;
|
||||||
GIT_COMMIT = gitHash;
|
GIT_COMMIT = gitHash;
|
||||||
BUILD_DATE = lastUpdated;
|
BUILD_DATE = lastUpdated;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,12 @@ func main() {
|
|||||||
|
|
||||||
store.bootstrapAdmin(context.Background(), logger)
|
store.bootstrapAdmin(context.Background(), logger)
|
||||||
|
|
||||||
|
hmacKeyFile := envOrDefault("HMAC_KEY_FILE", ".secret")
|
||||||
|
hmacKey, err := loadHMACKey(hmacKeyFile, logger)
|
||||||
|
if err != nil {
|
||||||
|
logger.Fatal("failed to load HMAC key", zap.Error(err))
|
||||||
|
}
|
||||||
|
|
||||||
searchEngine := createSearchEngine(logger, store, openAIKey)
|
searchEngine := createSearchEngine(logger, store, openAIKey)
|
||||||
|
|
||||||
mcpServer := server.NewMCPServer(
|
mcpServer := server.NewMCPServer(
|
||||||
@@ -59,7 +65,7 @@ func main() {
|
|||||||
streamableServer := server.NewStreamableHTTPServer(mcpServer)
|
streamableServer := server.NewStreamableHTTPServer(mcpServer)
|
||||||
|
|
||||||
withAuth := func(next http.Handler) http.Handler {
|
withAuth := func(next http.Handler) http.Handler {
|
||||||
return authMiddleware(store, logger, next)
|
return authMiddleware(store, hmacKey, logger, next)
|
||||||
}
|
}
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
@@ -78,8 +84,8 @@ func main() {
|
|||||||
|
|
||||||
mux.Handle("/mcp", withAuth(streamableServer))
|
mux.Handle("/mcp", withAuth(streamableServer))
|
||||||
|
|
||||||
mux.HandleFunc("/api/v1/auth/login", loginHandler(store, logger))
|
mux.HandleFunc("/api/v1/auth/login", loginHandler(store, hmacKey, logger))
|
||||||
mux.HandleFunc("/api/v1/auth/logout", logoutHandler(store))
|
mux.HandleFunc("/api/v1/auth/logout", logoutHandler())
|
||||||
mux.HandleFunc("/api/v1/auth/me", withAuth(getUserHandler(store, logger)).ServeHTTP)
|
mux.HandleFunc("/api/v1/auth/me", withAuth(getUserHandler(store, logger)).ServeHTTP)
|
||||||
mux.HandleFunc("/api/v1/auth/password", withAuth(changePasswordHandler(store, logger)).ServeHTTP)
|
mux.HandleFunc("/api/v1/auth/password", withAuth(changePasswordHandler(store, logger)).ServeHTTP)
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user