feat: name change + bugfixes
This commit is contained in:
+3
-3
@@ -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()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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,4 +1,4 @@
|
|||||||
module kooltab-updateserver
|
module kooltap-updateserver
|
||||||
|
|
||||||
go 1.26
|
go 1.26
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user