finalize M06-M12 with Go/SQLite/Synology NAS implementation decisions
This commit is contained in:
@@ -1,8 +1,39 @@
|
||||
# Milestone 6: App Store Backend API
|
||||
|
||||
**Status**: Planning
|
||||
**Status**: Decided
|
||||
**Goal**: REST API for app submission, review, and distribution.
|
||||
|
||||
## Decision
|
||||
|
||||
**Go + Chi router** with JSON REST API:
|
||||
|
||||
```
|
||||
Framework: Chi (lightweight, idiomatic)
|
||||
Validation: go-playground/validator/v10
|
||||
OpenAPI: ogen (generated from spec)
|
||||
Middleware: Custom auth, rate limiting, logging
|
||||
Database: SQLite via repository pattern
|
||||
```
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ mosis-portal container │
|
||||
│ ┌────────────────────────────────────────────────────────────┐ │
|
||||
│ │ Chi Router │ │
|
||||
│ │ /v1/auth/* /v1/apps/* /v1/store/* /v1/keys/* │ │
|
||||
│ └──────────────────────┬─────────────────────────────────────┘ │
|
||||
│ │ │
|
||||
│ ┌──────────┬───────────┼───────────┬──────────┐ │
|
||||
│ │ Handlers │ Services │ Repos │ SQLite │ │
|
||||
│ └──────────┴───────────┴───────────┴──────────┘ │
|
||||
│ │
|
||||
│ /volume1/mosis/data/portal.db │
|
||||
│ /volume1/mosis/packages/ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Overview
|
||||
@@ -319,108 +350,128 @@ GET /v1/apps/:id/crashes:
|
||||
|
||||
---
|
||||
|
||||
## Data Models
|
||||
## Data Models (Go)
|
||||
|
||||
### Developer
|
||||
|
||||
```typescript
|
||||
interface Developer {
|
||||
id: string;
|
||||
email: string;
|
||||
name: string;
|
||||
avatar_url?: string;
|
||||
verified: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
```go
|
||||
type Developer struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Name string `json:"name"`
|
||||
AvatarURL *string `json:"avatar_url,omitempty"`
|
||||
Verified bool `json:"verified"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
```
|
||||
|
||||
### App
|
||||
|
||||
```typescript
|
||||
interface App {
|
||||
id: string;
|
||||
package_id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
tags: string[];
|
||||
status: 'draft' | 'published' | 'suspended';
|
||||
icon_url?: string;
|
||||
latest_version?: AppVersion;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
```go
|
||||
type AppStatus string
|
||||
|
||||
const (
|
||||
AppStatusDraft AppStatus = "draft"
|
||||
AppStatusPublished AppStatus = "published"
|
||||
AppStatusSuspended AppStatus = "suspended"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
ID string `json:"id"`
|
||||
PackageID string `json:"package_id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Category *string `json:"category,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
Status AppStatus `json:"status"`
|
||||
IconURL *string `json:"icon_url,omitempty"`
|
||||
LatestVersion *AppVersion `json:"latest_version,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
```
|
||||
|
||||
### AppVersion
|
||||
|
||||
```typescript
|
||||
interface AppVersion {
|
||||
id: string;
|
||||
app_id: string;
|
||||
version_name: string;
|
||||
version_code: number;
|
||||
package_url?: string;
|
||||
package_size?: number;
|
||||
signature?: string;
|
||||
permissions: string[];
|
||||
min_mosis_version?: string;
|
||||
release_notes?: string;
|
||||
status: 'draft' | 'uploading' | 'validating' | 'review' | 'approved' | 'published' | 'rejected';
|
||||
review_notes?: string;
|
||||
published_at?: string;
|
||||
created_at: string;
|
||||
```go
|
||||
type VersionStatus string
|
||||
|
||||
const (
|
||||
VersionStatusDraft VersionStatus = "draft"
|
||||
VersionStatusUploading VersionStatus = "uploading"
|
||||
VersionStatusValidating VersionStatus = "validating"
|
||||
VersionStatusReview VersionStatus = "review"
|
||||
VersionStatusApproved VersionStatus = "approved"
|
||||
VersionStatusPublished VersionStatus = "published"
|
||||
VersionStatusRejected VersionStatus = "rejected"
|
||||
)
|
||||
|
||||
type AppVersion struct {
|
||||
ID string `json:"id"`
|
||||
AppID string `json:"app_id"`
|
||||
VersionName string `json:"version_name"`
|
||||
VersionCode int `json:"version_code"`
|
||||
PackageURL *string `json:"package_url,omitempty"`
|
||||
PackageSize *int64 `json:"package_size,omitempty"`
|
||||
Signature *string `json:"signature,omitempty"`
|
||||
Permissions []string `json:"permissions"`
|
||||
MinMosisVersion *string `json:"min_mosis_version,omitempty"`
|
||||
ReleaseNotes *string `json:"release_notes,omitempty"`
|
||||
Status VersionStatus `json:"status"`
|
||||
ReviewNotes *string `json:"review_notes,omitempty"`
|
||||
PublishedAt *time.Time `json:"published_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
```
|
||||
|
||||
### PublicApp
|
||||
|
||||
```typescript
|
||||
interface PublicApp {
|
||||
package_id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
category?: string;
|
||||
tags: string[];
|
||||
icon_url?: string;
|
||||
author_name: string;
|
||||
latest_version: string;
|
||||
download_count: number;
|
||||
rating?: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
```go
|
||||
type PublicApp struct {
|
||||
PackageID string `json:"package_id"`
|
||||
Name string `json:"name"`
|
||||
Description *string `json:"description,omitempty"`
|
||||
Category *string `json:"category,omitempty"`
|
||||
Tags []string `json:"tags"`
|
||||
IconURL *string `json:"icon_url,omitempty"`
|
||||
AuthorName string `json:"author_name"`
|
||||
LatestVersion string `json:"latest_version"`
|
||||
DownloadCount int64 `json:"download_count"`
|
||||
Rating *float64 `json:"rating,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
```
|
||||
|
||||
### APIKey
|
||||
|
||||
```typescript
|
||||
interface APIKey {
|
||||
id: string;
|
||||
name: string;
|
||||
key_prefix: string; // "mk_live_abc..."
|
||||
permissions: string[];
|
||||
last_used_at?: string;
|
||||
expires_at?: string;
|
||||
created_at: string;
|
||||
```go
|
||||
type APIKey struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
KeyPrefix string `json:"key_prefix"` // "mk_live_abc..."
|
||||
Permissions []string `json:"permissions"`
|
||||
LastUsedAt *time.Time `json:"last_used_at,omitempty"`
|
||||
ExpiresAt *time.Time `json:"expires_at,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
}
|
||||
```
|
||||
|
||||
### CrashReport
|
||||
|
||||
```typescript
|
||||
interface CrashReport {
|
||||
id: string;
|
||||
app_id: string;
|
||||
app_version: string;
|
||||
crash_type: string;
|
||||
message: string;
|
||||
stack_trace: string;
|
||||
context: object;
|
||||
occurrences: number;
|
||||
first_seen: string;
|
||||
last_seen: string;
|
||||
```go
|
||||
type CrashReport struct {
|
||||
ID string `json:"id"`
|
||||
AppID string `json:"app_id"`
|
||||
AppVersion string `json:"app_version"`
|
||||
CrashType string `json:"crash_type"`
|
||||
Message string `json:"message"`
|
||||
StackTrace string `json:"stack_trace"`
|
||||
Context map[string]interface{} `json:"context"`
|
||||
Occurrences int `json:"occurrences"`
|
||||
FirstSeen time.Time `json:"first_seen"`
|
||||
LastSeen time.Time `json:"last_seen"`
|
||||
}
|
||||
```
|
||||
|
||||
@@ -532,40 +583,100 @@ Full OpenAPI 3.0 spec will be generated and hosted at:
|
||||
|
||||
---
|
||||
|
||||
## Implementation Structure
|
||||
## Implementation Structure (Go)
|
||||
|
||||
```
|
||||
src/
|
||||
├── main.go (or index.ts)
|
||||
cmd/
|
||||
└── portal/
|
||||
└── main.go # Entry point, wire dependencies
|
||||
|
||||
internal/
|
||||
├── api/
|
||||
│ ├── routes.go
|
||||
│ ├── router.go # Chi router setup
|
||||
│ ├── middleware/
|
||||
│ │ ├── auth.go
|
||||
│ │ ├── ratelimit.go
|
||||
│ │ └── logging.go
|
||||
│ │ ├── auth.go # JWT/API key validation
|
||||
│ │ ├── ratelimit.go # Token bucket rate limiter
|
||||
│ │ ├── logging.go # Request/response logging
|
||||
│ │ └── recovery.go # Panic recovery
|
||||
│ └── handlers/
|
||||
│ ├── auth.go
|
||||
│ ├── apps.go
|
||||
│ ├── versions.go
|
||||
│ ├── store.go
|
||||
│ ├── keys.go
|
||||
│ └── telemetry.go
|
||||
│ ├── auth.go # OAuth2 + token endpoints
|
||||
│ ├── apps.go # App CRUD
|
||||
│ ├── versions.go # Version upload/publish
|
||||
│ ├── store.go # Public store API
|
||||
│ ├── keys.go # API/signing keys
|
||||
│ └── telemetry.go # Event ingestion
|
||||
├── service/
|
||||
│ ├── app_service.go
|
||||
│ ├── version_service.go
|
||||
│ ├── auth_service.go
|
||||
│ └── storage_service.go
|
||||
├── repository/
|
||||
│ ├── app_repo.go
|
||||
│ ├── version_repo.go
|
||||
│ └── developer_repo.go
|
||||
├── domain/
|
||||
│ ├── app.go
|
||||
│ ├── app.go # Business logic
|
||||
│ ├── version.go
|
||||
│ └── developer.go
|
||||
└── pkg/
|
||||
├── validator/
|
||||
└── crypto/
|
||||
│ ├── auth.go
|
||||
│ └── storage.go # File storage operations
|
||||
├── repository/
|
||||
│ ├── sqlite/ # SQLite implementations
|
||||
│ │ ├── app.go
|
||||
│ │ ├── version.go
|
||||
│ │ ├── developer.go
|
||||
│ │ └── migrations/ # SQL migrations
|
||||
│ └── interfaces.go # Repository interfaces
|
||||
└── domain/
|
||||
├── app.go # Domain types
|
||||
├── version.go
|
||||
└── developer.go
|
||||
|
||||
pkg/
|
||||
├── validator/ # Custom validation rules
|
||||
└── signing/ # Ed25519 operations
|
||||
```
|
||||
|
||||
### Chi Router Setup
|
||||
|
||||
```go
|
||||
func NewRouter(
|
||||
authHandler *handlers.AuthHandler,
|
||||
appHandler *handlers.AppHandler,
|
||||
storeHandler *handlers.StoreHandler,
|
||||
) chi.Router {
|
||||
r := chi.NewRouter()
|
||||
|
||||
// Global middleware
|
||||
r.Use(middleware.RequestID)
|
||||
r.Use(middleware.RealIP)
|
||||
r.Use(middleware.Logger)
|
||||
r.Use(middleware.Recoverer)
|
||||
r.Use(middleware.Timeout(30 * time.Second))
|
||||
|
||||
// API v1
|
||||
r.Route("/v1", func(r chi.Router) {
|
||||
// Public routes
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Post("/auth/oauth/github", authHandler.GitHubOAuth)
|
||||
r.Get("/auth/oauth/github/callback", authHandler.GitHubCallback)
|
||||
r.Post("/auth/refresh", authHandler.Refresh)
|
||||
r.Get("/store/apps", storeHandler.ListApps)
|
||||
r.Get("/store/apps/{packageID}", storeHandler.GetApp)
|
||||
})
|
||||
|
||||
// Protected routes
|
||||
r.Group(func(r chi.Router) {
|
||||
r.Use(middleware.RequireAuth)
|
||||
r.Get("/auth/me", authHandler.Me)
|
||||
r.Route("/apps", func(r chi.Router) {
|
||||
r.Get("/", appHandler.List)
|
||||
r.Post("/", appHandler.Create)
|
||||
r.Route("/{appID}", func(r chi.Router) {
|
||||
r.Get("/", appHandler.Get)
|
||||
r.Patch("/", appHandler.Update)
|
||||
r.Delete("/", appHandler.Delete)
|
||||
r.Route("/versions", func(r chi.Router) {
|
||||
r.Get("/", appHandler.ListVersions)
|
||||
r.Post("/", appHandler.CreateVersion)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
@@ -590,10 +701,10 @@ src/
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. GraphQL alongside REST?
|
||||
2. WebSocket for real-time review status?
|
||||
3. Batch operations for bulk updates?
|
||||
4. API versioning strategy (URL vs header)?
|
||||
1. ~~GraphQL alongside REST?~~ → REST only for simplicity
|
||||
2. WebSocket for real-time review status? → Consider for v1.1
|
||||
3. ~~Batch operations for bulk updates?~~ → Not needed for MVP
|
||||
4. ~~API versioning strategy (URL vs header)?~~ → URL prefix (/v1/)
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user