From 8b655d3496d01be8bbbb171f7c50f11717adf0d5 Mon Sep 17 00:00:00 2001 From: loveuer Date: Thu, 20 Nov 2025 14:55:48 +0800 Subject: [PATCH] refactor: reorganize models to pkg/model and add authentication module - Move ORM models from internal/model to pkg/model organized by module (auth/k8s/registry) - Add authentication module with login, user management handlers - Update all import paths to use new model locations - Add frontend auth pages (Login, UserManagement) and authStore - Remove deprecated internal/model/model.go --- frontend/src/App.tsx | 75 +++- frontend/src/pages/Login.tsx | 132 +++++++ frontend/src/pages/UserManagement.tsx | 353 +++++++++++++++++++ frontend/src/stores/authStore.ts | 77 ++++ go.mod | 2 + go.sum | 4 + internal/api/api.go | 24 +- internal/model/model.go | 92 ----- internal/module/auth/auth.go | 41 +++ internal/module/auth/handler.current.go | 42 +++ internal/module/auth/handler.login.go | 97 +++++ internal/module/auth/handler.user.go | 207 +++++++++++ internal/module/auth/wallpaper.go | 108 ++++++ internal/module/k8s/handler.config.go | 6 +- internal/module/k8s/handler.resource.go | 10 +- internal/module/k8s/k8s.go | 4 +- internal/module/registry/blob.go | 14 +- internal/module/registry/catalog.go | 4 +- internal/module/registry/handler.config.go | 8 +- internal/module/registry/handler.download.go | 10 +- internal/module/registry/handler.fetch.go | 12 +- internal/module/registry/handler.list.go | 10 +- internal/module/registry/handler.upload.go | 24 +- internal/module/registry/manifest.go | 68 ++-- internal/module/registry/registry.go | 14 +- internal/module/registry/tag.go | 6 +- pkg/model/auth/user.go | 23 ++ pkg/model/k8s/config.go | 18 + pkg/model/registry/blob.go | 20 ++ pkg/model/registry/blob_upload.go | 20 ++ pkg/model/registry/config.go | 18 + pkg/model/registry/manifest.go | 22 ++ pkg/model/registry/repository.go | 17 + pkg/model/registry/tag.go | 19 + 34 files changed, 1410 insertions(+), 191 deletions(-) create mode 100644 frontend/src/pages/Login.tsx create mode 100644 frontend/src/pages/UserManagement.tsx create mode 100644 frontend/src/stores/authStore.ts delete mode 100644 internal/model/model.go create mode 100644 internal/module/auth/auth.go create mode 100644 internal/module/auth/handler.current.go create mode 100644 internal/module/auth/handler.login.go create mode 100644 internal/module/auth/handler.user.go create mode 100644 internal/module/auth/wallpaper.go create mode 100644 pkg/model/auth/user.go create mode 100644 pkg/model/k8s/config.go create mode 100644 pkg/model/registry/blob.go create mode 100644 pkg/model/registry/blob_upload.go create mode 100644 pkg/model/registry/config.go create mode 100644 pkg/model/registry/manifest.go create mode 100644 pkg/model/registry/repository.go create mode 100644 pkg/model/registry/tag.go diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5c92410..269ca58 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,11 +1,40 @@ -import { Container, Typography, Box, AppBar, Toolbar, Button, Stack } from '@mui/material' -import { Routes, Route, Link } from 'react-router-dom' +import { Container, Typography, Box, AppBar, Toolbar, Button, Stack, Menu, MenuItem, Avatar } from '@mui/material' +import { Routes, Route, Link, Navigate } from 'react-router-dom' +import { useState } from 'react' +import { AccountCircle, Logout } from '@mui/icons-material' import { useAppStore } from './stores/appStore' +import { useAuthStore } from './stores/authStore' import RegistryImageList from './pages/RegistryImageList' import K8sResourceList from './pages/K8sResourceList' +import UserManagement from './pages/UserManagement' +import Login from './pages/Login' function App() { const { count, increment, decrement, reset } = useAppStore() + const { isAuthenticated, logout, username } = useAuthStore() + const [anchorEl, setAnchorEl] = useState(null) + + const handleMenuOpen = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget) + } + + const handleMenuClose = () => { + setAnchorEl(null) + } + + const handleLogout = () => { + handleMenuClose() + logout() + } + + if (!isAuthenticated) { + return ( + + } /> + } /> + + ) + } return ( @@ -17,12 +46,54 @@ function App() { + + + + {username?.charAt(0).toUpperCase()} + + + {username} + + + + + + 退出登录 + + } /> } /> + } /> diff --git a/frontend/src/pages/Login.tsx b/frontend/src/pages/Login.tsx new file mode 100644 index 0000000..efccb65 --- /dev/null +++ b/frontend/src/pages/Login.tsx @@ -0,0 +1,132 @@ +import { useState } from 'react' +import { + Box, + Paper, + TextField, + Button, + Typography, + Alert, + CircularProgress, + Container, + InputAdornment, + IconButton, +} from '@mui/material' +import { Visibility, VisibilityOff } from '@mui/icons-material' +import { useAuthStore } from '../stores/authStore' + +export default function Login() { + const [username, setUsername] = useState('') + const [password, setPassword] = useState('') + const [showPassword, setShowPassword] = useState(false) + const { login, loading, error } = useAuthStore() + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + await login(username, password) + } + + return ( + + + + + Cluster + + + 登录到容器镜像仓库管理系统 + + + {error && ( + + {error} + + )} + +
+ setUsername(e.target.value)} + disabled={loading} + autoFocus + /> + setPassword(e.target.value)} + disabled={loading} + InputProps={{ + endAdornment: ( + + setShowPassword(!showPassword)} + edge="end" + > + {showPassword ? : } + + + ), + }} + /> + + +
+
+
+ ) +} diff --git a/frontend/src/pages/UserManagement.tsx b/frontend/src/pages/UserManagement.tsx new file mode 100644 index 0000000..9bba719 --- /dev/null +++ b/frontend/src/pages/UserManagement.tsx @@ -0,0 +1,353 @@ +import { useEffect, useState } from 'react' +import { + Box, + Typography, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + CircularProgress, + Alert, + Button, + Dialog, + DialogTitle, + DialogContent, + DialogActions, + TextField, + IconButton, + Chip, + FormControl, + InputLabel, + Select, + MenuItem, + Checkbox, + FormGroup, + FormControlLabel, +} from '@mui/material' +import { + Add as AddIcon, + Edit as EditIcon, + Delete as DeleteIcon, +} from '@mui/icons-material' + +interface User { + id: number + username: string + email: string + nickname: string + role: string + status: string + permissions: string + created_at: string +} + +interface UserFormData { + username: string + password: string + email: string + nickname: string + role: string + status: string + permissions: string[] +} + +const PERMISSION_OPTIONS = [ + { value: 'registry_read', label: '镜像仓库 - 读' }, + { value: 'registry_write', label: '镜像仓库 - 写' }, + { value: 'cluster_read', label: '集群管理 - 读' }, + { value: 'cluster_write', label: '集群管理 - 写' }, + { value: 'user_read', label: '用户管理 - 读' }, + { value: 'user_write', label: '用户管理 - 写' }, +] + +export default function UserManagement() { + const [users, setUsers] = useState([]) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [dialogOpen, setDialogOpen] = useState(false) + const [editingUser, setEditingUser] = useState(null) + const [formData, setFormData] = useState({ + username: '', + password: '', + email: '', + nickname: '', + role: 'user', + status: 'active', + permissions: [], + }) + + const fetchUsers = async () => { + setLoading(true) + setError(null) + try { + const res = await fetch('/api/v1/auth/user/list') + if (!res.ok) throw new Error(`HTTP ${res.status}`) + const result = await res.json() + setUsers(result.data?.users || []) + } catch (err: any) { + setError(err.message) + } finally { + setLoading(false) + } + } + + useEffect(() => { + fetchUsers() + }, []) + + const handleOpenDialog = (user?: User) => { + if (user) { + setEditingUser(user) + setFormData({ + username: user.username, + password: '', + email: user.email, + nickname: user.nickname, + role: user.role, + status: user.status, + permissions: user.permissions ? user.permissions.split(',') : [], + }) + } else { + setEditingUser(null) + setFormData({ + username: '', + password: '', + email: '', + nickname: '', + role: 'user', + status: 'active', + permissions: [], + }) + } + setDialogOpen(true) + } + + const handleCloseDialog = () => { + setDialogOpen(false) + setEditingUser(null) + } + + const handleSubmit = async () => { + try { + const payload = { + ...formData, + permissions: formData.permissions.join(','), + } + + const url = editingUser + ? `/api/v1/auth/user/update/${editingUser.id}` + : '/api/v1/auth/user/create' + + const res = await fetch(url, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }) + + if (!res.ok) { + const errorData = await res.json().catch(() => ({})) + throw new Error(errorData.message || `操作失败: HTTP ${res.status}`) + } + + await fetchUsers() + handleCloseDialog() + } catch (err: any) { + setError(err.message) + } + } + + const handleDelete = async (userId: number) => { + if (!window.confirm('确定要删除该用户吗?')) return + + try { + const res = await fetch(`/api/v1/auth/user/delete/${userId}`, { + method: 'DELETE', + }) + + if (!res.ok) throw new Error(`HTTP ${res.status}`) + await fetchUsers() + } catch (err: any) { + setError(err.message) + } + } + + const handlePermissionToggle = (permission: string) => { + setFormData((prev) => ({ + ...prev, + permissions: prev.permissions.includes(permission) + ? prev.permissions.filter((p) => p !== permission) + : [...prev.permissions, permission], + })) + } + + if (loading) { + return ( + + + + ) + } + + return ( + + + + 用户管理 + + + + + {error && ( + setError(null)}> + {error} + + )} + + + + + + 用户名 + 昵称 + 邮箱 + 角色 + 状态 + 权限 + 创建时间 + 操作 + + + + {users.map((user) => ( + + {user.username} + {user.nickname} + {user.email} + + + + + + + + {user.permissions?.split(',').map((p) => ( + + ))} + + {new Date(user.created_at).toLocaleString()} + + handleOpenDialog(user)}> + + + handleDelete(user.id)}> + + + + + ))} + +
+
+ + + {editingUser ? '编辑用户' : '新建用户'} + + setFormData({ ...formData, username: e.target.value })} + disabled={!!editingUser} + /> + {!editingUser && ( + setFormData({ ...formData, password: e.target.value })} + /> + )} + setFormData({ ...formData, email: e.target.value })} + /> + setFormData({ ...formData, nickname: e.target.value })} + /> + + 角色 + + + + 状态 + + + + 权限 + + + {PERMISSION_OPTIONS.map((perm) => ( + handlePermissionToggle(perm.value)} + /> + } + label={perm.label} + /> + ))} + + + + + + + +
+ ) +} diff --git a/frontend/src/stores/authStore.ts b/frontend/src/stores/authStore.ts new file mode 100644 index 0000000..0f94be6 --- /dev/null +++ b/frontend/src/stores/authStore.ts @@ -0,0 +1,77 @@ +import { create } from 'zustand' +import { persist } from 'zustand/middleware' + +interface AuthState { + token: string | null + username: string | null + loading: boolean + error: string | null + isAuthenticated: boolean + login: (username: string, password: string) => Promise + logout: () => void +} + +export const useAuthStore = create()( + persist( + (set) => ({ + token: null, + username: null, + loading: false, + error: null, + isAuthenticated: false, + + login: async (username: string, password: string) => { + set({ loading: true, error: null }) + try { + const res = await fetch('/api/v1/auth/login', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ username, password }), + }) + + if (!res.ok) { + const errorData = await res.json().catch(() => ({})) + throw new Error(errorData.message || `登录失败: HTTP ${res.status}`) + } + + const result = await res.json() + const { token } = result.data + + set({ + token, + username, + isAuthenticated: true, + loading: false, + error: null, + }) + } catch (error: any) { + set({ + loading: false, + error: error.message || '登录失败', + isAuthenticated: false, + }) + throw error + } + }, + + logout: () => { + set({ + token: null, + username: null, + isAuthenticated: false, + error: null, + }) + }, + }), + { + name: 'auth-storage', + partialize: (state) => ({ + token: state.token, + username: state.username, + isAuthenticated: state.isAuthenticated, + }), + } + ) +) diff --git a/go.mod b/go.mod index f7013c4..94c1c3d 100644 --- a/go.mod +++ b/go.mod @@ -33,9 +33,11 @@ require ( github.com/go-openapi/jsonpointer v0.21.0 // indirect github.com/go-openapi/jsonreference v0.20.2 // indirect github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-resty/resty/v2 v2.16.5 // indirect github.com/gofiber/schema v1.6.0 // indirect github.com/gofiber/utils/v2 v2.0.0-rc.1 // indirect github.com/gogo/protobuf v1.3.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/gnostic-models v0.7.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect diff --git a/go.sum b/go.sum index 0b44497..6c8603c 100644 --- a/go.sum +++ b/go.sum @@ -33,6 +33,8 @@ github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= +github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/gofiber/fiber/v3 v3.0.0-rc.2 h1:5I3RQ7XygDBfWRlMhkATjyJKupMmfMAVmnsrgo6wmc0= @@ -45,6 +47,8 @@ github.com/gofrs/uuid v4.4.0+incompatible h1:3qXRTX8/NbyulANqlc0lchS1gqAVxRgsuW1 github.com/gofrs/uuid v4.4.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= diff --git a/internal/api/api.go b/internal/api/api.go index 9ac6ce2..e7f6797 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -7,9 +7,10 @@ import ( "net" "gitea.loveuer.com/loveuer/cluster/internal/middleware" - "gitea.loveuer.com/loveuer/cluster/internal/model" + "gitea.loveuer.com/loveuer/cluster/internal/module/auth" "gitea.loveuer.com/loveuer/cluster/internal/module/k8s" "gitea.loveuer.com/loveuer/cluster/internal/module/registry" + registry_model "gitea.loveuer.com/loveuer/cluster/pkg/model/registry" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" "gorm.io/gorm" @@ -33,10 +34,15 @@ func Init(ctx context.Context, address string, db *gorm.DB, store store.Store) ( // Ensure database migration for RegistryConfig // This is done here to ensure the table exists before config APIs are called - if err := db.AutoMigrate(&model.RegistryConfig{}); err != nil { + if err := db.AutoMigrate(®istry_model.RegistryConfig{}); err != nil { log.Printf("Warning: failed to migrate RegistryConfig: %v", err) } + // Initialize auth module + if err := auth.Init(ctx, db); err != nil { + log.Printf("Warning: failed to initialize auth module: %v", err) + } + // Initialize k8s module if err := k8s.Init(ctx, db, store); err != nil { log.Printf("Warning: failed to initialize k8s module: %v", err) @@ -87,6 +93,20 @@ func Init(ctx context.Context, address string, db *gorm.DB, store store.Store) ( k8sAPI.Delete("/service/delete", k8s.K8sServiceDelete(ctx, db, store)) } + // auth apis + { + authAPI := app.Group("/api/v1/auth") + authAPI.Post("/login", auth.Login(ctx, db)) + authAPI.Get("/wallpaper", auth.Wallpaper(ctx)) + authAPI.Get("/current", auth.GetCurrentUser(ctx)) + + // user management + authAPI.Get("/user/list", auth.UserList(ctx, db)) + authAPI.Post("/user/create", auth.UserCreate(ctx, db)) + authAPI.Post("/user/update/:id", auth.UserUpdate(ctx, db)) + authAPI.Delete("/user/delete/:id", auth.UserDelete(ctx, db)) + } + ln, err = net.Listen("tcp", address) if err != nil { return fn, fmt.Errorf("failed to listen on %s: %w", address, err) diff --git a/internal/model/model.go b/internal/model/model.go deleted file mode 100644 index aaab619..0000000 --- a/internal/model/model.go +++ /dev/null @@ -1,92 +0,0 @@ -package model - -import ( - "time" - - "gorm.io/gorm" -) - -// Repository ???? -type Repository struct { - ID uint `gorm:"primarykey" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - - Name string `gorm:"uniqueIndex;not null" json:"name"` // ?????? "library/nginx" -} - -// Blob blob ?? -type Blob struct { - ID uint `gorm:"primarykey" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - - Digest string `gorm:"uniqueIndex;not null" json:"digest"` // SHA256 digest - Size int64 `gorm:"not null" json:"size"` // ?????? - MediaType string `json:"media_type"` // ???? - Repository string `gorm:"index" json:"repository"` // ??????????????? -} - -// Manifest manifest ?? -type Manifest struct { - ID uint `gorm:"primarykey" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - - Repository string `gorm:"index;not null" json:"repository"` // ???? - Tag string `gorm:"index;not null" json:"tag"` // tag ?? - Digest string `gorm:"uniqueIndex;not null" json:"digest"` // manifest digest - MediaType string `json:"media_type"` // ???? - Size int64 `gorm:"not null" json:"size"` // manifest ?? - Content []byte `gorm:"type:blob" json:"-"` // manifest ???JSON? -} - -// Tag tag ?????????? -type Tag struct { - ID uint `gorm:"primarykey" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - - Repository string `gorm:"index;not null" json:"repository"` // ???? - Tag string `gorm:"index;not null" json:"tag"` // tag ?? - Digest string `gorm:"not null" json:"digest"` // ??? manifest digest -} - -// BlobUpload ????? blob ?? -type BlobUpload struct { - ID uint `gorm:"primarykey" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - - UUID string `gorm:"uniqueIndex;not null" json:"uuid"` // ???? UUID - Repository string `gorm:"index;not null" json:"repository"` // ???? - Path string `gorm:"not null" json:"path"` // ?????? - Size int64 `gorm:"default:0" json:"size"` // ????? -} - -// RegistryConfig registry ????? -type RegistryConfig struct { - ID uint `gorm:"primarykey" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - - Key string `gorm:"uniqueIndex;not null" json:"key"` // ???? key - Value string `gorm:"type:text" json:"value"` // ???? value -} - -// ClusterConfig k8s cluster configuration -type ClusterConfig struct { - ID uint `gorm:"primarykey" json:"id"` - CreatedAt time.Time `json:"created_at"` - UpdatedAt time.Time `json:"updated_at"` - DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - - Key string `gorm:"uniqueIndex;not null" json:"key"` - Value string `gorm:"type:text" json:"value"` -} diff --git a/internal/module/auth/auth.go b/internal/module/auth/auth.go new file mode 100644 index 0000000..18e2fc5 --- /dev/null +++ b/internal/module/auth/auth.go @@ -0,0 +1,41 @@ +package auth + +import ( + "context" + "log" + + auth "gitea.loveuer.com/loveuer/cluster/pkg/model/auth" + "gitea.loveuer.com/loveuer/cluster/pkg/tool" + "gorm.io/gorm" +) + +func Init(ctx context.Context, db *gorm.DB) error { + if err := db.AutoMigrate(&auth.User{}); err != nil { + return err + } + + var count int64 + if err := db.Model(&auth.User{}).Count(&count).Error; err != nil { + return err + } + + if count == 0 { + defaultAdmin := &auth.User{ + Username: "admin", + Password: tool.NewPassword("cluster"), + Email: "admin@cluster.local", + Nickname: "Administrator", + Role: "admin", + Status: "active", + Permissions: "registry_read,registry_write,cluster_read,cluster_write", + } + + if err := db.Create(defaultAdmin).Error; err != nil { + return err + } + + log.Println("[Auth] Default admin user created: username=admin, password=cluster") + } + + return nil +} diff --git a/internal/module/auth/handler.current.go b/internal/module/auth/handler.current.go new file mode 100644 index 0000000..db6dc27 --- /dev/null +++ b/internal/module/auth/handler.current.go @@ -0,0 +1,42 @@ +package auth + +import ( + "context" + + "gitea.loveuer.com/loveuer/cluster/pkg/resp" + "github.com/gofiber/fiber/v3" + "github.com/golang-jwt/jwt/v5" +) + +func GetCurrentUser(ctx context.Context) fiber.Handler { + return func(c fiber.Ctx) error { + authHeader := c.Get("Authorization") + if authHeader == "" { + return resp.R401(c, "MISSING_TOKEN", nil, "authorization token is required") + } + + tokenString := authHeader + if len(authHeader) > 7 && authHeader[:7] == "Bearer " { + tokenString = authHeader[7:] + } + + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(JWTSecret), nil + }) + + if err != nil || !token.Valid { + return resp.R401(c, "INVALID_TOKEN", nil, "invalid or expired token") + } + + claims, ok := token.Claims.(*Claims) + if !ok { + return resp.R401(c, "INVALID_CLAIMS", nil, "invalid token claims") + } + + return resp.R200(c, map[string]interface{}{ + "user_id": claims.UserID, + "username": claims.Username, + "role": claims.Role, + }) + } +} diff --git a/internal/module/auth/handler.login.go b/internal/module/auth/handler.login.go new file mode 100644 index 0000000..542a6e1 --- /dev/null +++ b/internal/module/auth/handler.login.go @@ -0,0 +1,97 @@ +package auth + +import ( + "context" + "encoding/json" + "time" + + authModel "gitea.loveuer.com/loveuer/cluster/pkg/model/auth" + "gitea.loveuer.com/loveuer/cluster/pkg/resp" + "gitea.loveuer.com/loveuer/cluster/pkg/tool" + "github.com/gofiber/fiber/v3" + "github.com/golang-jwt/jwt/v5" + "gorm.io/gorm" +) + +const ( + JWTSecret = "cluster-secret-key-change-in-production" + TokenDuration = 7 * 24 * time.Hour +) + +type LoginRequest struct { + Username string `json:"username"` + Password string `json:"password"` +} + +type LoginResponse struct { + Token string `json:"token"` + Username string `json:"username"` + Nickname string `json:"nickname"` + Role string `json:"role"` +} + +type Claims struct { + UserID uint `json:"user_id"` + Username string `json:"username"` + Role string `json:"role"` + jwt.RegisteredClaims +} + +func Login(ctx context.Context, db *gorm.DB) fiber.Handler { + return func(c fiber.Ctx) error { + var req LoginRequest + + body := c.Body() + if len(body) == 0 { + return resp.R400(c, "EMPTY_BODY", nil, "request body is empty") + } + + if err := json.Unmarshal(body, &req); err != nil { + return resp.R400(c, "INVALID_REQUEST", nil, "invalid request body") + } + + if req.Username == "" || req.Password == "" { + return resp.R400(c, "MISSING_CREDENTIALS", nil, "username and password are required") + } + + var user authModel.User + if err := db.Where("username = ?", req.Username).First(&user).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return resp.R401(c, "INVALID_CREDENTIALS", nil, "invalid username or password") + } + return resp.R500(c, "", nil, err.Error()) + } + + if user.Status != "active" { + return resp.R403(c, "USER_INACTIVE", nil, "user account is inactive") + } + + if !tool.ComparePassword(req.Password, user.Password) { + return resp.R401(c, "INVALID_CREDENTIALS", nil, "invalid username or password") + } + + claims := &Claims{ + UserID: user.ID, + Username: user.Username, + Role: user.Role, + RegisteredClaims: jwt.RegisteredClaims{ + ExpiresAt: jwt.NewNumericDate(time.Now().Add(TokenDuration)), + IssuedAt: jwt.NewNumericDate(time.Now()), + NotBefore: jwt.NewNumericDate(time.Now()), + }, + } + + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, err := token.SignedString([]byte(JWTSecret)) + if err != nil { + return resp.R500(c, "", nil, "failed to generate token") + } + + return resp.R200(c, LoginResponse{ + Token: tokenString, + Username: user.Username, + Nickname: user.Nickname, + Role: user.Role, + }) + } +} diff --git a/internal/module/auth/handler.user.go b/internal/module/auth/handler.user.go new file mode 100644 index 0000000..9f09a6c --- /dev/null +++ b/internal/module/auth/handler.user.go @@ -0,0 +1,207 @@ +package auth + +import ( + "context" + "encoding/json" + "strconv" + + auth "gitea.loveuer.com/loveuer/cluster/pkg/model/auth" + "gitea.loveuer.com/loveuer/cluster/pkg/resp" + "gitea.loveuer.com/loveuer/cluster/pkg/tool" + "github.com/gofiber/fiber/v3" + "github.com/golang-jwt/jwt/v5" + "gorm.io/gorm" +) + +func UserList(ctx context.Context, db *gorm.DB) fiber.Handler { + return func(c fiber.Ctx) error { + var users []auth.User + if err := db.Find(&users).Error; err != nil { + return resp.R500(c, "", nil, err.Error()) + } + + return resp.R200(c, map[string]interface{}{ + "users": users, + }) + } +} + +func UserCreate(ctx context.Context, db *gorm.DB) fiber.Handler { + return func(c fiber.Ctx) error { + var req struct { + Username string `json:"username"` + Password string `json:"password"` + Email string `json:"email"` + Nickname string `json:"nickname"` + Role string `json:"role"` + Status string `json:"status"` + Permissions string `json:"permissions"` + } + + body := c.Body() + if len(body) == 0 { + return resp.R400(c, "EMPTY_BODY", nil, "request body is empty") + } + + if err := json.Unmarshal(body, &req); err != nil { + return resp.R400(c, "INVALID_REQUEST", nil, "invalid request body") + } + + if req.Username == "" || req.Password == "" { + return resp.R400(c, "MISSING_FIELDS", nil, "username and password are required") + } + + if err := tool.CheckPassword(req.Password); err != nil { + return resp.R400(c, "WEAK_PASSWORD", nil, err.Error()) + } + + var existing auth.User + if err := db.Unscoped().Where("username = ?", req.Username).First(&existing).Error; err == nil { + return resp.R400(c, "USER_EXISTS", nil, "username already exists") + } + + user := &auth.User{ + Username: req.Username, + Password: tool.NewPassword(req.Password), + Email: req.Email, + Nickname: req.Nickname, + Role: req.Role, + Status: req.Status, + Permissions: req.Permissions, + } + + if user.Role == "" { + user.Role = "user" + } + if user.Status == "" { + user.Status = "active" + } + + if err := db.Create(user).Error; err != nil { + return resp.R500(c, "", nil, err.Error()) + } + + return resp.R200(c, map[string]interface{}{ + "user": user, + }) + } +} + +func UserUpdate(ctx context.Context, db *gorm.DB) fiber.Handler { + return func(c fiber.Ctx) error { + userID := c.Params("id") + if userID == "" { + return resp.R400(c, "MISSING_ID", nil, "user id is required") + } + + authHeader := c.Get("Authorization") + var currentUserID uint + if authHeader != "" { + tokenString := authHeader + if len(authHeader) > 7 && authHeader[:7] == "Bearer " { + tokenString = authHeader[7:] + } + + token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) { + return []byte(JWTSecret), nil + }) + + if err == nil && token.Valid { + if claims, ok := token.Claims.(*Claims); ok { + currentUserID = claims.UserID + } + } + } + + targetUserID, _ := strconv.ParseUint(userID, 10, 32) + isSelf := currentUserID == uint(targetUserID) + + var req struct { + Email string `json:"email"` + Nickname string `json:"nickname"` + Password string `json:"password"` + Role string `json:"role"` + Status string `json:"status"` + Permissions string `json:"permissions"` + } + + body := c.Body() + if len(body) == 0 { + return resp.R400(c, "EMPTY_BODY", nil, "request body is empty") + } + + if err := json.Unmarshal(body, &req); err != nil { + return resp.R400(c, "INVALID_REQUEST", nil, "invalid request body") + } + + var user auth.User + if err := db.First(&user, userID).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return resp.R404(c, "USER_NOT_FOUND", nil, "user not found") + } + return resp.R500(c, "", nil, err.Error()) + } + + user.Email = req.Email + user.Nickname = req.Nickname + + if req.Password != "" { + if err := tool.CheckPassword(req.Password); err != nil { + return resp.R400(c, "WEAK_PASSWORD", nil, err.Error()) + } + user.Password = tool.NewPassword(req.Password) + } + + if !isSelf { + user.Role = req.Role + user.Status = req.Status + user.Permissions = req.Permissions + } + + if err := db.Save(&user).Error; err != nil { + return resp.R500(c, "", nil, err.Error()) + } + + return resp.R200(c, map[string]interface{}{ + "user": user, + }) + } +} + +func UserDelete(ctx context.Context, db *gorm.DB) fiber.Handler { + return func(c fiber.Ctx) error { + userID := c.Params("id") + if userID == "" { + return resp.R400(c, "MISSING_ID", nil, "user id is required") + } + + var user auth.User + if err := db.First(&user, userID).Error; err != nil { + if err == gorm.ErrRecordNotFound { + return resp.R404(c, "USER_NOT_FOUND", nil, "user not found") + } + return resp.R500(c, "", nil, err.Error()) + } + + if user.Username == "admin" { + return resp.R403(c, "CANNOT_DELETE_ADMIN", nil, "cannot delete admin user") + } + + var count int64 + if err := db.Model(&auth.User{}).Count(&count).Error; err != nil { + return resp.R500(c, "", nil, err.Error()) + } + + if count <= 1 { + return resp.R403(c, "LAST_USER", nil, "cannot delete the last user") + } + + if err := db.Delete(&user).Error; err != nil { + return resp.R500(c, "", nil, err.Error()) + } + + return resp.R200(c, map[string]interface{}{ + "message": "user deleted successfully", + }) + } +} diff --git a/internal/module/auth/wallpaper.go b/internal/module/auth/wallpaper.go new file mode 100644 index 0000000..89698be --- /dev/null +++ b/internal/module/auth/wallpaper.go @@ -0,0 +1,108 @@ +package auth + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "strings" + "sync" + "time" + + "gitea.loveuer.com/loveuer/cluster/pkg/resp" + "github.com/go-resty/resty/v2" + "github.com/gofiber/fiber/v3" +) + +func Wallpaper(ctx context.Context) fiber.Handler { + type Result struct { + Images []struct { + Startdate string `json:"startdate"` + Fullstartdate string `json:"fullstartdate"` + Enddate string `json:"enddate"` + URL string `json:"url"` + Urlbase string `json:"urlbase"` + Copyright string `json:"copyright"` + Copyrightlink string `json:"copyrightlink"` + Title string `json:"title"` + Quiz string `json:"quiz"` + Wp bool `json:"wp"` + Hsh string `json:"hsh"` + Drk int `json:"drk"` + Top int `json:"top"` + Bot int `json:"bot"` + } `json:"images"` + } + + type Store struct { + sync.Mutex + Date string `json:"date"` + Body []byte `json:"body"` + Headers map[string]string `json:"headers"` + } + + client := resty.New().SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) + apiUrl := "https://cn.bing.com/HPImageArchive.aspx?format=js&idx=0&n=1" + imgUrlPrefix := "https://cn.bing.com" + store := &Store{} + + get := func() ([]byte, map[string]string, error) { + var ( + err error + rr *resty.Response + result = new(Result) + headers = make(map[string]string) + date = time.Now().Format("2006-01-02") + ) + + if store.Date == date { + return store.Body, store.Headers, nil + } + + if _, err = client.R(). + SetResult(result). + Get(apiUrl); err != nil { + return nil, nil, fmt.Errorf("[BingWallpaper] get %s err: %w", apiUrl, err) + } + + if len(result.Images) == 0 { + err = errors.New("[BingWallpaper]: image length = 0") + return nil, nil, err + } + + address := fmt.Sprintf("%s%s", imgUrlPrefix, result.Images[0].URL) + + if rr, err = client.R(). + Get(address); err != nil { + err = fmt.Errorf("[BingWallpaper] get image body: %s err: %w", address, err) + return nil, nil, err + } + + for key := range rr.Header() { + headers[key] = strings.Join(rr.Header()[key], ", ") + } + + store.Lock() + store.Date = date + store.Body = rr.Body() + store.Headers = headers + store.Unlock() + + return rr.Body(), headers, nil + } + + return func(c fiber.Ctx) error { + bs, headers, err := get() + if err != nil { + return resp.R500(c, "", nil, err.Error()) + } + + for key := range headers { + c.Set(key, headers[key]) + } + + _, err = c.Write(bs) + + return err + } +} diff --git a/internal/module/k8s/handler.config.go b/internal/module/k8s/handler.config.go index df7c6cf..3eeeb4a 100644 --- a/internal/module/k8s/handler.config.go +++ b/internal/module/k8s/handler.config.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" - "gitea.loveuer.com/loveuer/cluster/internal/model" + k8s "gitea.loveuer.com/loveuer/cluster/pkg/model/k8s" "gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" @@ -13,7 +13,7 @@ import ( func ClusterConfigGet(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { return func(c fiber.Ctx) error { - var config model.ClusterConfig + var config k8s.ClusterConfig if err := db.Where("key = ?", "kubeconfig").First(&config).Error; err != nil { if err == gorm.ErrRecordNotFound { @@ -40,7 +40,7 @@ func ClusterConfigSet(ctx context.Context, db *gorm.DB, store store.Store) fiber return resp.R400(c, "", nil, err) } - config := model.ClusterConfig{ + config := k8s.ClusterConfig{ Key: "kubeconfig", Value: req.Kubeconfig, } diff --git a/internal/module/k8s/handler.resource.go b/internal/module/k8s/handler.resource.go index 039f8eb..f28c52e 100644 --- a/internal/module/k8s/handler.resource.go +++ b/internal/module/k8s/handler.resource.go @@ -8,7 +8,7 @@ import ( "io" "strings" - "gitea.loveuer.com/loveuer/cluster/internal/model" + k8sModel "gitea.loveuer.com/loveuer/cluster/pkg/model/k8s" "gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" @@ -26,7 +26,7 @@ import ( ) func getK8sClient(db *gorm.DB) (*kubernetes.Clientset, error) { - var config model.ClusterConfig + var config k8sModel.ClusterConfig if err := db.Where("key = ?", "kubeconfig").First(&config).Error; err != nil { return nil, fmt.Errorf("kubeconfig not found: %w", err) @@ -52,7 +52,7 @@ func getK8sClient(db *gorm.DB) (*kubernetes.Clientset, error) { } func getK8sConfig(db *gorm.DB) (*rest.Config, error) { - var config model.ClusterConfig + var config k8sModel.ClusterConfig if err := db.Where("key = ?", "kubeconfig").First(&config).Error; err != nil { return nil, fmt.Errorf("kubeconfig not found: %w", err) @@ -428,7 +428,7 @@ func K8sResourceApply(ctx context.Context, db *gorm.DB, store store.Store) fiber return resp.R400(c, "", nil, fmt.Errorf("yaml content is empty")) } - var config model.ClusterConfig + var config k8sModel.ClusterConfig if err := db.Where("key = ?", "kubeconfig").First(&config).Error; err != nil { return resp.R500(c, "", nil, fmt.Errorf("kubeconfig not found: %w", err)) } @@ -636,7 +636,7 @@ func K8sResourceUpdate(ctx context.Context, db *gorm.DB, store store.Store) fibe return resp.R400(c, "", nil, fmt.Errorf("yaml content is empty")) } - var config model.ClusterConfig + var config k8sModel.ClusterConfig if err := db.Where("key = ?", "kubeconfig").First(&config).Error; err != nil { return resp.R500(c, "", nil, fmt.Errorf("kubeconfig not found: %w", err)) } diff --git a/internal/module/k8s/k8s.go b/internal/module/k8s/k8s.go index e600734..d8767f1 100644 --- a/internal/module/k8s/k8s.go +++ b/internal/module/k8s/k8s.go @@ -4,14 +4,14 @@ import ( "context" "log" - "gitea.loveuer.com/loveuer/cluster/internal/model" + k8s "gitea.loveuer.com/loveuer/cluster/pkg/model/k8s" "gitea.loveuer.com/loveuer/cluster/pkg/store" "gorm.io/gorm" ) func Init(ctx context.Context, db *gorm.DB, store store.Store) error { if err := db.AutoMigrate( - &model.ClusterConfig{}, + &k8s.ClusterConfig{}, ); err != nil { log.Fatalf("failed to migrate k8s database: %v", err) return err diff --git a/internal/module/registry/blob.go b/internal/module/registry/blob.go index 0525bcb..20b78ee 100644 --- a/internal/module/registry/blob.go +++ b/internal/module/registry/blob.go @@ -10,7 +10,7 @@ import ( "strconv" "strings" - "gitea.loveuer.com/loveuer/cluster/internal/model" + "gitea.loveuer.com/loveuer/cluster/pkg/model/registry" "gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" @@ -51,7 +51,7 @@ func HandleBlobs(c fiber.Ctx, db *gorm.DB, store store.Store) error { repo := strings.Join(parts[:blobsIndex], "/") // Strip registry_address prefix from repo if present - var registryConfig model.RegistryConfig + var registryConfig registry.RegistryConfig registryAddress := "" if err := db.Where("key = ?", "registry_address").First(®istryConfig).Error; err == nil { registryAddress = registryConfig.Value @@ -120,7 +120,7 @@ func handleBlobUploadStart(c fiber.Ctx, db *gorm.DB, store store.Store, repo str uuid := hex.EncodeToString(uuidBytes) // ?????? - upload := &model.BlobUpload{ + upload := ®istry.BlobUpload{ UUID: uuid, Repository: repo, Path: uuid, // ?? UUID ?????? @@ -150,7 +150,7 @@ func handleBlobUploadStart(c fiber.Ctx, db *gorm.DB, store store.Store, repo str // handleBlobUploadChunk ?? blob ??? func handleBlobUploadChunk(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, uuid string) error { // ?????? - var upload model.BlobUpload + var upload registry.BlobUpload if err := db.Where("uuid = ? AND repository = ?", uuid, repo).First(&upload).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "UPLOAD_NOT_FOUND", nil, "upload session not found") @@ -187,7 +187,7 @@ func handleBlobUploadChunk(c fiber.Ctx, db *gorm.DB, store store.Store, repo str // handleBlobUploadComplete ?? blob ?? func handleBlobUploadComplete(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, uuid string, digest string) error { // ?????? - var upload model.BlobUpload + var upload registry.BlobUpload if err := db.Where("uuid = ? AND repository = ?", uuid, repo).First(&upload).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "UPLOAD_NOT_FOUND", nil, "upload session not found") @@ -215,10 +215,10 @@ func handleBlobUploadComplete(c fiber.Ctx, db *gorm.DB, store store.Store, repo } // ????? blob ?? - var blob model.Blob + var blob registry.Blob if err := db.Where("digest = ?", digest).First(&blob).Error; err != nil { if err == gorm.ErrRecordNotFound { - blob = model.Blob{ + blob = registry.Blob{ Digest: digest, Size: size, Repository: repo, diff --git a/internal/module/registry/catalog.go b/internal/module/registry/catalog.go index 016ac64..c9e12f8 100644 --- a/internal/module/registry/catalog.go +++ b/internal/module/registry/catalog.go @@ -4,7 +4,7 @@ import ( "strconv" "strings" - "gitea.loveuer.com/loveuer/cluster/internal/model" + "gitea.loveuer.com/loveuer/cluster/pkg/model/registry" "gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" @@ -31,7 +31,7 @@ func HandleCatalog(c fiber.Ctx, db *gorm.DB, store store.Store) error { last := c.Query("last") // ???? - var repos []model.Repository + var repos []registry.Repository query := db.Order("name ASC").Limit(n + 1) if last != "" { diff --git a/internal/module/registry/handler.config.go b/internal/module/registry/handler.config.go index 96baaf2..7d02b35 100644 --- a/internal/module/registry/handler.config.go +++ b/internal/module/registry/handler.config.go @@ -4,7 +4,7 @@ import ( "context" "encoding/json" - "gitea.loveuer.com/loveuer/cluster/internal/model" + "gitea.loveuer.com/loveuer/cluster/pkg/model/registry" "gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" @@ -14,7 +14,7 @@ import ( // RegistryConfigGet returns the registry configuration func RegistryConfigGet(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { return func(c fiber.Ctx) error { - var configs []model.RegistryConfig + var configs []registry.RegistryConfig if err := db.Find(&configs).Error; err != nil { return resp.R500(c, "", nil, err) } @@ -53,11 +53,11 @@ func RegistryConfigSet(ctx context.Context, db *gorm.DB, store store.Store) fibe } // Find or create config - var config model.RegistryConfig + var config registry.RegistryConfig err := db.Where("key = ?", req.Key).First(&config).Error if err == gorm.ErrRecordNotFound { // Create new config - config = model.RegistryConfig{ + config = registry.RegistryConfig{ Key: req.Key, Value: req.Value, } diff --git a/internal/module/registry/handler.download.go b/internal/module/registry/handler.download.go index 956b34b..13ad230 100644 --- a/internal/module/registry/handler.download.go +++ b/internal/module/registry/handler.download.go @@ -12,7 +12,7 @@ import ( "strings" "time" - "gitea.loveuer.com/loveuer/cluster/internal/model" + "gitea.loveuer.com/loveuer/cluster/pkg/model/registry" "gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" @@ -39,7 +39,7 @@ func RegistryImageDownload(ctx context.Context, db *gorm.DB, store store.Store) log.Printf("[Download] Start downloading: %s", fullImageName) // Get current registry_address to strip it from the request - var registryConfig model.RegistryConfig + var registryConfig registry.RegistryConfig registryAddress := "" if err := db.Where("key = ?", "registry_address").First(®istryConfig).Error; err == nil { registryAddress = registryConfig.Value @@ -71,7 +71,7 @@ func RegistryImageDownload(ctx context.Context, db *gorm.DB, store store.Store) // Find the repository t1 := time.Now() - var repositoryModel model.Repository + var repositoryModel registry.Repository if err := db.Where("name = ?", repository).First(&repositoryModel).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "IMAGE_NOT_FOUND", nil, fmt.Sprintf("image %s not found", repository)) @@ -82,7 +82,7 @@ func RegistryImageDownload(ctx context.Context, db *gorm.DB, store store.Store) // Find the tag record t2 := time.Now() - var tagRecord model.Tag + var tagRecord registry.Tag if err := db.Where("repository = ? AND tag = ?", repository, tag).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { // Try to get the first available tag @@ -102,7 +102,7 @@ func RegistryImageDownload(ctx context.Context, db *gorm.DB, store store.Store) // Get the manifest t3 := time.Now() - var manifest model.Manifest + var manifest registry.Manifest if err := db.Where("digest = ?", tagRecord.Digest).First(&manifest).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found") diff --git a/internal/module/registry/handler.fetch.go b/internal/module/registry/handler.fetch.go index 1975ff0..a422d00 100644 --- a/internal/module/registry/handler.fetch.go +++ b/internal/module/registry/handler.fetch.go @@ -14,7 +14,7 @@ import ( "strings" "time" - "gitea.loveuer.com/loveuer/cluster/internal/model" + "gitea.loveuer.com/loveuer/cluster/pkg/model/registry" "gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" @@ -161,7 +161,7 @@ func pullImage(ctx context.Context, db *gorm.DB, store store.Store, repo string, log.Printf("[PullImage] Got manifest with %d layers", len(manifest.Layers)) // Create repository - if err := db.FirstOrCreate(&model.Repository{}, model.Repository{Name: repo}).Error; err != nil { + if err := db.FirstOrCreate(®istry.Repository{}, registry.Repository{Name: repo}).Error; err != nil { return nil, fmt.Errorf("failed to create repository: %w", err) } @@ -195,7 +195,7 @@ func pullImage(ctx context.Context, db *gorm.DB, store store.Store, repo string, return nil, fmt.Errorf("failed to write config blob: %w", err) } - if err := db.Create(&model.Blob{ + if err := db.Create(®istry.Blob{ Digest: digest, Size: manifest.Config.Size, MediaType: "application/vnd.docker.container.image.v1+json", @@ -229,7 +229,7 @@ func pullImage(ctx context.Context, db *gorm.DB, store store.Store, repo string, return nil, fmt.Errorf("failed to write layer blob %d: %w", idx, err) } - if err := db.Create(&model.Blob{ + if err := db.Create(®istry.Blob{ Digest: layerDigest, Size: layerDesc.Size, MediaType: string(layerDesc.MediaType), @@ -275,7 +275,7 @@ func pullImage(ctx context.Context, db *gorm.DB, store store.Store, repo string, return nil, fmt.Errorf("failed to write manifest: %w", err) } - if err := db.Create(&model.Manifest{ + if err := db.Create(®istry.Manifest{ Repository: repo, Tag: tag, Digest: manifestDigest, @@ -286,7 +286,7 @@ func pullImage(ctx context.Context, db *gorm.DB, store store.Store, repo string, return nil, fmt.Errorf("failed to save manifest: %w", err) } - if err := db.Create(&model.Tag{ + if err := db.Create(®istry.Tag{ Repository: repo, Tag: tag, Digest: manifestDigest, diff --git a/internal/module/registry/handler.list.go b/internal/module/registry/handler.list.go index 00a9fb4..2a9e647 100644 --- a/internal/module/registry/handler.list.go +++ b/internal/module/registry/handler.list.go @@ -3,7 +3,7 @@ package registry import ( "context" - "gitea.loveuer.com/loveuer/cluster/internal/model" + "gitea.loveuer.com/loveuer/cluster/pkg/model/registry" "gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" @@ -17,7 +17,7 @@ func RegistryImageList(ctx context.Context, db *gorm.DB, store store.Store) fibe filter := c.Query("filter", "") // Get current registry_address setting - var registryConfig model.RegistryConfig + var registryConfig registry.RegistryConfig registryAddress := "" if err := db.Where("key = ?", "registry_address").First(®istryConfig).Error; err == nil { registryAddress = registryConfig.Value @@ -26,7 +26,7 @@ func RegistryImageList(ctx context.Context, db *gorm.DB, store store.Store) fibe registryAddress = "localhost:9119" } - var repositories []model.Repository + var repositories []registry.Repository // Query all repositories from the database query := db @@ -41,7 +41,7 @@ func RegistryImageList(ctx context.Context, db *gorm.DB, store store.Store) fibe var result []map[string]interface{} for _, repo := range repositories { // Get all tags for this repository - var tags []model.Tag + var tags []registry.Tag if err := db.Where("repository = ?", repo.Name).Find(&tags).Error; err != nil { continue // Skip this repository if we can't get tags } @@ -56,7 +56,7 @@ func RegistryImageList(ctx context.Context, db *gorm.DB, store store.Store) fibe var sizeResult struct { Total int64 } - err := db.Model(&model.Blob{}). + err := db.Model(®istry.Blob{}). Where("repository = ?", repo.Name). Select("COALESCE(SUM(size), 0) as total"). Scan(&sizeResult).Error diff --git a/internal/module/registry/handler.upload.go b/internal/module/registry/handler.upload.go index e803171..eae3a5b 100644 --- a/internal/module/registry/handler.upload.go +++ b/internal/module/registry/handler.upload.go @@ -10,7 +10,7 @@ import ( "io" "strings" - "gitea.loveuer.com/loveuer/cluster/internal/model" + "gitea.loveuer.com/loveuer/cluster/pkg/model/registry" "gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" @@ -142,7 +142,7 @@ func RegistryImageUpload(ctx context.Context, db *gorm.DB, store store.Store) fi // This allows registry_address to be changed without breaking existing images repoName := originalRepo - if err := db.FirstOrCreate(&model.Repository{}, model.Repository{Name: repoName}).Error; err != nil { + if err := db.FirstOrCreate(®istry.Repository{}, registry.Repository{Name: repoName}).Error; err != nil { return resp.R500(c, "", nil, fmt.Errorf("failed to create repository: %w", err)) } @@ -155,7 +155,7 @@ func RegistryImageUpload(ctx context.Context, db *gorm.DB, store store.Store) fi return resp.R500(c, "", nil, fmt.Errorf("failed to write config blob: %w", err)) } - if err := db.Create(&model.Blob{ + if err := db.Create(®istry.Blob{ Digest: configDigest, Size: int64(len(configContent)), MediaType: "application/vnd.docker.container.image.v1+json", @@ -179,7 +179,7 @@ func RegistryImageUpload(ctx context.Context, db *gorm.DB, store store.Store) fi return resp.R500(c, "", nil, fmt.Errorf("failed to write layer blob: %w", err)) } - if err := db.Create(&model.Blob{ + if err := db.Create(®istry.Blob{ Digest: digest, Size: int64(len(content)), MediaType: "application/vnd.docker.image.rootfs.diff.tar.gzip", @@ -218,7 +218,7 @@ func RegistryImageUpload(ctx context.Context, db *gorm.DB, store store.Store) fi return resp.R500(c, "", nil, fmt.Errorf("failed to write manifest: %w", err)) } - if err := db.Create(&model.Manifest{ + if err := db.Create(®istry.Manifest{ Repository: repoName, Tag: tag, Digest: manifestDigest, @@ -229,7 +229,7 @@ func RegistryImageUpload(ctx context.Context, db *gorm.DB, store store.Store) fi return resp.R500(c, "", nil, fmt.Errorf("failed to save manifest: %w", err)) } - if err := db.Create(&model.Tag{ + if err := db.Create(®istry.Tag{ Repository: repoName, Tag: tag, Digest: manifestDigest, @@ -277,7 +277,7 @@ func handleOCIFormat(c fiber.Ctx, db *gorm.DB, store store.Store, ociIndex *OCII } // Create repository - if err := db.FirstOrCreate(&model.Repository{}, model.Repository{Name: repoName}).Error; err != nil { + if err := db.FirstOrCreate(®istry.Repository{}, registry.Repository{Name: repoName}).Error; err != nil { return resp.R500(c, "", nil, fmt.Errorf("failed to create repository: %w", err)) } @@ -291,7 +291,7 @@ func handleOCIFormat(c fiber.Ctx, db *gorm.DB, store store.Store, ociIndex *OCII if err := store.WriteBlob(c.Context(), indexDigest, strings.NewReader(string(indexJSON))); err != nil { return resp.R500(c, "", nil, fmt.Errorf("failed to write index blob: %w", err)) } - if err := db.Create(&model.Blob{ + if err := db.Create(®istry.Blob{ Digest: indexDigest, Size: int64(len(indexJSON)), MediaType: "application/vnd.oci.image.index.v1+json", @@ -323,7 +323,7 @@ func handleOCIFormat(c fiber.Ctx, db *gorm.DB, store store.Store, ociIndex *OCII if err := store.WriteBlob(c.Context(), ociManifest.Config.Digest, strings.NewReader(string(configContent))); err != nil { return resp.R500(c, "", nil, fmt.Errorf("failed to write config blob: %w", err)) } - if err := db.Create(&model.Blob{ + if err := db.Create(®istry.Blob{ Digest: ociManifest.Config.Digest, Size: ociManifest.Config.Size, MediaType: ociManifest.Config.MediaType, @@ -343,7 +343,7 @@ func handleOCIFormat(c fiber.Ctx, db *gorm.DB, store store.Store, ociIndex *OCII if err := store.WriteBlob(c.Context(), layer.Digest, strings.NewReader(string(layerContent))); err != nil { return resp.R500(c, "", nil, fmt.Errorf("failed to write layer blob: %w", err)) } - if err := db.Create(&model.Blob{ + if err := db.Create(®istry.Blob{ Digest: layer.Digest, Size: layer.Size, MediaType: layer.MediaType, @@ -387,7 +387,7 @@ func handleOCIFormat(c fiber.Ctx, db *gorm.DB, store store.Store, ociIndex *OCII return resp.R500(c, "", nil, fmt.Errorf("failed to write manifest: %w", err)) } - if err := db.Create(&model.Manifest{ + if err := db.Create(®istry.Manifest{ Repository: repoName, Tag: tag, Digest: manifestDigest, @@ -398,7 +398,7 @@ func handleOCIFormat(c fiber.Ctx, db *gorm.DB, store store.Store, ociIndex *OCII return resp.R500(c, "", nil, fmt.Errorf("failed to save manifest: %w", err)) } - if err := db.Create(&model.Tag{ + if err := db.Create(®istry.Tag{ Repository: repoName, Tag: tag, Digest: manifestDigest, diff --git a/internal/module/registry/manifest.go b/internal/module/registry/manifest.go index 5b13ba7..f524ac9 100644 --- a/internal/module/registry/manifest.go +++ b/internal/module/registry/manifest.go @@ -7,7 +7,7 @@ import ( "fmt" "strings" - "gitea.loveuer.com/loveuer/cluster/internal/model" + "gitea.loveuer.com/loveuer/cluster/pkg/model/registry" "gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" @@ -20,28 +20,28 @@ func isDigestFormat(s string) bool { if len(parts) != 2 { return false } - + algo := parts[0] hash := parts[1] - + // Check algorithm if algo != "sha256" { // Could be extended to support other algorithms like sha512 return false } - + // Check that hash is a valid hex string of expected length (64 for sha256) if len(hash) != 64 { return false } - + // Verify it's all hex characters for _, r := range hash { if !((r >= '0' && r <= '9') || (r >= 'a' && r <= 'f')) { return false } } - + return true } @@ -75,9 +75,9 @@ func HandleManifest(c fiber.Ctx, db *gorm.DB, store store.Store) error { // ???? manifests ??????? repo := strings.Join(parts[:manifestsIndex], "/") - + // Strip registry_address prefix from repo if present - var registryConfig model.RegistryConfig + var registryConfig registry.RegistryConfig registryAddress := "" if err := db.Where("key = ?", "registry_address").First(®istryConfig).Error; err == nil { registryAddress = registryConfig.Value @@ -85,7 +85,7 @@ func HandleManifest(c fiber.Ctx, db *gorm.DB, store store.Store) error { if registryAddress != "" && strings.HasPrefix(repo, registryAddress+"/") { repo = strings.TrimPrefix(repo, registryAddress+"/") } - + // tag ? manifests ????? tag := parts[manifestsIndex+1] @@ -140,10 +140,10 @@ func handleManifestPut(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, } // ?????? - var repository model.Repository + var repository registry.Repository if err := db.Where("name = ?", repo).First(&repository).Error; err != nil { if err == gorm.ErrRecordNotFound { - repository = model.Repository{Name: repo} + repository = registry.Repository{Name: repo} if err := db.Create(&repository).Error; err != nil { return resp.R500(c, "", nil, err) } @@ -158,11 +158,11 @@ func handleManifestPut(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, } // ?? manifest ????? - var manifest model.Manifest + var manifest registry.Manifest if err := db.Where("digest = ?", digest).First(&manifest).Error; err != nil { if err == gorm.ErrRecordNotFound { // ???? manifest ?? - manifest = model.Manifest{ + manifest = registry.Manifest{ Repository: repo, Tag: tag, Digest: digest, @@ -186,10 +186,10 @@ func handleManifestPut(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, } // ????? tag ?? - var tagRecord model.Tag + var tagRecord registry.Tag if err := db.Where("repository = ? AND tag = ?", repo, tag).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { - tagRecord = model.Tag{ + tagRecord = registry.Tag{ Repository: repo, Tag: tag, Digest: digest, @@ -217,13 +217,13 @@ func handleManifestPut(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, // handleManifestGet ?? manifest func handleManifestGet(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, tag string) error { - var manifest model.Manifest + var manifest registry.Manifest // ?? tag ?????????????????????? if isDigestFormat(tag) { // ?? digest ???????????? repository digest := tag - + // ?? manifest ??? if err := db.Where("digest = ?", digest).First(&manifest).Error; err != nil { if err == gorm.ErrRecordNotFound { @@ -231,9 +231,9 @@ func handleManifestGet(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, } return resp.R500(c, "", nil, err) } - + // ???? manifest ?????????? repository ?? - var tagRecord model.Tag + var tagRecord registry.Tag if err := db.Where("repository = ? AND digest = ?", repo, digest).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found in this repository") @@ -242,7 +242,7 @@ func handleManifestGet(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, } } else { // ?? tag ???? tag ????????? - var tagRecord model.Tag + var tagRecord registry.Tag if err := db.Where("repository = ? AND tag = ?", repo, tag).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found") @@ -302,13 +302,13 @@ func handleManifestGet(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, // handleManifestHead ?? manifest ???? func handleManifestHead(c fiber.Ctx, db *gorm.DB, store store.Store, repo string, tag string) error { - var manifest model.Manifest + var manifest registry.Manifest // ?? tag ?????????????????????? if isDigestFormat(tag) { // ?? digest ???????????? repository digest := tag - + // ?? manifest ??? if err := db.Where("digest = ?", digest).First(&manifest).Error; err != nil { if err == gorm.ErrRecordNotFound { @@ -316,9 +316,9 @@ func handleManifestHead(c fiber.Ctx, db *gorm.DB, store store.Store, repo string } return resp.R500(c, "", nil, err) } - + // ???? manifest ?????????? repository ?? - var tagRecord model.Tag + var tagRecord registry.Tag if err := db.Where("repository = ? AND digest = ?", repo, digest).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found in this repository") @@ -327,7 +327,7 @@ func handleManifestHead(c fiber.Ctx, db *gorm.DB, store store.Store, repo string } } else { // ?? tag ???? tag ????????? - var tagRecord model.Tag + var tagRecord registry.Tag if err := db.Where("repository = ? AND tag = ?", repo, tag).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found") @@ -358,32 +358,32 @@ func handleManifestDelete(c fiber.Ctx, db *gorm.DB, store store.Store, repo stri if isDigestFormat(tag) { // ?? digest ???????????? repository digest = tag - + // ???? manifest ?????????? repository ?? - var tagRecord model.Tag + var tagRecord registry.Tag if err := db.Where("repository = ? AND digest = ?", repo, digest).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found in this repository") } return resp.R500(c, "", nil, err) } - + // ???????? tag ??? manifest var count int64 - if err := db.Model(&model.Tag{}).Where("digest = ?", digest).Count(&count).Error; err != nil { + if err := db.Model(®istry.Tag{}).Where("digest = ?", digest).Count(&count).Error; err != nil { return resp.R500(c, "", nil, err) } // ??? tag ??????? manifest ?? if count == 0 { - var manifest model.Manifest + var manifest registry.Manifest if err := db.Where("digest = ?", digest).First(&manifest).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found") } return resp.R500(c, "", nil, err) } - + if err := db.Delete(&manifest).Error; err != nil { return resp.R500(c, "", nil, err) } @@ -394,7 +394,7 @@ func handleManifestDelete(c fiber.Ctx, db *gorm.DB, store store.Store, repo stri } } else { // ?? tag ???? tag ????????? - var tagRecord model.Tag + var tagRecord registry.Tag if err := db.Where("repository = ? AND tag = ?", repo, tag).First(&tagRecord).Error; err != nil { if err == gorm.ErrRecordNotFound { return resp.R404(c, "MANIFEST_NOT_FOUND", nil, "manifest not found") @@ -411,13 +411,13 @@ func handleManifestDelete(c fiber.Ctx, db *gorm.DB, store store.Store, repo stri // ???????? tag ??? manifest var count int64 - if err := db.Model(&model.Tag{}).Where("digest = ?", digest).Count(&count).Error; err != nil { + if err := db.Model(®istry.Tag{}).Where("digest = ?", digest).Count(&count).Error; err != nil { return resp.R500(c, "", nil, err) } // ?????? tag ????? manifest ?? if count == 0 { - var manifest model.Manifest + var manifest registry.Manifest if err := db.Where("digest = ?", digest).First(&manifest).Error; err == nil { if err := db.Delete(&manifest).Error; err != nil { return resp.R500(c, "", nil, err) diff --git a/internal/module/registry/registry.go b/internal/module/registry/registry.go index 80f7fa9..2abac4a 100644 --- a/internal/module/registry/registry.go +++ b/internal/module/registry/registry.go @@ -5,7 +5,7 @@ import ( "log" "strings" - "gitea.loveuer.com/loveuer/cluster/internal/model" + registry "gitea.loveuer.com/loveuer/cluster/pkg/model/registry" "gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" @@ -14,12 +14,12 @@ import ( func Registry(ctx context.Context, db *gorm.DB, store store.Store) fiber.Handler { if err := db.AutoMigrate( - &model.Repository{}, - &model.Blob{}, - &model.Manifest{}, - &model.Tag{}, - &model.BlobUpload{}, - &model.RegistryConfig{}, + ®istry.Repository{}, + ®istry.Blob{}, + ®istry.Manifest{}, + ®istry.Tag{}, + ®istry.BlobUpload{}, + ®istry.RegistryConfig{}, ); err != nil { log.Fatalf("failed to migrate database: %v", err) } diff --git a/internal/module/registry/tag.go b/internal/module/registry/tag.go index 0bbb99b..57bcb1b 100644 --- a/internal/module/registry/tag.go +++ b/internal/module/registry/tag.go @@ -4,7 +4,7 @@ import ( "strconv" "strings" - "gitea.loveuer.com/loveuer/cluster/internal/model" + "gitea.loveuer.com/loveuer/cluster/pkg/model/registry" "gitea.loveuer.com/loveuer/cluster/pkg/resp" "gitea.loveuer.com/loveuer/cluster/pkg/store" "github.com/gofiber/fiber/v3" @@ -40,7 +40,7 @@ func HandleTags(c fiber.Ctx, db *gorm.DB, store store.Store) error { repo := strings.Join(parts[:tagsIndex], "/") // Strip registry_address prefix from repo if present - var registryConfig model.RegistryConfig + var registryConfig registry.RegistryConfig registryAddress := "" if err := db.Where("key = ?", "registry_address").First(®istryConfig).Error; err == nil { registryAddress = registryConfig.Value @@ -58,7 +58,7 @@ func HandleTags(c fiber.Ctx, db *gorm.DB, store store.Store) error { last := c.Query("last") // ?? tags - var tags []model.Tag + var tags []registry.Tag query := db.Where("repository = ?", repo).Order("tag ASC").Limit(n + 1) if last != "" { diff --git a/pkg/model/auth/user.go b/pkg/model/auth/user.go new file mode 100644 index 0000000..61248bd --- /dev/null +++ b/pkg/model/auth/user.go @@ -0,0 +1,23 @@ +package auth + +import ( + "time" + + "gorm.io/gorm" +) + +// User 用户模型 +type User struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Username string `gorm:"uniqueIndex;not null" json:"username"` + Password string `gorm:"not null" json:"-"` + Email string `gorm:"index" json:"email"` + Nickname string `json:"nickname"` + Role string `gorm:"default:'user'" json:"role"` + Status string `gorm:"default:'active'" json:"status"` + Permissions string `gorm:"type:text" json:"permissions"` +} diff --git a/pkg/model/k8s/config.go b/pkg/model/k8s/config.go new file mode 100644 index 0000000..af17d08 --- /dev/null +++ b/pkg/model/k8s/config.go @@ -0,0 +1,18 @@ +package k8s + +import ( + "time" + + "gorm.io/gorm" +) + +// ClusterConfig k8s cluster configuration +type ClusterConfig struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Key string `gorm:"uniqueIndex;not null" json:"key"` + Value string `gorm:"type:text" json:"value"` +} diff --git a/pkg/model/registry/blob.go b/pkg/model/registry/blob.go new file mode 100644 index 0000000..1fcb753 --- /dev/null +++ b/pkg/model/registry/blob.go @@ -0,0 +1,20 @@ +package registry + +import ( + "time" + + "gorm.io/gorm" +) + +// Blob blob ?? +type Blob struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Digest string `gorm:"uniqueIndex;not null" json:"digest"` // SHA256 digest + Size int64 `gorm:"not null" json:"size"` // ?????? + MediaType string `json:"media_type"` // ???? + Repository string `gorm:"index" json:"repository"` // ??????????????? +} diff --git a/pkg/model/registry/blob_upload.go b/pkg/model/registry/blob_upload.go new file mode 100644 index 0000000..e589b4c --- /dev/null +++ b/pkg/model/registry/blob_upload.go @@ -0,0 +1,20 @@ +package registry + +import ( + "time" + + "gorm.io/gorm" +) + +// BlobUpload ????? blob ?? +type BlobUpload struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + UUID string `gorm:"uniqueIndex;not null" json:"uuid"` // ???? UUID + Repository string `gorm:"index;not null" json:"repository"` // ???? + Path string `gorm:"not null" json:"path"` // ?????? + Size int64 `gorm:"default:0" json:"size"` // ????? +} diff --git a/pkg/model/registry/config.go b/pkg/model/registry/config.go new file mode 100644 index 0000000..4c46501 --- /dev/null +++ b/pkg/model/registry/config.go @@ -0,0 +1,18 @@ +package registry + +import ( + "time" + + "gorm.io/gorm" +) + +// RegistryConfig registry ????? +type RegistryConfig struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Key string `gorm:"uniqueIndex;not null" json:"key"` // ???? key + Value string `gorm:"type:text" json:"value"` // ???? value +} diff --git a/pkg/model/registry/manifest.go b/pkg/model/registry/manifest.go new file mode 100644 index 0000000..63303f0 --- /dev/null +++ b/pkg/model/registry/manifest.go @@ -0,0 +1,22 @@ +package registry + +import ( + "time" + + "gorm.io/gorm" +) + +// Manifest manifest ?? +type Manifest struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Repository string `gorm:"index;not null" json:"repository"` // ???? + Tag string `gorm:"index;not null" json:"tag"` // tag ?? + Digest string `gorm:"uniqueIndex;not null" json:"digest"` // manifest digest + MediaType string `json:"media_type"` // ???? + Size int64 `gorm:"not null" json:"size"` // manifest ?? + Content []byte `gorm:"type:blob" json:"-"` // manifest ???JSON? +} diff --git a/pkg/model/registry/repository.go b/pkg/model/registry/repository.go new file mode 100644 index 0000000..bc7515f --- /dev/null +++ b/pkg/model/registry/repository.go @@ -0,0 +1,17 @@ +package registry + +import ( + "time" + + "gorm.io/gorm" +) + +// Repository ???? +type Repository struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Name string `gorm:"uniqueIndex;not null" json:"name"` // ?????? "library/nginx" +} diff --git a/pkg/model/registry/tag.go b/pkg/model/registry/tag.go new file mode 100644 index 0000000..e47b2b1 --- /dev/null +++ b/pkg/model/registry/tag.go @@ -0,0 +1,19 @@ +package registry + +import ( + "time" + + "gorm.io/gorm" +) + +// Tag tag ?????????? +type Tag struct { + ID uint `gorm:"primarykey" json:"id"` + CreatedAt time.Time `json:"created_at"` + UpdatedAt time.Time `json:"updated_at"` + DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` + + Repository string `gorm:"index;not null" json:"repository"` // ???? + Tag string `gorm:"index;not null" json:"tag"` // tag ?? + Digest string `gorm:"not null" json:"digest"` // ??? manifest digest +}