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
import (
"kooltab-updateserver/internal/db"
"kooltab-updateserver/internal/routes"
"kooltap-updateserver/internal/db"
"kooltap-updateserver/internal/routes"
"os"
"github.com/gin-gonic/gin"
@@ -28,7 +28,7 @@ func main() {
})
r.Use(func(c *gin.Context) {
c.Header("Server", "KoolTab Updater")
c.Header("Server", "KoolTap Updater")
c.Next()
})
+15 -7
View File
@@ -1,5 +1,5 @@
services:
kooltab-updater:
kooltap-updater:
build:
context: .
dockerfile: Dockerfile
@@ -8,16 +8,24 @@ services:
environment:
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:
- kooltab_uploads:/app/uploads
- kooltab_tmp:/app/tmp
- kooltab_data:/app/data
- kooltap_uploads:/app/uploads
- kooltap_tmp:/app/tmp
- kooltap_data:/app/data
expose:
- "3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/"]
interval: 15s
timeout: 5s
retries: 5
volumes:
kooltab_uploads:
kooltab_tmp:
kooltab_data:
kooltap_uploads:
kooltap_tmp:
kooltap_data:
+4 -3
View File
@@ -1,9 +1,9 @@
version: "3.9"
services:
kooltab-updater:
image: kooltab-updater:latest
container_name: kooltab-updater
kooltap-updater:
image: kooltap-updater:latest
container_name: kooltap-updater
restart: unless-stopped
#ports:
@@ -11,6 +11,7 @@ services:
environment:
GIN_MODE: release
ADMIN_TOKEN: ${ADMIN_TOKEN}
volumes:
- ./uploads:/app/uploads
+1 -1
View File
@@ -1,4 +1,4 @@
module kooltab-updateserver
module kooltap-updateserver
go 1.26
+4 -1
View File
@@ -1,6 +1,7 @@
package auth
import (
"crypto/subtle"
"net/http"
"os"
)
@@ -15,7 +16,9 @@ func AdminAuth() gin.HandlerFunc {
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{
"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
import (
"kooltab-updateserver/internal/models"
"kooltap-updateserver/internal/models"
"log"
"github.com/glebarez/sqlite"
+40 -7
View File
@@ -3,11 +3,12 @@ package routes
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"kooltab-updateserver/internal/apk_util"
"kooltab-updateserver/internal/auth"
"kooltab-updateserver/internal/db"
"kooltab-updateserver/internal/models"
"kooltap-updateserver/internal/apk_util"
"kooltap-updateserver/internal/auth"
"kooltap-updateserver/internal/db"
"kooltap-updateserver/internal/models"
"net/http"
"os"
"path/filepath"
@@ -24,6 +25,8 @@ func Register(r *gin.Engine) {
api.GET("/releases", ListReleases)
}
r.GET("/download/latest", DownloadLatest)
admin := r.Group("/admin")
admin.Use(auth.AdminAuth())
{
@@ -38,9 +41,8 @@ func CheckUpdate(c *gin.Context) {
var latest models.Release
if err := db.DB.
Order("created_at desc").
First(&latest).Error; err != nil {
// The highest Android version code is the latest build, regardless of upload order
if err := latestRelease(&latest); err != nil {
c.JSON(http.StatusOK, gin.H{
"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) {
var releases []models.Release
@@ -124,6 +147,16 @@ func UploadRelease(c *gin.Context) {
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"
path := filepath.Join("uploads", filename)