feat: add server foundation (config, database, auth, main)
- config: Load from env vars (SERVER_PORT, DATABASE_URL, JWT_SECRET, CAS_DIR, etc.) - database: pgx/v5 connection pool, models (User, YggdrasilSession, Modpack, GlobalFile, LauncherRelease) - auth: Yggdrasil endpoints (authenticate, refresh, validate) with SHA-256 password hashing, token rotation - main: graceful shutdown, HTTP server on configured port - go.mod: module gitea.mrixs.me/Mrixs/MrixsCraft-server, pgx/v5 dependency Co-Authored-By: OWL <noreply@anthropic.com>
This commit is contained in:
@@ -1,26 +1,77 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"context"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/auth"
|
||||||
|
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config"
|
||||||
|
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
port := os.Getenv("SERVER_PORT")
|
ctx := context.Background()
|
||||||
if port == "" {
|
|
||||||
port = "8080"
|
cfg, err := config.Load()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to load config: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
db, err := database.Open(ctx, cfg.DatabaseURL)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to connect to database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
|
// Health check.
|
||||||
|
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
fmt.Fprintln(w, "ok")
|
w.Write([]byte("ok"))
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Printf("MrixsCraft Server starting on :%s", port)
|
// Yggdrasil API.
|
||||||
if err := http.ListenAndServe(":"+port, mux); err != nil {
|
authHandler := auth.NewHandler(db, cfg)
|
||||||
log.Fatal(err)
|
authHandler.RegisterRoutes(mux)
|
||||||
|
|
||||||
|
// TODO: register API, Admin, CAS routes.
|
||||||
|
|
||||||
|
addr := ":" + itoa(cfg.Port)
|
||||||
|
srv := &http.Server{
|
||||||
|
Addr: addr,
|
||||||
|
Handler: mux,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Graceful shutdown.
|
||||||
|
done := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(done, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Printf("MrixsCraft Server starting on %s", addr)
|
||||||
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("Server error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
<-done
|
||||||
|
log.Println("Shutting down…")
|
||||||
|
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||||
|
log.Printf("Shutdown error: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("Stopped.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// itoa converts int to string (stdlib alias to avoid fmt import just for this).
|
||||||
|
func itoa(n int) string {
|
||||||
|
return strconv.Itoa(n)
|
||||||
}
|
}
|
||||||
|
|||||||
13
go.mod
13
go.mod
@@ -1,3 +1,14 @@
|
|||||||
module github.com/Mrixs/MrixsCraft-server
|
module gitea.mrixs.me/Mrixs/MrixsCraft-server
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
|
require github.com/jackc/pgx/v5 v5.6.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||||
|
github.com/jackc/puddle/v2 v2.2.1 // indirect
|
||||||
|
golang.org/x/crypto v0.17.0 // indirect
|
||||||
|
golang.org/x/sync v0.1.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,2 +1,288 @@
|
|||||||
// Package auth implements Yggdrasil authentication protocol.
|
// package auth implements the Yggdrasil authentication protocol.
|
||||||
package auth
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/subtle"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/config"
|
||||||
|
"gitea.mrixs.me/Mrixs/MrixsCraft-server/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Handler serves Yggdrasil endpoints.
|
||||||
|
type Handler struct {
|
||||||
|
db *database.DB
|
||||||
|
cfg *config.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHandler creates a new auth handler.
|
||||||
|
func NewHandler(db *database.DB, cfg *config.Config) *Handler {
|
||||||
|
return &Handler{db: db, cfg: cfg}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRoutes mounts the Yggdrasil endpoints on the given mux.
|
||||||
|
func (h *Handler) RegisterRoutes(mux *http.ServeMux) {
|
||||||
|
mux.HandleFunc("POST /authserver/authenticate", h.authenticate)
|
||||||
|
mux.HandleFunc("POST /authserver/refresh", h.refresh)
|
||||||
|
mux.HandleFunc("POST /authserver/validate", h.validate)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Request / Response types ──────────────────────────────────
|
||||||
|
|
||||||
|
type authenticateRequest struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type authenticateResponse struct {
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
ClientToken string `json:"clientToken"`
|
||||||
|
AvailableProfile []profile `json:"availableProfiles"`
|
||||||
|
SelectedProfile *profile `json:"selectedProfile,omitempty"`
|
||||||
|
User *userProperties `json:"user,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type profile struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type userProperties struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Properties []property `json:"properties"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type property struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type refreshRequest struct {
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
ClientToken string `json:"clientToken"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type refreshResponse struct {
|
||||||
|
AccessToken string `json:"accessToken"`
|
||||||
|
ClientToken string `json:"clientToken"`
|
||||||
|
SelectedProfile *profile `json:"selectedProfile"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type errorResponse struct {
|
||||||
|
Error string `json:"error"`
|
||||||
|
ErrorMessage string `json:"errorMessage"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Handlers ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func (h *Handler) authenticate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Bad Request", "Cannot read body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req authenticateRequest
|
||||||
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Bad Request", "Invalid JSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up user by username or email.
|
||||||
|
user, err := h.findUser(r.Context(), req.Username)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusUnauthorized, "Forbidden", "Invalid credentials")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify password (SHA-256 hex comparison).
|
||||||
|
if !verifyPassword(req.Password, user.PasswordHash) {
|
||||||
|
writeError(w, http.StatusUnauthorized, "Forbidden", "Invalid credentials")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate tokens.
|
||||||
|
accessToken := generateToken()
|
||||||
|
clientToken := generateToken()
|
||||||
|
|
||||||
|
// Store session.
|
||||||
|
expiresAt := time.Now().Add(24 * time.Hour)
|
||||||
|
_, err = h.db.Pool().Exec(r.Context(),
|
||||||
|
`INSERT INTO yggdrasil_sessions (client_token, access_token, user_id, expires_at)
|
||||||
|
VALUES ($1, $2, $3, $4)`,
|
||||||
|
clientToken, accessToken, user.ID, expiresAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Internal Error", "Failed to create session")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp := authenticateResponse{
|
||||||
|
AccessToken: accessToken,
|
||||||
|
ClientToken: clientToken,
|
||||||
|
SelectedProfile: &profile{
|
||||||
|
ID: user.UUID,
|
||||||
|
Name: user.Username,
|
||||||
|
},
|
||||||
|
AvailableProfile: []profile{{
|
||||||
|
ID: user.UUID,
|
||||||
|
Name: user.Username,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) refresh(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Bad Request", "Cannot read body")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req refreshRequest
|
||||||
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
|
writeError(w, http.StatusBadRequest, "Bad Request", "Invalid JSON")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look up session.
|
||||||
|
var userID int
|
||||||
|
var expiresAt time.Time
|
||||||
|
err = h.db.Pool().QueryRow(r.Context(),
|
||||||
|
`SELECT user_id, expires_at FROM yggdrasil_sessions
|
||||||
|
WHERE access_token = $1 AND client_token = $2`,
|
||||||
|
req.AccessToken, req.ClientToken,
|
||||||
|
).Scan(&userID, &expiresAt)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusUnauthorized, "Forbidden", "Invalid token")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().After(expiresAt) {
|
||||||
|
writeError(w, http.StatusUnauthorized, "Forbidden", "Token expired")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rotate access token.
|
||||||
|
newAccessToken := generateToken()
|
||||||
|
_, err = h.db.Pool().Exec(r.Context(),
|
||||||
|
`UPDATE yggdrasil_sessions SET access_token = $1, expires_at = $2
|
||||||
|
WHERE access_token = $3`,
|
||||||
|
newAccessToken, time.Now().Add(24*time.Hour), req.AccessToken,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Internal Error", "Failed to refresh")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get user info.
|
||||||
|
var username, uuid string
|
||||||
|
err = h.db.Pool().QueryRow(r.Context(),
|
||||||
|
`SELECT username, uuid FROM users WHERE id = $1`, userID,
|
||||||
|
).Scan(&username, &uuid)
|
||||||
|
if err != nil {
|
||||||
|
writeError(w, http.StatusInternalServerError, "Internal Error", "User not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
writeJSON(w, http.StatusOK, refreshResponse{
|
||||||
|
AccessToken: newAccessToken,
|
||||||
|
ClientToken: req.ClientToken,
|
||||||
|
SelectedProfile: &profile{
|
||||||
|
ID: uuid,
|
||||||
|
Name: username,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) validate(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var req refreshRequest
|
||||||
|
if err := json.Unmarshal(body, &req); err != nil {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var expiresAt time.Time
|
||||||
|
err = h.db.Pool().QueryRow(r.Context(),
|
||||||
|
`SELECT expires_at FROM yggdrasil_sessions
|
||||||
|
WHERE access_token = $1 AND client_token = $2`,
|
||||||
|
req.AccessToken, req.ClientToken,
|
||||||
|
).Scan(&expiresAt)
|
||||||
|
|
||||||
|
if err != nil || time.Now().After(expiresAt) {
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Helpers ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
func (h *Handler) findUser(ctx context.Context, login string) (*database.User, error) {
|
||||||
|
var user database.User
|
||||||
|
err := h.db.Pool().QueryRow(ctx,
|
||||||
|
`SELECT id, username, email, password_hash, uuid, role FROM users
|
||||||
|
WHERE username = $1 OR email = $1`, login,
|
||||||
|
).Scan(&user.ID, &user.Username, &user.Email, &user.PasswordHash, &user.UUID, &user.Role)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyPassword(password, hash string) bool {
|
||||||
|
h := sha256.Sum256([]byte(password))
|
||||||
|
return subtle.ConstantTimeCompare([]byte(hex.EncodeToString(h[:])), []byte(hash)) == 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateToken() string {
|
||||||
|
b := make([]byte, 16)
|
||||||
|
_, _ = rand.Read(b)
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_ = json.NewEncoder(w).Encode(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeError(w http.ResponseWriter, status int, err, msg string) {
|
||||||
|
writeJSON(w, status, errorResponse{
|
||||||
|
Error: err,
|
||||||
|
ErrorMessage: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashPassword returns the SHA-256 hex of a password for storage.
|
||||||
|
func HashPassword(password string) string {
|
||||||
|
h := sha256.Sum256([]byte(password))
|
||||||
|
return hex.EncodeToString(h[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateUUID creates a random UUID v4-like string.
|
||||||
|
func GenerateUUID() string {
|
||||||
|
b := make([]byte, 16)
|
||||||
|
_, _ = rand.Read(b)
|
||||||
|
b[6] = (b[6] & 0x0f) | 0x40 // version 4
|
||||||
|
b[8] = (b[8] & 0x3f) | 0x80 // variant
|
||||||
|
return fmt.Sprintf("%x-%x-%x-%x-%x",
|
||||||
|
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,62 @@
|
|||||||
// package config handles server configuration (env vars, config files).
|
// package config handles server configuration from environment variables.
|
||||||
package config
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Config holds all server configuration.
|
||||||
|
type Config struct {
|
||||||
|
// HTTP
|
||||||
|
Port int `json:"port"`
|
||||||
|
|
||||||
|
// Database
|
||||||
|
DatabaseURL string `json:"database_url"`
|
||||||
|
|
||||||
|
// Storage
|
||||||
|
CASDir string `json:"cas_dir"` // Content-Addressable Storage root
|
||||||
|
SkinsDir string `json:"skins_dir"` // Uploaded skins
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
JWTSecret string `json:"jwt_secret"`
|
||||||
|
CIsecret string `json:"ci_secret"` // Token for CI/CD launcher release endpoint
|
||||||
|
|
||||||
|
// Public
|
||||||
|
BaseURL string `json:"base_url"` // External URL for CDN links
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load reads configuration from environment variables with sensible defaults.
|
||||||
|
func Load() (*Config, error) {
|
||||||
|
port, err := strconv.Atoi(getEnv("SERVER_PORT", "8080"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("invalid SERVER_PORT: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := &Config{
|
||||||
|
Port: port,
|
||||||
|
DatabaseURL: getEnv("DATABASE_URL", ""),
|
||||||
|
CASDir: getEnv("CAS_DIR", "/var/www/cdn/files"),
|
||||||
|
SkinsDir: getEnv("SKINS_DIR", "/var/www/cdn/skins"),
|
||||||
|
JWTSecret: getEnv("JWT_SECRET", ""),
|
||||||
|
CIsecret: getEnv("CI_SECRET", ""),
|
||||||
|
BaseURL: getEnv("BASE_URL", "https://minecraft.mrixs.me"),
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.DatabaseURL == "" {
|
||||||
|
return nil, fmt.Errorf("DATABASE_URL is required")
|
||||||
|
}
|
||||||
|
if cfg.JWTSecret == "" {
|
||||||
|
return nil, fmt.Errorf("JWT_SECRET is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, fallback string) string {
|
||||||
|
if v := os.Getenv(key); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,87 @@
|
|||||||
// package database manages PostgreSQL connections, migrations, and data models.
|
// package database manages PostgreSQL connections and data models.
|
||||||
package database
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DB wraps pgxpool.Pool with application-specific helpers.
|
||||||
|
type DB struct {
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open creates a new database connection pool.
|
||||||
|
func Open(ctx context.Context, databaseURL string) (*DB, error) {
|
||||||
|
pool, err := pgxpool.New(ctx, databaseURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("connecting to database: %w", err)
|
||||||
|
}
|
||||||
|
if err := pool.Ping(ctx); err != nil {
|
||||||
|
pool.Close()
|
||||||
|
return nil, fmt.Errorf("pinging database: %w", err)
|
||||||
|
}
|
||||||
|
return &DB{pool: pool}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close shuts down the connection pool.
|
||||||
|
func (db *DB) Close() {
|
||||||
|
db.pool.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Pool returns the underlying pool for direct queries.
|
||||||
|
func (db *DB) Pool() *pgxpool.Pool {
|
||||||
|
return db.pool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Models ─────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// User represents a registered player.
|
||||||
|
type User struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Username string `db:"username"`
|
||||||
|
Email string `db:"email"`
|
||||||
|
PasswordHash string `db:"password_hash"`
|
||||||
|
UUID string `db:"uuid"`
|
||||||
|
Role string `db:"role"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// YggdrasilSession represents an active authentication session.
|
||||||
|
type YggdrasilSession struct {
|
||||||
|
ClientToken string `db:"client_token"`
|
||||||
|
AccessToken string `db:"access_token"`
|
||||||
|
UserID int `db:"user_id"`
|
||||||
|
ExpiresAt string `db:"expires_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modpack represents a game server / modpack configuration.
|
||||||
|
type Modpack struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Slug string `db:"slug"`
|
||||||
|
Name string `db:"name"`
|
||||||
|
MinecraftVersion string `db:"minecraft_version"`
|
||||||
|
JavaVersion int `db:"java_version"`
|
||||||
|
ServerIP string `db:"server_ip"`
|
||||||
|
IsActive bool `db:"is_active"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GlobalFile is a CAS entry.
|
||||||
|
type GlobalFile struct {
|
||||||
|
SHA1 string `db:"sha1"`
|
||||||
|
Size int64 `db:"size_bytes"`
|
||||||
|
FileName string `db:"file_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LauncherRelease tracks published launcher binaries.
|
||||||
|
type LauncherRelease struct {
|
||||||
|
ID int `db:"id"`
|
||||||
|
Version string `db:"version"`
|
||||||
|
OS string `db:"os"`
|
||||||
|
Arch string `db:"arch"`
|
||||||
|
SHA256 string `db:"sha256"`
|
||||||
|
FilePath string `db:"file_path"`
|
||||||
|
IsActive bool `db:"is_active"`
|
||||||
|
IsMandatory bool `db:"is_mandatory"`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user