feat(session): add session list API
- Move token-related functions from auth.go to new session.go - Add session tracking with expiration and cleanup - Implement session list API endpoint - Update login/logout handlers to use session system - Add hourly cleanup of expired tokens and sessions
This commit is contained in:
@@ -59,8 +59,10 @@ For detailed API documentation, please see [docs/api.md](docs/api.md)
|
|||||||
## TODO
|
## TODO
|
||||||
|
|
||||||
- [x] Add Windows boot service support
|
- [x] Add Windows boot service support
|
||||||
|
- [x] Add session list API
|
||||||
- [ ] Add session management API
|
- [ ] Add session management API
|
||||||
- [ ] Add codes per file documentation
|
- [ ] Add codes per file documentation
|
||||||
|
- [ ] Add user config modify API
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
|
|||||||
@@ -87,6 +87,15 @@ func main() {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(1 * time.Hour)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for range ticker.C {
|
||||||
|
CleanupExpiredTokens()
|
||||||
|
CleanupExpiredSessions()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
quit := make(chan os.Signal, 1)
|
quit := make(chan os.Signal, 1)
|
||||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
<-quit
|
<-quit
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ func setupRoutes() {
|
|||||||
http.HandleFunc("/userMgr/create", CreateUserHandler)
|
http.HandleFunc("/userMgr/create", CreateUserHandler)
|
||||||
http.HandleFunc("/userMgr/remove", RemoveUserHandler)
|
http.HandleFunc("/userMgr/remove", RemoveUserHandler)
|
||||||
http.HandleFunc("/userMgr/list", ListUserHandler)
|
http.HandleFunc("/userMgr/list", ListUserHandler)
|
||||||
|
// http.HandleFunc("/userMgr/modify", ModifyUserHandler)
|
||||||
|
|
||||||
|
http.HandleFunc("/sessionMgr/list", ListActiveSessionsHandler)
|
||||||
|
|
||||||
http.HandleFunc("/frpcAct/instanceMgr/create", CreateInstanceHandler)
|
http.HandleFunc("/frpcAct/instanceMgr/create", CreateInstanceHandler)
|
||||||
http.HandleFunc("/frpcAct/instanceMgr/list", ListInstancesHandler)
|
http.HandleFunc("/frpcAct/instanceMgr/list", ListInstancesHandler)
|
||||||
|
|||||||
+96
-1
@@ -26,6 +26,19 @@ var (
|
|||||||
tokenTTL = time.Hour
|
tokenTTL = time.Hour
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Session struct {
|
||||||
|
ID string
|
||||||
|
UserID int
|
||||||
|
Username string
|
||||||
|
ExpireAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
sessionMap = make(map[string]*Session)
|
||||||
|
sessionMux sync.RWMutex
|
||||||
|
sessionTTL = time.Hour
|
||||||
|
)
|
||||||
|
|
||||||
func GenerateToken(userID int) (string, error) {
|
func GenerateToken(userID int) (string, error) {
|
||||||
randomBytes := make([]byte, 32)
|
randomBytes := make([]byte, 32)
|
||||||
_, err := rand.Read(randomBytes)
|
_, err := rand.Read(randomBytes)
|
||||||
@@ -131,6 +144,19 @@ func CleanupExpiredTokens() {
|
|||||||
if time.Since(tokenInfo.CreatedAt) > tokenTTL {
|
if time.Since(tokenInfo.CreatedAt) > tokenTTL {
|
||||||
delete(tokenMap, userID)
|
delete(tokenMap, userID)
|
||||||
postLog.Debug(fmt.Sprintf("[CleanupExpiredTokens] Removed expired token for userID %d: %s", userID, tokenInfo.Token))
|
postLog.Debug(fmt.Sprintf("[CleanupExpiredTokens] Removed expired token for userID %d: %s", userID, tokenInfo.Token))
|
||||||
|
RemoveSession(userID, "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func CleanupExpiredSessions() {
|
||||||
|
sessionMux.Lock()
|
||||||
|
defer sessionMux.Unlock()
|
||||||
|
|
||||||
|
for sessionID, session := range sessionMap {
|
||||||
|
if time.Now().After(session.ExpireAt) {
|
||||||
|
delete(sessionMap, sessionID)
|
||||||
|
postLog.Debug(fmt.Sprintf("[CleanupExpiredSessions] Removed expired session %s for user [%d]%s", sessionID, session.UserID, session.Username))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -215,10 +241,79 @@ func DeleteTokenInfo(userID int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetUsernameByID(userID int) (string) {
|
func GetUsernameByID(userID int) string {
|
||||||
user, err := GetUserByID(userID)
|
user, err := GetUserByID(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
return user.Username
|
return user.Username
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateSessionID() string {
|
||||||
|
randomBytes := make([]byte, 16)
|
||||||
|
_, err := rand.Read(randomBytes)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Sprintf("session-%d-%s", time.Now().UnixNano(), fmt.Sprintf("%d", time.Now().Unix()))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("session-%x-%d", randomBytes, time.Now().UnixNano())
|
||||||
|
}
|
||||||
|
|
||||||
|
func JoinSession(userID int, userName string, token string) error {
|
||||||
|
sessionMux.Lock()
|
||||||
|
defer sessionMux.Unlock()
|
||||||
|
|
||||||
|
sessionID := generateSessionID()
|
||||||
|
session := &Session{
|
||||||
|
ID: sessionID,
|
||||||
|
UserID: userID,
|
||||||
|
Username: userName,
|
||||||
|
ExpireAt: time.Now().Add(sessionTTL),
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionMap[sessionID] = session
|
||||||
|
postLog.Info(fmt.Sprintf("[JoinSession] User [%d]%s joined session %s with token %s", userID, userName, sessionID, token))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveSession(userID int, token string) error {
|
||||||
|
sessionMux.Lock()
|
||||||
|
defer sessionMux.Unlock()
|
||||||
|
|
||||||
|
var sessionIDToRemove string
|
||||||
|
for sessionID, session := range sessionMap {
|
||||||
|
if session.UserID == userID {
|
||||||
|
sessionIDToRemove = sessionID
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionIDToRemove != "" {
|
||||||
|
delete(sessionMap, sessionIDToRemove)
|
||||||
|
postLog.Info(fmt.Sprintf("[RemoveSession] Removed session %s for user [%d]%s", sessionIDToRemove, userID, GetUsernameByID(userID)))
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenMux.Lock()
|
||||||
|
defer tokenMux.Unlock()
|
||||||
|
|
||||||
|
if tokenInfo, exists := tokenMap[userID]; exists {
|
||||||
|
if tokenInfo.Token == token || token == "" {
|
||||||
|
delete(tokenMap, userID)
|
||||||
|
postLog.Info(fmt.Sprintf("[RemoveSession] Removed token for user [%d]%s", userID, GetUsernameByID(userID)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListActiveSessions() []*Session {
|
||||||
|
sessionMux.RLock()
|
||||||
|
defer sessionMux.RUnlock()
|
||||||
|
|
||||||
|
var activeSessions []*Session
|
||||||
|
for _, session := range sessionMap {
|
||||||
|
if session.ExpireAt.After(time.Now()) {
|
||||||
|
activeSessions = append(activeSessions, session)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return activeSessions
|
||||||
|
}
|
||||||
+38
-1
@@ -161,6 +161,12 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := JoinSession(user.UserID, user.Username, token); err != nil {
|
||||||
|
SendErrorResponse(w, http.StatusInternalServerError, "Failed to create session")
|
||||||
|
postLog.Error(fmt.Sprintf("[LoginHandler] Failed to create session for user \"%s\": %v", req.Username, err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
SendSuccessResponse(w, "Login successful", map[string]interface{}{
|
SendSuccessResponse(w, "Login successful", map[string]interface{}{
|
||||||
"token": token,
|
"token": token,
|
||||||
"userID": user.UserID,
|
"userID": user.UserID,
|
||||||
@@ -189,7 +195,7 @@ func LogoutHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := DeleteTokenInfo(userID); err != nil {
|
if err := RemoveSession(userID, r.Header.Get("X-Token")); err != nil {
|
||||||
SendErrorResponse(w, http.StatusInternalServerError, "Failed to logout")
|
SendErrorResponse(w, http.StatusInternalServerError, "Failed to logout")
|
||||||
postLog.Error(fmt.Sprintf("[LogoutHandler] Failed to logout user [%d]%s: %v", userID, GetUsernameByID(userID), err))
|
postLog.Error(fmt.Sprintf("[LogoutHandler] Failed to logout user [%d]%s: %v", userID, GetUsernameByID(userID), err))
|
||||||
return
|
return
|
||||||
@@ -368,3 +374,34 @@ func ListUserHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
SendSuccessResponse(w, "User list retrieved successfully", userList)
|
SendSuccessResponse(w, "User list retrieved successfully", userList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ListActiveSessionsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.Method != http.MethodGet {
|
||||||
|
SendErrorResponse(w, http.StatusMethodNotAllowed, "Invalid request method")
|
||||||
|
postLog.Warning(fmt.Sprintf("[ListActiveSessionsHandler] Invalid request method: %s", r.Method))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ValidateTimeStamp(r.Header) {
|
||||||
|
SendErrorResponse(w, http.StatusBadRequest, "Invalid or missing X-Timestamp in header")
|
||||||
|
postLog.Warning("[ListActiveSessionsHandler] Invalid or missing X-Timestamp in header")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, err := extractUserIDFromToken(r.Header.Get("X-Token"))
|
||||||
|
if err != nil {
|
||||||
|
SendErrorResponse(w, http.StatusUnauthorized, "Invalid or missing token")
|
||||||
|
postLog.Warning(fmt.Sprintf("[ListActiveSessionsHandler] Invalid or missing token: %v", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := CheckPermission(userID, "superuser"); err != nil {
|
||||||
|
SendErrorResponse(w, http.StatusForbidden, "Permission denied")
|
||||||
|
postLog.Warning(fmt.Sprintf("[ListActiveSessionsHandler] Permission denied for user [%d]%s: %v", userID, GetUsernameByID(userID), err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessions := ListActiveSessions()
|
||||||
|
postLog.Debug(fmt.Sprintf("[ListActiveSessionsHandler] User [%d]%s listed %d active sessions", userID, GetUsernameByID(userID), len(sessions)))
|
||||||
|
SendSuccessResponse(w, "Active sessions listed", sessions)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user