193 lines
5.0 KiB
Go
193 lines
5.0 KiB
Go
package httpapi
|
||
|
||
import (
|
||
"crypto/rand"
|
||
"encoding/hex"
|
||
"net/http"
|
||
"regexp"
|
||
"sync"
|
||
"time"
|
||
)
|
||
|
||
var bearerRe = regexp.MustCompile(`Bearer\s(\S+)`)
|
||
|
||
// Token 对应 getBearerToken() + $_POST['token'] + $_GET['token'] 三通道
|
||
func (c *Ctx) Token() string {
|
||
if m := bearerRe.FindStringSubmatch(c.G.GetHeader("Authorization")); m != nil {
|
||
return m[1]
|
||
}
|
||
if t := c.Form("token"); t != "" {
|
||
return t
|
||
}
|
||
if t := c.Q("token"); t != "" {
|
||
return t
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// UID 对应 getUserId():通过 token 查 user_tokens;未登录返回 0。
|
||
func (c *Ctx) UID() int64 {
|
||
if c.uidResolved {
|
||
return c.uid
|
||
}
|
||
c.uidResolved = true
|
||
tok := c.Token()
|
||
if tok == "" {
|
||
return 0
|
||
}
|
||
row, err := c.DB.QueryOne("SELECT user_id FROM user_tokens WHERE token = ? AND expires_at > NOW()", tok)
|
||
if err != nil || row == nil {
|
||
return 0
|
||
}
|
||
c.uid = row.Int("user_id")
|
||
return c.uid
|
||
}
|
||
|
||
// CoupleID 对应 getCoupleId();无则返回 0
|
||
func (c *Ctx) CoupleID() int64 {
|
||
if c.coupleResolved {
|
||
return c.coupleID
|
||
}
|
||
c.coupleResolved = true
|
||
uid := c.UID()
|
||
if uid == 0 {
|
||
return 0
|
||
}
|
||
row, _ := c.DB.QueryOne("SELECT couple_id FROM users WHERE id = ?", uid)
|
||
if row == nil {
|
||
return 0
|
||
}
|
||
c.coupleID = row.Int("couple_id")
|
||
return c.coupleID
|
||
}
|
||
|
||
// NewToken 生成 32 位 hex token(与 PHP session_id 格式一致)
|
||
func NewToken() string {
|
||
b := make([]byte, 16)
|
||
rand.Read(b)
|
||
return hex.EncodeToString(b)
|
||
}
|
||
|
||
// SaveUserToken 对应 saveUserToken(),写入新 token(7 天)并清理过期 token。
|
||
// PHP 会删除该用户旧 token,但旧 session 文件仍然有效(实际上允许多端同时登录);
|
||
// Go 没有 session 文件,因此保留旧 token 以维持同样的多端登录效果。
|
||
func (c *Ctx) SaveUserToken(userID int64, token string) {
|
||
c.DB.Exec("DELETE FROM user_tokens WHERE expires_at < NOW()")
|
||
c.DB.Exec("INSERT INTO user_tokens (token, user_id, expires_at) VALUES (?, ?, DATE_ADD(NOW(), INTERVAL 7 DAY))", token, userID)
|
||
}
|
||
|
||
// requireUserLogin 对应 requireUserLogin():401/404,并做 5 分钟去重的登录记录
|
||
func (c *Ctx) requireUserLogin() error {
|
||
uid := c.UID()
|
||
if uid == 0 {
|
||
return c.Err(401, "请先登录")
|
||
}
|
||
if err := c.DB.AdvanceUnbindings(); err != nil {
|
||
return err
|
||
}
|
||
user, err := c.DB.QueryOne("SELECT couple_id, last_login_at FROM users WHERE id = ?", uid)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
if user == nil {
|
||
return c.Err(404, "用户不存在")
|
||
}
|
||
needRecord := false
|
||
if user.IsNull("last_login_at") || user.Str("last_login_at") == "" {
|
||
needRecord = true
|
||
} else if t, err := time.ParseInLocation("2006-01-02 15:04:05", user.Str("last_login_at"), time.Local); err != nil || time.Since(t) > 300*time.Second {
|
||
needRecord = true
|
||
}
|
||
if needRecord {
|
||
c.DB.Exec("UPDATE users SET last_login_at = NOW() WHERE id = ?", uid)
|
||
if user.Int("couple_id") != 0 {
|
||
c.DB.Exec("INSERT INTO activity_log (couple_id, user_id, action_type, description, detail, created_at) VALUES (?, ?, 'login', '登录了小程序', '', NOW())", user.Int("couple_id"), uid)
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ---------- 后台管理员会话(对应 PHP Cookie Session)----------
|
||
|
||
const adminCookieName = "PHPSESSID"
|
||
const adminSessionTTL = 12 * time.Hour
|
||
|
||
type adminSession struct {
|
||
AdminID int64
|
||
Username string
|
||
Expires time.Time
|
||
}
|
||
|
||
type AdminSessions struct {
|
||
mu sync.Mutex
|
||
m map[string]*adminSession
|
||
}
|
||
|
||
func NewAdminSessions() *AdminSessions {
|
||
s := &AdminSessions{m: map[string]*adminSession{}}
|
||
go func() {
|
||
for range time.Tick(10 * time.Minute) {
|
||
s.mu.Lock()
|
||
now := time.Now()
|
||
for k, v := range s.m {
|
||
if v.Expires.Before(now) {
|
||
delete(s.m, k)
|
||
}
|
||
}
|
||
s.mu.Unlock()
|
||
}
|
||
}()
|
||
return s
|
||
}
|
||
|
||
func (s *AdminSessions) get(id string) *adminSession {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
v := s.m[id]
|
||
if v == nil || v.Expires.Before(time.Now()) {
|
||
delete(s.m, id)
|
||
return nil
|
||
}
|
||
v.Expires = time.Now().Add(adminSessionTTL)
|
||
return v
|
||
}
|
||
|
||
// AdminSession 当前请求的管理员会话;未登录返回 nil
|
||
func (c *Ctx) AdminSession() *adminSession {
|
||
id, err := c.G.Cookie(adminCookieName)
|
||
if err != nil || id == "" {
|
||
return nil
|
||
}
|
||
return c.App.Admin.get(id)
|
||
}
|
||
|
||
// AdminLogin 建立管理员会话并写 Cookie(对应 session_start + $_SESSION['admin_logged_in']=true)
|
||
func (c *Ctx) AdminLogin(adminID int64, username string) {
|
||
id := NewToken()
|
||
c.App.Admin.mu.Lock()
|
||
c.App.Admin.m[id] = &adminSession{AdminID: adminID, Username: username, Expires: time.Now().Add(adminSessionTTL)}
|
||
c.App.Admin.mu.Unlock()
|
||
c.G.SetSameSite(http.SameSiteLaxMode)
|
||
c.G.SetCookie(adminCookieName, id, 0, "/", "", false, true)
|
||
}
|
||
|
||
// AdminLogout 对应 session_destroy()
|
||
func (c *Ctx) AdminLogout() {
|
||
if id, err := c.G.Cookie(adminCookieName); err == nil {
|
||
c.App.Admin.mu.Lock()
|
||
delete(c.App.Admin.m, id)
|
||
c.App.Admin.mu.Unlock()
|
||
}
|
||
c.G.SetCookie(adminCookieName, "", -1, "/", "", false, true)
|
||
}
|
||
|
||
// requireAdminLogin 对应 requireAdminLogin()
|
||
func (c *Ctx) requireAdminLogin() error {
|
||
if c.AdminSession() == nil {
|
||
return c.Err(401, "未登录或登录已过期")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (c *Ctx) SetCoupleID(cid int64) { c.coupleID = cid; c.coupleResolved = true }
|