feat: 初始提交 - Code Server Bridge完整实现
- OAuth认证系统(Gitea + Lua扩展) - Git自动化操作(本地/SSH远程) - 实时进度WebSocket推送 - 现代化Tab界面UI - Cobra CLI命令行(init/version/serve) - 完整构建系统(Makefile + Taskfile) - UPX压缩支持(体积减少70%)
This commit is contained in:
111
internal/oauth/gitea.go
Normal file
111
internal/oauth/gitea.go
Normal file
@@ -0,0 +1,111 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"cs-bridge/internal/auth"
|
||||
"cs-bridge/internal/config"
|
||||
"cs-bridge/pkg/httpclient"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Gitea struct {
|
||||
cfg config.Provider
|
||||
redirectURI string
|
||||
}
|
||||
|
||||
// Name implements [Provider].
|
||||
func (g Gitea) Name() string {
|
||||
return "gitea"
|
||||
}
|
||||
|
||||
// AuthURL implements [Provider].
|
||||
func (g Gitea) AuthURL(state string) (string, error) {
|
||||
u, _ := url.Parse(g.cfg.BaseURL + g.cfg.AuthorizeURL)
|
||||
q := u.Query()
|
||||
q.Set("client_id", g.cfg.ClientID)
|
||||
q.Set("redirect_uri", g.redirectURI)
|
||||
q.Set("response_type", "code")
|
||||
q.Set("scope", "read:user")
|
||||
q.Set("state", state)
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
// Exchange implements [Provider].
|
||||
func (g Gitea) Exchange(code string) (string, error) {
|
||||
resp, err := httpclient.Default.R().
|
||||
SetHeader("Accept", "application/json").
|
||||
SetFormData(map[string]string{
|
||||
"client_id": g.cfg.ClientID,
|
||||
"client_secret": g.cfg.ClientSecret,
|
||||
"code": code,
|
||||
"grant_type": "authorization_code",
|
||||
"redirect_uri": g.redirectURI,
|
||||
}).
|
||||
Post(fmt.Sprintf("%s%s", g.cfg.BaseURL, g.cfg.TokenURL))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var out struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn string `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&out)
|
||||
|
||||
return out.AccessToken, nil
|
||||
}
|
||||
|
||||
// UserInfo implements [Provider].
|
||||
func (g Gitea) UserInfo(token string) (auth.Identify, error) {
|
||||
resp, err := httpclient.Default.R().
|
||||
SetHeader("Authorization", fmt.Sprintf("bearer %s", token)).
|
||||
Get(fmt.Sprintf("%s%s", g.cfg.BaseURL, g.cfg.UserURL))
|
||||
if err != nil {
|
||||
return auth.Identify{}, err
|
||||
}
|
||||
var raw raw
|
||||
json.NewDecoder(resp.Body).Decode(&raw)
|
||||
return auth.Identify{
|
||||
Provider: g.Name(),
|
||||
UserId: fmt.Sprint(raw.ID),
|
||||
Username: raw.Login,
|
||||
Avatar: raw.AvatarURL,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func NewGitea(cfg config.Provider, redirectURI string) Gitea {
|
||||
return Gitea{
|
||||
cfg: cfg,
|
||||
redirectURI: redirectURI,
|
||||
}
|
||||
}
|
||||
|
||||
type raw struct {
|
||||
ID int `json:"id"`
|
||||
Login string `json:"login"`
|
||||
LoginName string `json:"login_name"`
|
||||
SourceID int `json:"source_id"`
|
||||
FullName string `json:"full_name"`
|
||||
Email string `json:"email"`
|
||||
AvatarURL string `json:"avatar_url"`
|
||||
HTMLURL string `json:"html_url"`
|
||||
Language string `json:"language"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
LastLogin time.Time `json:"last_login"`
|
||||
Created time.Time `json:"created"`
|
||||
Restricted bool `json:"restricted"`
|
||||
Active bool `json:"active"`
|
||||
ProhibitLogin bool `json:"prohibit_login"`
|
||||
Location string `json:"location"`
|
||||
Website string `json:"website"`
|
||||
Description string `json:"description"`
|
||||
Visibility string `json:"visibility"`
|
||||
FollowersCount int `json:"followers_count"`
|
||||
FollowingCount int `json:"following_count"`
|
||||
StarredReposCount int `json:"starred_repos_count"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
86
internal/oauth/lua_provider.go
Normal file
86
internal/oauth/lua_provider.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"cs-bridge/internal/auth"
|
||||
"cs-bridge/pkg/lua"
|
||||
"fmt"
|
||||
|
||||
lua2 "github.com/yuin/gopher-lua"
|
||||
)
|
||||
|
||||
type LuaProvider struct {
|
||||
model ProviderModel
|
||||
engine *lua.Engine
|
||||
redirectURI string
|
||||
}
|
||||
|
||||
// NewLuaProvider creates a new Lua-based provider
|
||||
func NewLuaProvider(model ProviderModel, redirectURI, scriptPath string) (*LuaProvider, error) {
|
||||
engine := lua.New()
|
||||
|
||||
// Register all API modules (http, json, log, util)
|
||||
engine.RegisterAPI()
|
||||
|
||||
// Load the Lua script
|
||||
if err := engine.LoadFile(scriptPath); err != nil {
|
||||
engine.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &LuaProvider{
|
||||
model: model,
|
||||
engine: engine,
|
||||
redirectURI: redirectURI,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close cleans up the Lua engine
|
||||
func (p *LuaProvider) Close() {
|
||||
p.engine.Close()
|
||||
}
|
||||
|
||||
// Name implements [Provider].
|
||||
func (p *LuaProvider) Name() string {
|
||||
return p.model.Name
|
||||
}
|
||||
|
||||
// luaConfig constructs the config table passed to Lua functions
|
||||
func (p *LuaProvider) luaConfig() *lua2.LTable {
|
||||
L := p.engine.L
|
||||
config := L.NewTable()
|
||||
config.RawSetString("client_id", lua2.LString(p.model.ClientID))
|
||||
config.RawSetString("client_secret", lua2.LString(p.model.ClientSecret))
|
||||
config.RawSetString("base_url", lua2.LString(p.model.BaseURL))
|
||||
config.RawSetString("authorize_url", lua2.LString(p.model.AuthorizeURL))
|
||||
config.RawSetString("token_url", lua2.LString(p.model.TokenURL))
|
||||
config.RawSetString("user_url", lua2.LString(p.model.UserURL))
|
||||
config.RawSetString("redirect_uri", lua2.LString(p.redirectURI))
|
||||
return config
|
||||
}
|
||||
|
||||
// AuthURL implements [Provider].
|
||||
func (p *LuaProvider) AuthURL(state string) (string, error) {
|
||||
cfg := p.luaConfig()
|
||||
return p.engine.CallString("auth_url", cfg, lua2.LString(state))
|
||||
}
|
||||
|
||||
// Exchange implements [Provider].
|
||||
func (p *LuaProvider) Exchange(code string) (string, error) {
|
||||
cfg := p.luaConfig()
|
||||
return p.engine.CallString("exchange", cfg, lua2.LString(code))
|
||||
}
|
||||
|
||||
// UserInfo implements [Provider].
|
||||
func (p *LuaProvider) UserInfo(token string) (auth.Identify, error) {
|
||||
cfg := p.luaConfig()
|
||||
|
||||
var userInfo auth.Identify
|
||||
if err := p.engine.CallStruct("user_info", &userInfo, cfg, lua2.LString(token)); err != nil {
|
||||
return auth.Identify{}, fmt.Errorf("failed to get user info: %w", err)
|
||||
}
|
||||
|
||||
// Set the provider name
|
||||
userInfo.Provider = p.model.Name
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
25
internal/oauth/manager.go
Normal file
25
internal/oauth/manager.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package oauth
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Manager struct {
|
||||
provider map[string]Provider
|
||||
}
|
||||
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
provider: map[string]Provider{},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Manager) Register(p Provider) {
|
||||
m.provider[p.Name()] = p
|
||||
}
|
||||
|
||||
func (m *Manager) Get(name string) (Provider, error) {
|
||||
p, ok := m.provider[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("oauth provider not found: %s", name)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
22
internal/oauth/model.go
Normal file
22
internal/oauth/model.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package oauth
|
||||
|
||||
import "time"
|
||||
|
||||
type ProviderModel struct {
|
||||
ID uint `gorm:"primaryKey"`
|
||||
Name string `gorm:"uniqueIndex"`
|
||||
Type string
|
||||
BaseURL string
|
||||
ClientID string
|
||||
ClientSecret string
|
||||
|
||||
AuthorizeURL string
|
||||
TokenURL string
|
||||
UserURL string
|
||||
|
||||
LuaScript string
|
||||
Enabled bool
|
||||
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
16
internal/oauth/provider.go
Normal file
16
internal/oauth/provider.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package oauth
|
||||
|
||||
import "cs-bridge/internal/auth"
|
||||
|
||||
type UserInfo struct {
|
||||
ID string `json:"id"`
|
||||
Username string `json:"username"`
|
||||
Avatar string `json:"avatar_url"`
|
||||
}
|
||||
|
||||
type Provider interface {
|
||||
Name() string
|
||||
AuthURL(state string) (string, error)
|
||||
Exchange(code string) (string, error)
|
||||
UserInfo(token string) (auth.Identify, error)
|
||||
}
|
||||
12
internal/oauth/state.go
Normal file
12
internal/oauth/state.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package oauth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
)
|
||||
|
||||
func NewState() string {
|
||||
b := make([]byte, 16)
|
||||
_, _ = rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
||||
Reference in New Issue
Block a user