diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..d69b1e4 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,90 @@ +name: CI + +on: + push: + branches: ["**"] + pull_request: + branches: ["main"] + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + + - name: go vet + run: go vet ./... + + - name: gofmt check + run: | + fmt=$(gofmt -l .) + if [ -n "$fmt" ]; then + echo "Files needing formatting:" + echo "$fmt" + exit 1 + fi + + test: + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + + - name: Run tests + run: go test ./... -v -race -cover + + build: + runs-on: ubuntu-latest + needs: test + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version: "1.25" + + - name: Build binary + run: go build -o mrixscraft-server ./cmd/server + + - name: Upload artifact + if: github.ref == 'refs/heads/main' + uses: actions/upload-artifact@v4 + with: + name: mrixscraft-server + path: mrixscraft-server + + docker: + runs-on: ubuntu-latest + needs: build + if: github.ref == 'refs/heads/main' + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Gitea Registry + uses: docker/login-action@v3 + with: + registry: gitea.mrixs.me + username: ${{ secrets.GITEA_USERNAME }} + password: ${{ secrets.GITEA_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: | + gitea.mrixs.me/Mrixs/MrixsCraft-server:latest + gitea.mrixs.me/Mrixs/MrixsCraft-server:${{ github.sha }} + cache-from: type=registry,ref=gitea.mrixs.me/Mrixs/MrixsCraft-server:latest + cache-to: type=inline diff --git a/cmd/server/main.go b/cmd/server/main.go index e7fdbe3..3a19bb2 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -81,7 +81,7 @@ func main() { Handler: handler, } - // Graceful shutdown. + // Graceful shutdown on SIGINT/SIGTERM. done := make(chan os.Signal, 1) signal.Notify(done, syscall.SIGINT, syscall.SIGTERM) diff --git a/internal/api/api.go b/internal/api/api.go index 41c5363..eea3e1c 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -7,6 +7,7 @@ import ( "image/png" "io" "net/http" + "net/mail" "os" "path/filepath" "strings" @@ -156,8 +157,12 @@ func (h *Handler) register(w http.ResponseWriter, r *http.Request) { return } - // Basic email validation. - if !strings.Contains(req.Email, "@") || !strings.Contains(req.Email, ".") { + // Basic email validation (RFC 5321). + if len(req.Email) > 254 { + utils.WriteError(w, http.StatusBadRequest, "Email too long (max 254 characters)") + return + } + if _, err := mail.ParseAddress(req.Email); err != nil { utils.WriteError(w, http.StatusBadRequest, "Invalid email address") return } diff --git a/internal/templates/html/base.html b/internal/templates/html/base.html new file mode 100644 index 0000000..5dfa98f --- /dev/null +++ b/internal/templates/html/base.html @@ -0,0 +1,103 @@ + + + + + + {{.Title}} — MrixsCraft + + + +
+
+ + +
+
+
{{template "content" .}}
+ + + diff --git a/internal/templates/html/index.html b/internal/templates/html/index.html new file mode 100644 index 0000000..8c43d5c --- /dev/null +++ b/internal/templates/html/index.html @@ -0,0 +1,17 @@ +{{define "content"}} +
+

Добро пожаловать в MrixsCraft

+

Приватный Minecraft-сервер с модпаками. Зарегистрируйся, скачай лаунчер и играй.

+

+ Начать играть +

+
+
+

Как начать

+
    +
  1. Зарегистрируйся на сайте
  2. +
  3. Скачай лаунчер для своей ОС
  4. +
  5. Авторизуйся, выбирай модпак и нажимай PLAY
  6. +
+
+{{end}} diff --git a/internal/templates/html/login.html b/internal/templates/html/login.html new file mode 100644 index 0000000..fe58358 --- /dev/null +++ b/internal/templates/html/login.html @@ -0,0 +1,42 @@ +{{define "content"}} +
+

Вход

+
+ + + + + + + +
+ +

Нет аккаунта? Зарегистрироваться

+
+ + +{{end}} diff --git a/internal/templates/html/register.html b/internal/templates/html/register.html new file mode 100644 index 0000000..ca5009d --- /dev/null +++ b/internal/templates/html/register.html @@ -0,0 +1,43 @@ +{{define "content"}} +
+

Регистрация

+
+ + + + + + + + + + +
+ +

Уже есть аккаунт? Войти

+
+ + +{{end}} diff --git a/internal/templates/templates.go b/internal/templates/templates.go index b77101a..15a6fc9 100644 --- a/internal/templates/templates.go +++ b/internal/templates/templates.go @@ -1,7 +1,4 @@ -// package templates handles Go html/template rendering for the site and admin panel. -// -// This is a placeholder implementation. Actual templates will be added -// when the web UI is designed. +// package templates handles Go html/template rendering for the website. package templates import ( @@ -15,14 +12,20 @@ import ( "gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database" ) -// Handler serves template-rendered pages. -type Handler struct { - db *database.DB - cfg *config.Config - layout *template.Template +// pageData is passed to all templates. +type pageData struct { + Title string } -// NewHandler creates a new templates handler and parses embedded/ondisk templates. +// Handler serves template-rendered pages. +type Handler struct { + db *database.DB + cfg *config.Config + tmpl *template.Template + loaded bool +} + +// NewHandler creates a new templates handler and parses on-disk templates. func NewHandler(db *database.DB, cfg *config.Config) *Handler { h := &Handler{db: db, cfg: cfg} h.parseTemplates() @@ -43,36 +46,47 @@ func (h *Handler) index(w http.ResponseWriter, r *http.Request) { http.NotFound(w, r) return } - if h.layout == nil { - w.WriteHeader(http.StatusOK) - w.Write([]byte("

MrixsCraft

")) + if !h.loaded { + fallback(w, "MrixsCraft") return } - h.layout.ExecuteTemplate(w, "index", nil) + h.render(w, "index.html", pageData{Title: "Главная"}) } func (h *Handler) loginPage(w http.ResponseWriter, r *http.Request) { - if h.layout == nil { - w.WriteHeader(http.StatusOK) - w.Write([]byte("

Login

")) + if !h.loaded { + fallback(w, "Login") return } - h.layout.ExecuteTemplate(w, "login", nil) + h.render(w, "login.html", pageData{Title: "Вход"}) } func (h *Handler) registerPage(w http.ResponseWriter, r *http.Request) { - if h.layout == nil { - w.WriteHeader(http.StatusOK) - w.Write([]byte("

Register

")) + if !h.loaded { + fallback(w, "Register") return } - h.layout.ExecuteTemplate(w, "register", nil) + h.render(w, "register.html", pageData{Title: "Регистрация"}) +} + +// render executes the named template with data, writing to w. +func (h *Handler) render(w http.ResponseWriter, name string, data pageData) { + w.Header().Set("Content-Type", "text/html; charset=utf-8") + if err := h.tmpl.ExecuteTemplate(w, name, data); err != nil { + log.Printf("Template error (%s): %v", name, err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + } +} + +// fallback writes a minimal placeholder when templates are missing. +func fallback(w http.ResponseWriter, title string) { + w.WriteHeader(http.StatusOK) + w.Write([]byte("

" + title + "

")) } // ── Template parsing ─────────────────────────────────────────── func (h *Handler) parseTemplates() { - // Look for templates in common locations. dirs := []string{ filepath.Join("internal", "templates", "html"), "templates", @@ -81,16 +95,18 @@ func (h *Handler) parseTemplates() { if _, err := os.Stat(dir); err != nil { continue } - tmpl, err := template.ParseGlob(filepath.Join(dir, "*.html")) - if err == nil && tmpl != nil { - h.layout = tmpl + pattern := filepath.Join(dir, "*.html") + tmpl, err := template.New("").Funcs(template.FuncMap{}).ParseGlob(pattern) + if err != nil { + log.Printf("Template parse error in %s: %v", dir, err) + continue + } + if tmpl != nil { + h.tmpl = tmpl + h.loaded = true log.Printf("Loaded templates from %s", dir) return } - if err != nil { - log.Printf("Template parse error in %s: %v", dir, err) - } } - // No templates found — handler will use placeholder responses. log.Println("No HTML templates found; using placeholder responses") } diff --git a/migrations/002_migration_history.sql b/migrations/002_migration_history.sql new file mode 100644 index 0000000..ef7808e --- /dev/null +++ b/migrations/002_migration_history.sql @@ -0,0 +1,12 @@ +-- 002_migration_history.sql — Track applied migrations. +-- Run manually: psql $DATABASE_URL -f migrations/002_migration_history.sql + +CREATE TABLE IF NOT EXISTS migration_history ( + id SERIAL PRIMARY KEY, + filename VARCHAR(255) UNIQUE NOT NULL, + applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +-- Record this migration. +INSERT INTO migration_history (filename) VALUES ('002_migration_history.sql') + ON CONFLICT (filename) DO NOTHING; diff --git a/migrations/README.md b/migrations/README.md new file mode 100644 index 0000000..e669485 --- /dev/null +++ b/migrations/README.md @@ -0,0 +1,14 @@ +# Migrations + +Applied manually in order: + +```bash +psql "$DATABASE_URL" -f migrations/001_init.sql # schema +psql "$DATABASE_URL" -f migrations/002_migration_history.sql # tracking +``` + +Check applied: + +```sql +SELECT * FROM migration_history ORDER BY id; +```