重組前檢查點:根目錄 main package

This commit is contained in:
2026-10-03 09:23:38 +08:00
commit f373cb8d37
45 changed files with 5332 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
package main
import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"time"
"gorm.io/gorm"
)
// Session 為使用者瀏覽器 Session(SSO 核心):ID 為加密安全亂數,
// 存於 HttpOnly Cookie,效期內使用者再經任何 RP 發起授權請求時
// 無須重新輸入帳密。
type Session struct {
ID string `gorm:"primaryKey;size:43"` // 32 bytes 亂數的 base64url
UserID uint `gorm:"not null;index"`
User User
ExpiresAt time.Time `gorm:"not null"`
CreatedAt time.Time
UpdatedAt time.Time
}
// sessionTTL 為 Session 有效時間,到期後 Cookie 失效、列為可清除。
const sessionTTL = 24 * time.Hour
// ErrSessionExpired 表示 Session 不存在或已過期。
var ErrSessionExpired = errors.New("session 不存在或已過期")
// newRandomToken 產生 n bytes 加密安全亂數的 base64url 字串(無填充;
// n=32 時為 43 字元),供 Session ID 與 CSRF token 共用。
func newRandomToken(n int) (string, error) {
b := make([]byte, n)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("read random: %w", err)
}
return base64.RawURLEncoding.EncodeToString(b), nil
}
// createSession 為使用者建立新 Session,順帶刪除所有已過期 Session
// (最佳清除,失敗不影響登入結果)。
func createSession(db *gorm.DB, userID uint) (*Session, error) {
id, err := newRandomToken(32)
if err != nil {
return nil, err
}
s := &Session{ID: id, UserID: userID, ExpiresAt: time.Now().Add(sessionTTL)}
if err := db.Create(s).Error; err != nil {
return nil, fmt.Errorf("create session: %w", err)
}
db.Where("expires_at < ?", time.Now()).Delete(&Session{})
return s, nil
}
// deleteSession 以 ID 刪除 Session(登出用)。查無該 Session 不視為
// 錯誤,讓登出維持冪等。
func deleteSession(db *gorm.DB, id string) error {
if err := db.Delete(&Session{}, "id = ?", id).Error; err != nil {
return fmt.Errorf("delete session: %w", err)
}
return nil
}
// getSession 以 ID 查詢效期內的 Session(含所屬使用者)。
func getSession(db *gorm.DB, id string) (*Session, error) {
var s Session
err := db.Preload("User").Where("id = ? AND expires_at > ?", id, time.Now()).First(&s).Error
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ErrSessionExpired
}
if err != nil {
return nil, fmt.Errorf("query session: %w", err)
}
return &s, nil
}