add app review system with validation pipeline and admin htmx UI
This commit is contained in:
@@ -878,3 +878,209 @@ func (db *DB) GetPublishedVersionByCode(ctx context.Context, packageID string, v
|
||||
|
||||
return scanVersion(row)
|
||||
}
|
||||
|
||||
// VersionWithApp combines version data with its parent app data for review display
|
||||
type VersionWithApp struct {
|
||||
Version *AppVersion `json:"version"`
|
||||
App *App `json:"app"`
|
||||
DeveloperName string `json:"developer_name"`
|
||||
DeveloperEmail string `json:"developer_email"`
|
||||
}
|
||||
|
||||
// GetVersionsInReview returns versions pending review with pagination
|
||||
func (db *DB) GetVersionsInReview(ctx context.Context, limit, offset int) ([]VersionWithApp, int, error) {
|
||||
// Get total count
|
||||
var total int
|
||||
err := db.QueryRowContext(ctx, `
|
||||
SELECT COUNT(*) FROM app_versions WHERE status = 'in_review'
|
||||
`).Scan(&total)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
// Query versions with app and developer info
|
||||
rows, err := db.QueryContext(ctx, `
|
||||
SELECT v.id, v.app_id, v.version_code, v.version_name, v.package_url, v.package_size, v.signature,
|
||||
v.permissions, v.min_mosis_version, v.release_notes, v.status, v.review_notes, v.published_at, v.created_at,
|
||||
a.id, a.developer_id, a.package_id, a.name, a.description, a.category, a.tags, a.status, a.created_at, a.updated_at,
|
||||
d.name, d.email
|
||||
FROM app_versions v
|
||||
JOIN apps a ON a.id = v.app_id
|
||||
JOIN developers d ON d.id = a.developer_id
|
||||
WHERE v.status = 'in_review'
|
||||
ORDER BY v.created_at ASC
|
||||
LIMIT ? OFFSET ?
|
||||
`, limit, offset)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var results []VersionWithApp
|
||||
for rows.Next() {
|
||||
var vwa VersionWithApp
|
||||
var v AppVersion
|
||||
var app App
|
||||
|
||||
var vPackageURL, vSignature, vPermsJSON, vMinVersion, vReleaseNotes, vReviewNotes sql.NullString
|
||||
var vPublishedAt, vCreatedAt sql.NullString
|
||||
var vPackageSize sql.NullInt64
|
||||
var aDesc, aCat, aTagsJSON sql.NullString
|
||||
var aCreatedAt, aUpdatedAt string
|
||||
|
||||
err := rows.Scan(
|
||||
&v.ID, &v.AppID, &v.VersionCode, &v.VersionName, &vPackageURL, &vPackageSize, &vSignature,
|
||||
&vPermsJSON, &vMinVersion, &vReleaseNotes, &v.Status, &vReviewNotes, &vPublishedAt, &vCreatedAt,
|
||||
&app.ID, &app.DeveloperID, &app.PackageID, &app.Name, &aDesc, &aCat, &aTagsJSON, &app.Status, &aCreatedAt, &aUpdatedAt,
|
||||
&vwa.DeveloperName, &vwa.DeveloperEmail,
|
||||
)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
// Populate version
|
||||
v.PackageURL = vPackageURL.String
|
||||
v.PackageSize = vPackageSize.Int64
|
||||
v.Signature = vSignature.String
|
||||
v.MinMosisVersion = vMinVersion.String
|
||||
v.ReleaseNotes = vReleaseNotes.String
|
||||
v.ReviewNotes = vReviewNotes.String
|
||||
v.Permissions = []string{}
|
||||
if vPermsJSON.Valid && vPermsJSON.String != "" {
|
||||
json.Unmarshal([]byte(vPermsJSON.String), &v.Permissions)
|
||||
}
|
||||
if vCreatedAt.Valid {
|
||||
v.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", vCreatedAt.String)
|
||||
}
|
||||
if vPublishedAt.Valid {
|
||||
t, _ := time.Parse("2006-01-02 15:04:05", vPublishedAt.String)
|
||||
v.PublishedAt = &t
|
||||
}
|
||||
|
||||
// Populate app
|
||||
app.Description = aDesc.String
|
||||
app.Category = aCat.String
|
||||
app.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", aCreatedAt)
|
||||
app.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", aUpdatedAt)
|
||||
app.Tags = []string{}
|
||||
if aTagsJSON.Valid && aTagsJSON.String != "" {
|
||||
json.Unmarshal([]byte(aTagsJSON.String), &app.Tags)
|
||||
}
|
||||
|
||||
vwa.Version = &v
|
||||
vwa.App = &app
|
||||
results = append(results, vwa)
|
||||
}
|
||||
|
||||
return results, total, nil
|
||||
}
|
||||
|
||||
// GetVersionWithApp retrieves a version with its app and developer info
|
||||
func (db *DB) GetVersionWithApp(ctx context.Context, versionID string) (*VersionWithApp, error) {
|
||||
row := db.QueryRowContext(ctx, `
|
||||
SELECT v.id, v.app_id, v.version_code, v.version_name, v.package_url, v.package_size, v.signature,
|
||||
v.permissions, v.min_mosis_version, v.release_notes, v.status, v.review_notes, v.published_at, v.created_at,
|
||||
a.id, a.developer_id, a.package_id, a.name, a.description, a.category, a.tags, a.status, a.created_at, a.updated_at,
|
||||
d.name, d.email
|
||||
FROM app_versions v
|
||||
JOIN apps a ON a.id = v.app_id
|
||||
JOIN developers d ON d.id = a.developer_id
|
||||
WHERE v.id = ?
|
||||
`, versionID)
|
||||
|
||||
var vwa VersionWithApp
|
||||
var v AppVersion
|
||||
var app App
|
||||
|
||||
var vPackageURL, vSignature, vPermsJSON, vMinVersion, vReleaseNotes, vReviewNotes sql.NullString
|
||||
var vPublishedAt, vCreatedAt sql.NullString
|
||||
var vPackageSize sql.NullInt64
|
||||
var aDesc, aCat, aTagsJSON sql.NullString
|
||||
var aCreatedAt, aUpdatedAt string
|
||||
|
||||
err := row.Scan(
|
||||
&v.ID, &v.AppID, &v.VersionCode, &v.VersionName, &vPackageURL, &vPackageSize, &vSignature,
|
||||
&vPermsJSON, &vMinVersion, &vReleaseNotes, &v.Status, &vReviewNotes, &vPublishedAt, &vCreatedAt,
|
||||
&app.ID, &app.DeveloperID, &app.PackageID, &app.Name, &aDesc, &aCat, &aTagsJSON, &app.Status, &aCreatedAt, &aUpdatedAt,
|
||||
&vwa.DeveloperName, &vwa.DeveloperEmail,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Populate version
|
||||
v.PackageURL = vPackageURL.String
|
||||
v.PackageSize = vPackageSize.Int64
|
||||
v.Signature = vSignature.String
|
||||
v.MinMosisVersion = vMinVersion.String
|
||||
v.ReleaseNotes = vReleaseNotes.String
|
||||
v.ReviewNotes = vReviewNotes.String
|
||||
v.Permissions = []string{}
|
||||
if vPermsJSON.Valid && vPermsJSON.String != "" {
|
||||
json.Unmarshal([]byte(vPermsJSON.String), &v.Permissions)
|
||||
}
|
||||
if vCreatedAt.Valid {
|
||||
v.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", vCreatedAt.String)
|
||||
}
|
||||
if vPublishedAt.Valid {
|
||||
t, _ := time.Parse("2006-01-02 15:04:05", vPublishedAt.String)
|
||||
v.PublishedAt = &t
|
||||
}
|
||||
|
||||
// Populate app
|
||||
app.Description = aDesc.String
|
||||
app.Category = aCat.String
|
||||
app.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", aCreatedAt)
|
||||
app.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", aUpdatedAt)
|
||||
app.Tags = []string{}
|
||||
if aTagsJSON.Valid && aTagsJSON.String != "" {
|
||||
json.Unmarshal([]byte(aTagsJSON.String), &app.Tags)
|
||||
}
|
||||
|
||||
vwa.Version = &v
|
||||
vwa.App = &app
|
||||
return &vwa, nil
|
||||
}
|
||||
|
||||
// ApproveVersion approves a version and optionally publishes it
|
||||
func (db *DB) ApproveVersion(ctx context.Context, versionID, reviewerNotes string) error {
|
||||
_, err := db.ExecContext(ctx, `
|
||||
UPDATE app_versions
|
||||
SET status = 'published', review_notes = ?, published_at = datetime('now')
|
||||
WHERE id = ?
|
||||
`, reviewerNotes, versionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Also update the app status to published
|
||||
_, err = db.ExecContext(ctx, `
|
||||
UPDATE apps SET status = 'published', updated_at = datetime('now')
|
||||
WHERE id = (SELECT app_id FROM app_versions WHERE id = ?)
|
||||
`, versionID)
|
||||
return err
|
||||
}
|
||||
|
||||
// RejectVersion rejects a version with feedback
|
||||
func (db *DB) RejectVersion(ctx context.Context, versionID, reason, message string) error {
|
||||
notes := reason
|
||||
if message != "" {
|
||||
notes = reason + ": " + message
|
||||
}
|
||||
_, err := db.ExecContext(ctx, `
|
||||
UPDATE app_versions SET status = 'rejected', review_notes = ? WHERE id = ?
|
||||
`, notes, versionID)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetReviewStats returns statistics about the review queue
|
||||
func (db *DB) GetReviewStats(ctx context.Context) (pending, approved, rejected int, err error) {
|
||||
err = db.QueryRowContext(ctx, `
|
||||
SELECT
|
||||
COALESCE(SUM(CASE WHEN status = 'in_review' THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN status = 'published' THEN 1 ELSE 0 END), 0),
|
||||
COALESCE(SUM(CASE WHEN status = 'rejected' THEN 1 ELSE 0 END), 0)
|
||||
FROM app_versions
|
||||
`).Scan(&pending, &approved, &rejected)
|
||||
return
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user