Files
cyi-box/backend/internal/service/auth.go
T
chiyiandDevin 834ecacdb9 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>
2026-10-01 01:58:47 +08:00

124 lines
3.6 KiB
Go

package service
import (
"crypto/rand"
"encoding/hex"
"errors"
"fmt"
"log"
"strings"
"time"
"github.com/cyi-cc/fun"
"golang.org/x/crypto/bcrypt"
"github.com/cyi-cc/cyi-box/backend/internal/dto"
"github.com/cyi-cc/cyi-box/backend/internal/store"
)
// 登录态:24h 滑动过期;距上次续期超过 2h 的请求自动把有效期顺延回 24h
const sessionTTL = 24 * time.Hour
const sessionRenewAfter = 2 * time.Hour
type AuthSvc struct {
fun.Ctx
Store *store.Store
}
// Login 用户名+密码 → 签发会话 token(前端存 localStorage,随 state 透传)
func (s *AuthSvc) Login(d dto.LoginDto) (dto.SessionView, error) {
name := strings.TrimSpace(d.Name)
if name == "" || d.Password == "" {
return dto.SessionView{}, fun.Error(4001, "请输入用户名和密码")
}
user, err := s.Store.GetUserByName(name)
if errors.Is(err, store.ErrNotFound) {
return dto.SessionView{}, fun.Error(4001, "用户名或密码错误")
}
if err != nil {
return dto.SessionView{}, fmt.Errorf("auth: query user failed: %w", err)
}
if bcrypt.CompareHashAndPassword([]byte(user.PasswordHash), []byte(d.Password)) != nil {
return dto.SessionView{}, fun.Error(4001, "用户名或密码错误")
}
if user.Status != "active" {
return dto.SessionView{}, fun.Error(4003, "账号已被禁用")
}
token, err := newToken()
if err != nil {
return dto.SessionView{}, err
}
if err := s.Store.CreateSession(token, user.Id, sessionTTL); err != nil {
return dto.SessionView{}, fmt.Errorf("auth: create session failed: %w", err)
}
log.Printf("auth: user %q logged in from %s", user.Name, s.Ip)
view := userView(user)
return dto.SessionView{User: &view, Token: &token}, nil
}
// Me 返回当前登录用户(不重复签发 token)
func (s *AuthSvc) Me() (dto.SessionView, error) {
su, err := sessionFrom(s.RequestCtx)
if err != nil {
return dto.SessionView{}, err
}
view := userView(su.User)
return dto.SessionView{User: &view}, nil
}
// Logout 幂等:删掉当前会话即可
func (s *AuthSvc) Logout() (dto.OkView, error) {
su, err := sessionFrom(s.RequestCtx)
if err != nil {
return dto.OkView{Message: "已退出"}, nil
}
if err := s.Store.DeleteSession(su.Token); err != nil {
return dto.OkView{}, fmt.Errorf("auth: logout failed: %w", err)
}
return dto.OkView{Message: "已退出"}, nil
}
// ChangePassword 校验旧密码 → 改密 → 吊销其他会话(当前会话保留)
func (s *AuthSvc) ChangePassword(d dto.ChangePasswordDto) (dto.OkView, error) {
su, err := sessionFrom(s.RequestCtx)
if err != nil {
return dto.OkView{}, err
}
if len(d.NewPassword) < 6 {
return dto.OkView{}, fun.Error(4001, "新密码至少 6 位")
}
if bcrypt.CompareHashAndPassword([]byte(su.User.PasswordHash), []byte(d.OldPassword)) != nil {
return dto.OkView{}, fun.Error(4001, "旧密码错误")
}
hash, err := bcrypt.GenerateFromPassword([]byte(d.NewPassword), bcrypt.DefaultCost)
if err != nil {
return dto.OkView{}, err
}
h := string(hash)
if err := s.Store.UpdateUser(su.User.Id, su.User.Name, su.User.Role, su.User.Status, &h); err != nil {
return dto.OkView{}, fmt.Errorf("auth: update password failed: %w", err)
}
if err := s.Store.DeleteOtherSessions(su.User.Id, su.Token); err != nil {
return dto.OkView{}, fmt.Errorf("auth: revoke sessions failed: %w", err)
}
return dto.OkView{Message: "密码已修改"}, nil
}
func newToken() (string, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil {
return "", err
}
return hex.EncodeToString(b), nil
}
func userView(u store.User) dto.UserView {
return dto.UserView{
Id: u.Id,
Name: u.Name,
Role: u.Role,
Status: u.Status,
CreatedAt: u.CreatedAt,
}
}