diff --git a/.env b/.env index a087fff..1c39d5c 100644 --- a/.env +++ b/.env @@ -8,6 +8,7 @@ THIRDONE_BIN=thirdone TUTOR_PUBLIC_URL=https://tutor.uljisoft.com # third-one endpoint (no API key needed — auth handled by third-one): TUTOR_LLM_ENDPOINT=http://localhost:11434/v1 +TUTOR_DEPLOY_SECRET= # For direct API access (e.g. OpenAI, DeepSeek), set endpoint + key: # TUTOR_LLM_ENDPOINT=https://api.deepseek.com # TUTOR_LLM_API_KEY=sk-your-key-here diff --git a/deploy.sh b/deploy.sh new file mode 100644 index 0000000..d6c1fc9 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euo pipefail + +cd "$(dirname "$0")" + +echo "[deploy] pulling latest code..." +git pull origin master + +echo "[deploy] building..." +go build -o tutor-api ./cmd/tutor-api + +echo "[deploy] restarting service..." +sudo systemctl restart tutor-api + +echo "[deploy] done" diff --git a/internal/config/config.go b/internal/config/config.go index 21f117f..9ccc797 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -23,6 +23,7 @@ type Config struct { LLMEndpoint string GoogleClientID string JWTSecret string + DeploySecret string } func LoadFromEnv() Config { @@ -38,6 +39,7 @@ func LoadFromEnv() Config { LLMEndpoint: envOrDefault("TUTOR_LLM_ENDPOINT", ""), GoogleClientID: envOrDefault("GOOGLE_CLIENT_ID", ""), JWTSecret: envOrDefault("JWT_SECRET", ""), + DeploySecret: envOrDefault("TUTOR_DEPLOY_SECRET", ""), } } @@ -45,6 +47,10 @@ func (c Config) HasLLM() bool { return c.LLMEndpoint != "" } +func (c Config) HasDeploy() bool { + return c.DeploySecret != "" +} + func envOrDefault(key string, fallback string) string { value := os.Getenv(key) if value == "" { diff --git a/internal/httpapi/deploy.go b/internal/httpapi/deploy.go new file mode 100644 index 0000000..a2d4f4b --- /dev/null +++ b/internal/httpapi/deploy.go @@ -0,0 +1,31 @@ +package httpapi + +import ( + "log" + "net/http" + "os/exec" +) + +func (h Handler) handleDeploy(w http.ResponseWriter, r *http.Request) { + if !h.cfg.HasDeploy() { + writeError(w, http.StatusNotFound, "deploy endpoint not configured") + return + } + + if r.Header.Get("X-Deploy-Secret") != h.cfg.DeploySecret { + writeError(w, http.StatusUnauthorized, "invalid deploy secret") + return + } + + writeJSON(w, http.StatusAccepted, map[string]string{"status": "deploy started"}) + + go func() { + cmd := exec.Command("/bin/bash", "deploy.sh") + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf("[deploy] failed: %v, output: %s", err, string(output)) + return + } + log.Printf("[deploy] success: %s", string(output)) + }() +} diff --git a/internal/httpapi/handler.go b/internal/httpapi/handler.go index ebdf9ba..a5c14fb 100644 --- a/internal/httpapi/handler.go +++ b/internal/httpapi/handler.go @@ -53,6 +53,9 @@ func (h Handler) Routes() http.Handler { mux.HandleFunc("GET /api/v1/ontology", h.getOntology) mux.HandleFunc("POST /api/v1/teaching-assets/prompts", h.generateTeachingAssetPrompt) mux.HandleFunc("GET /api/v1/teaching-assets", h.getTeachingAssets) + if h.cfg.HasDeploy() { + mux.HandleFunc("POST /api/v1/_deploy", h.handleDeploy) + } mux.Handle("GET /", webapp.Handler()) return mux }