feat: name change + bugfixes

This commit is contained in:
2026-10-09 22:55:02 +02:00
parent fb84e3384b
commit 95a25453df
8 changed files with 113 additions and 23 deletions
+3 -3
View File
@@ -1,8 +1,8 @@
package main package main
import ( import (
"kooltab-updateserver/internal/db" "kooltap-updateserver/internal/db"
"kooltab-updateserver/internal/routes" "kooltap-updateserver/internal/routes"
"os" "os"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@@ -28,7 +28,7 @@ func main() {
}) })
r.Use(func(c *gin.Context) { r.Use(func(c *gin.Context) {
c.Header("Server", "KoolTab Updater") c.Header("Server", "KoolTap Updater")
c.Next() c.Next()
}) })
+15 -7
View File
@@ -1,5 +1,5 @@
services: services:
kooltab-updater: kooltap-updater:
build: build:
context: . context: .
dockerfile: Dockerfile dockerfile: Dockerfile
@@ -8,16 +8,24 @@ services:
environment: environment:
GIN_MODE: release GIN_MODE: release
# Generated by Coolify (see Environment Variables); needed in the X-Admin-Token header to upload releases
ADMIN_TOKEN: ${SERVICE_PASSWORD_64_ADMINTOKEN}
volumes: volumes:
- kooltab_uploads:/app/uploads - kooltap_uploads:/app/uploads
- kooltab_tmp:/app/tmp - kooltap_tmp:/app/tmp
- kooltab_data:/app/data - kooltap_data:/app/data
expose: expose:
- "3000" - "3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/"]
interval: 15s
timeout: 5s
retries: 5
volumes: volumes:
kooltab_uploads: kooltap_uploads:
kooltab_tmp: kooltap_tmp:
kooltab_data: kooltap_data:
+4 -3
View File
@@ -1,9 +1,9 @@
version: "3.9" version: "3.9"
services: services:
kooltab-updater: kooltap-updater:
image: kooltab-updater:latest image: kooltap-updater:latest
container_name: kooltab-updater container_name: kooltap-updater
restart: unless-stopped restart: unless-stopped
#ports: #ports:
@@ -11,6 +11,7 @@ services:
environment: environment:
GIN_MODE: release GIN_MODE: release
ADMIN_TOKEN: ${ADMIN_TOKEN}
volumes: volumes:
- ./uploads:/app/uploads - ./uploads:/app/uploads
+1 -1
View File
@@ -1,4 +1,4 @@
module kooltab-updateserver module kooltap-updateserver
go 1.26 go 1.26
+4 -1
View File
@@ -1,6 +1,7 @@
package auth package auth
import ( import (
"crypto/subtle"
"net/http" "net/http"
"os" "os"
) )
@@ -15,7 +16,9 @@ func AdminAuth() gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
if c.GetHeader("X-Admin-Token") != token { // Without a configured token every admin request is refused, never allowed
provided := c.GetHeader("X-Admin-Token")
if token == "" || subtle.ConstantTimeCompare([]byte(provided), []byte(token)) != 1 {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{ c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{
"error": "Unauthorized", "error": "Unauthorized",
}) })
+45
View File
@@ -0,0 +1,45 @@
package auth
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
)
func status(t *testing.T, configured, provided string) int {
t.Setenv("ADMIN_TOKEN", configured)
gin.SetMode(gin.TestMode)
r := gin.New()
r.POST("/admin", AdminAuth(), func(c *gin.Context) { c.Status(http.StatusOK) })
req := httptest.NewRequest(http.MethodPost, "/admin", nil)
if provided != "" {
req.Header.Set("X-Admin-Token", provided)
}
w := httptest.NewRecorder()
r.ServeHTTP(w, req)
return w.Code
}
func TestAdminAuth(t *testing.T) {
cases := []struct {
name, configured, provided string
want int
}{
{"no token configured refuses requests without a header", "", "", http.StatusUnauthorized},
{"no token configured refuses any header", "", "anything", http.StatusUnauthorized},
{"wrong token", "secret", "nope", http.StatusUnauthorized},
{"missing header", "secret", "", http.StatusUnauthorized},
{"correct token", "secret", "secret", http.StatusOK},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := status(t, tc.configured, tc.provided); got != tc.want {
t.Fatalf("got %d, want %d", got, tc.want)
}
})
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
package db package db
import ( import (
"kooltab-updateserver/internal/models" "kooltap-updateserver/internal/models"
"log" "log"
"github.com/glebarez/sqlite" "github.com/glebarez/sqlite"
+40 -7
View File
@@ -3,11 +3,12 @@ package routes
import ( import (
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"fmt"
"io" "io"
"kooltab-updateserver/internal/apk_util" "kooltap-updateserver/internal/apk_util"
"kooltab-updateserver/internal/auth" "kooltap-updateserver/internal/auth"
"kooltab-updateserver/internal/db" "kooltap-updateserver/internal/db"
"kooltab-updateserver/internal/models" "kooltap-updateserver/internal/models"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@@ -24,6 +25,8 @@ func Register(r *gin.Engine) {
api.GET("/releases", ListReleases) api.GET("/releases", ListReleases)
} }
r.GET("/download/latest", DownloadLatest)
admin := r.Group("/admin") admin := r.Group("/admin")
admin.Use(auth.AdminAuth()) admin.Use(auth.AdminAuth())
{ {
@@ -38,9 +41,8 @@ func CheckUpdate(c *gin.Context) {
var latest models.Release var latest models.Release
if err := db.DB. // The highest Android version code is the latest build, regardless of upload order
Order("created_at desc"). if err := latestRelease(&latest); err != nil {
First(&latest).Error; err != nil {
c.JSON(http.StatusOK, gin.H{ c.JSON(http.StatusOK, gin.H{
"update": false, "update": false,
@@ -79,6 +81,27 @@ func CheckUpdate(c *gin.Context) {
}) })
} }
// DownloadLatest redirects to the newest APK, for installing on a new tablet from a browser
func DownloadLatest(c *gin.Context) {
var latest models.Release
if err := latestRelease(&latest); err != nil {
c.JSON(http.StatusNotFound, gin.H{
"error": "No releases yet",
})
return
}
c.Redirect(http.StatusFound, "/downloads/"+latest.APKFile)
}
func latestRelease(release *models.Release) error {
return db.DB.
Order("version_code desc").
First(release).Error
}
func ListReleases(c *gin.Context) { func ListReleases(c *gin.Context) {
var releases []models.Release var releases []models.Release
@@ -124,6 +147,16 @@ func UploadRelease(c *gin.Context) {
return return
} }
// Tablets only see the release with the highest version code, so an older or equal one would never be offered
var latest models.Release
if err := latestRelease(&latest); err == nil && int64(info.VersionCode) <= latest.VersionCode {
c.JSON(http.StatusConflict, gin.H{
"error": fmt.Sprintf("version code %d must be higher than the latest release (%s, code %d)",
info.VersionCode, latest.Version, latest.VersionCode),
})
return
}
filename := "app-" + info.Version + ".apk" filename := "app-" + info.Version + ".apk"
path := filepath.Join("uploads", filename) path := filepath.Join("uploads", filename)