cyi-box: Memphis-style toolbox admin platform
Go(fun)+SQLite backend, Vue 3 frontend. Modules: dashboard, pay gateway (Epay-compatible + CDK stock), license/card system (per-app appid, domain binding), AI memory bank with built-in MCP server (/mcp, FTS5 search, signed read links), server SSH monitor + terminal + SFTP, proxy pool with protocol-level alive checks, DB manager (mysql/pg/sqlite + SQL autocomplete + import/export), disk share, vault (AES-GCM + TOTP), bookmarks, dev tools. Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/cyi-cc/fun"
|
||||
|
||||
"github.com/cyi-cc/cyi-box/backend/internal/config"
|
||||
"github.com/cyi-cc/cyi-box/backend/internal/dto"
|
||||
"github.com/cyi-cc/cyi-box/backend/internal/store"
|
||||
"github.com/cyi-cc/cyi-box/backend/internal/vault"
|
||||
)
|
||||
|
||||
// VaultSvc 密码箱:密文只在服务端出现;列表只回元数据,
|
||||
// 密码经 Reveal 显式取回,2FA 只回当前验证码不回密钥。
|
||||
type VaultSvc struct {
|
||||
fun.Ctx
|
||||
Store *store.Store
|
||||
Cfg *config.Config `fun:"auto"`
|
||||
}
|
||||
|
||||
func (s *VaultSvc) owner() (store.SessionUser, error) {
|
||||
return sessionFrom(s.RequestCtx)
|
||||
}
|
||||
|
||||
func (s *VaultSvc) List() ([]dto.VaultItemView, error) {
|
||||
su, err := s.owner()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, err := s.Store.ListVault(su.User.Id, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("vault: list failed: %w", err)
|
||||
}
|
||||
views := make([]dto.VaultItemView, 0, len(items))
|
||||
for _, e := range items {
|
||||
views = append(views, vaultView(e))
|
||||
}
|
||||
return views, nil
|
||||
}
|
||||
|
||||
func (s *VaultSvc) Save(d dto.SaveVaultDto) (dto.VaultItemView, error) {
|
||||
su, err := s.owner()
|
||||
if err != nil {
|
||||
return dto.VaultItemView{}, err
|
||||
}
|
||||
title := strings.TrimSpace(d.Title)
|
||||
if title == "" || len(title) > 64 {
|
||||
return dto.VaultItemView{}, fun.Error(4001, "标题必填且不超过 64 字符")
|
||||
}
|
||||
|
||||
e := store.VaultEntry{UserID: su.User.Id, Title: title}
|
||||
if d.Id != nil {
|
||||
old, err := s.Store.GetVault(su.User.Id, *d.Id)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return dto.VaultItemView{}, fun.Error(4001, "条目不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return dto.VaultItemView{}, err
|
||||
}
|
||||
e = old
|
||||
e.Title = title
|
||||
}
|
||||
if d.Username != nil {
|
||||
e.Username = strings.TrimSpace(*d.Username)
|
||||
}
|
||||
if d.Url != nil {
|
||||
e.Url = strings.TrimSpace(*d.Url)
|
||||
}
|
||||
if d.Note != nil {
|
||||
e.Note = strings.TrimSpace(*d.Note)
|
||||
}
|
||||
|
||||
if d.Password != nil {
|
||||
enc, err := vault.Encrypt(s.Cfg.VaultKey, *d.Password)
|
||||
if err != nil {
|
||||
return dto.VaultItemView{}, err
|
||||
}
|
||||
e.PasswordEnc = enc
|
||||
}
|
||||
if d.TotpSecret != nil {
|
||||
secret := strings.TrimSpace(*d.TotpSecret)
|
||||
if secret == "" {
|
||||
e.TotpEnc = "" // 空字符串 = 清除 2FA
|
||||
} else {
|
||||
parsed, err := vault.ParseSecret(secret)
|
||||
if err != nil {
|
||||
return dto.VaultItemView{}, fun.Error(4001, "2FA 密钥无效:支持 base32 或 otpauth:// 链接")
|
||||
}
|
||||
enc, err := vault.Encrypt(s.Cfg.VaultKey, parsed)
|
||||
if err != nil {
|
||||
return dto.VaultItemView{}, err
|
||||
}
|
||||
e.TotpEnc = enc
|
||||
}
|
||||
}
|
||||
if err := s.Store.SaveVault(&e); err != nil {
|
||||
return dto.VaultItemView{}, fmt.Errorf("vault: save failed: %w", err)
|
||||
}
|
||||
return vaultView(e), nil
|
||||
}
|
||||
|
||||
// Reveal 显式取回明文密码(前端点「查看」才调用,列表永不携带)
|
||||
func (s *VaultSvc) Reveal(d dto.VaultIdDto) (dto.SecretView, error) {
|
||||
su, err := s.owner()
|
||||
if err != nil {
|
||||
return dto.SecretView{}, err
|
||||
}
|
||||
e, err := s.Store.GetVault(su.User.Id, d.Id)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return dto.SecretView{}, fun.Error(4001, "条目不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return dto.SecretView{}, err
|
||||
}
|
||||
if e.PasswordEnc == "" {
|
||||
return dto.SecretView{}, fun.Error(4001, "该条目没有密码")
|
||||
}
|
||||
pt, err := vault.Decrypt(s.Cfg.VaultKey, e.PasswordEnc)
|
||||
if err != nil {
|
||||
return dto.SecretView{}, fmt.Errorf("vault: decrypt failed: %w", err)
|
||||
}
|
||||
return dto.SecretView{Value: pt}, nil
|
||||
}
|
||||
|
||||
// Totp 返回当前 6 位验证码与剩余秒数;密钥不出服务端
|
||||
func (s *VaultSvc) Totp(d dto.VaultIdDto) (dto.TotpView, error) {
|
||||
su, err := s.owner()
|
||||
if err != nil {
|
||||
return dto.TotpView{}, err
|
||||
}
|
||||
e, err := s.Store.GetVault(su.User.Id, d.Id)
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return dto.TotpView{}, fun.Error(4001, "条目不存在")
|
||||
}
|
||||
if err != nil {
|
||||
return dto.TotpView{}, err
|
||||
}
|
||||
if e.TotpEnc == "" {
|
||||
return dto.TotpView{}, fun.Error(4001, "该条目未设置 2FA")
|
||||
}
|
||||
secret, err := vault.Decrypt(s.Cfg.VaultKey, e.TotpEnc)
|
||||
if err != nil {
|
||||
return dto.TotpView{}, fmt.Errorf("vault: decrypt totp failed: %w", err)
|
||||
}
|
||||
code, left, err := vault.TOTP(secret, time.Now())
|
||||
if err != nil {
|
||||
return dto.TotpView{}, fun.Error(4001, "2FA 密钥无效")
|
||||
}
|
||||
return dto.TotpView{Code: code, SecondsLeft: left}, nil
|
||||
}
|
||||
|
||||
func (s *VaultSvc) Delete(d dto.VaultIdDto) (dto.OkView, error) {
|
||||
su, err := s.owner()
|
||||
if err != nil {
|
||||
return dto.OkView{}, err
|
||||
}
|
||||
if err := s.Store.DeleteVault(su.User.Id, d.Id); err != nil {
|
||||
if errors.Is(err, store.ErrNotFound) {
|
||||
return dto.OkView{}, fun.Error(4001, "条目不存在")
|
||||
}
|
||||
return dto.OkView{}, fmt.Errorf("vault: delete failed: %w", err)
|
||||
}
|
||||
return dto.OkView{Message: "已删除"}, nil
|
||||
}
|
||||
|
||||
func vaultView(e store.VaultEntry) dto.VaultItemView {
|
||||
v := dto.VaultItemView{
|
||||
Id: e.Id,
|
||||
Title: e.Title,
|
||||
HasPassword: e.PasswordEnc != "",
|
||||
HasTotp: e.TotpEnc != "",
|
||||
UpdatedAt: e.UpdatedAt,
|
||||
}
|
||||
if e.Username != "" {
|
||||
v.Username = &e.Username
|
||||
}
|
||||
if e.Url != "" {
|
||||
v.Url = &e.Url
|
||||
}
|
||||
if e.Note != "" {
|
||||
v.Note = &e.Note
|
||||
}
|
||||
return v
|
||||
}
|
||||
Reference in New Issue
Block a user