mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-07 16:16:37 +08:00
refactor(plugins): restructure admin and message_gateway into standard layered sub-packages
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package service
|
||||
|
||||
import (
|
||||
"Wavelet/core/contracts"
|
||||
"Wavelet/pkg/logger"
|
||||
"Wavelet/plugins/domain/admin/errs"
|
||||
"Wavelet/plugins/domain/admin/model"
|
||||
"Wavelet/plugins/domain/admin/repository"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
analyticsDays = 7
|
||||
|
||||
denyingRobotsFile = "User-Agent: *\nDisallow: /\n"
|
||||
allowingRobotsFile = "User-Agent: *\nAllow: /\n"
|
||||
)
|
||||
|
||||
// RecentSystemLogs reads a page of the process log ring buffer.
|
||||
func RecentSystemLogs(cursor, limit int) model.LogsResponse {
|
||||
entries, hasMore := logger.GlobalRingBuffer.Query(cursor, limit)
|
||||
|
||||
resp := model.LogsResponse{
|
||||
Lines: entries,
|
||||
HasMore: hasMore,
|
||||
}
|
||||
if len(entries) > 0 {
|
||||
resp.NextCursor = entries[0].Index
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
// RobotsTxtBody resolves the robots.txt payload from the indexing setting.
|
||||
func RobotsTxtBody(ctx context.Context) string {
|
||||
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeySearchEngineIndexingEnabled)
|
||||
if err == nil && enabled {
|
||||
return allowingRobotsFile
|
||||
}
|
||||
return denyingRobotsFile
|
||||
}
|
||||
|
||||
// IsAllowedLogOrigin reports whether a WebSocket handshake origin may subscribe to logs.
|
||||
func IsAllowedLogOrigin(ctx context.Context, origin, host string) bool {
|
||||
if origin == "" {
|
||||
return true
|
||||
}
|
||||
|
||||
// 1. 同源检查 (Same-origin check)
|
||||
u, err := url.Parse(origin)
|
||||
if err == nil && strings.EqualFold(u.Host, host) {
|
||||
return true
|
||||
}
|
||||
|
||||
// 2. 检查配置的允许跨域 Origin (Check allowed origins in system config)
|
||||
sc, cfgErr := repository.GetSystemConfigByKey(ctx, model.ConfigKeyServerAddress)
|
||||
if cfgErr != nil || sc.Value == "" {
|
||||
return false
|
||||
}
|
||||
originToCheck := strings.TrimRight(strings.TrimSpace(origin), "/")
|
||||
for _, allowed := range strings.Split(sc.Value, ",") {
|
||||
allowed = strings.TrimRight(strings.TrimSpace(allowed), "/")
|
||||
if allowed != "" && strings.EqualFold(allowed, originToCheck) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// AccessLogs queries the analytical access log store and decorates rows with user names.
|
||||
func AccessLogs(ctx context.Context, q model.AccessLogQuery) (model.AccessLogsResponse, error) {
|
||||
rc := GetRiskControlService()
|
||||
if rc == nil {
|
||||
return model.AccessLogsResponse{}, errs.ErrLogStoreUnavailable
|
||||
}
|
||||
|
||||
filter, err := buildAccessLogFilter(ctx, q)
|
||||
if err != nil {
|
||||
return model.AccessLogsResponse{}, err
|
||||
}
|
||||
if filter.UserIDs != nil && len(filter.UserIDs) == 0 {
|
||||
return model.AccessLogsResponse{Total: 0, List: []model.AccessLogItem{}}, nil
|
||||
}
|
||||
|
||||
logs, total, err := rc.QueryAccessLogs(ctx, filter, q.Page, q.PageSize)
|
||||
if err != nil {
|
||||
return model.AccessLogsResponse{}, err
|
||||
}
|
||||
if total == 0 {
|
||||
return model.AccessLogsResponse{Total: 0, List: []model.AccessLogItem{}}, nil
|
||||
}
|
||||
|
||||
list := make([]model.AccessLogItem, len(logs))
|
||||
for i, logItem := range logs {
|
||||
list[i] = model.AccessLogItem{
|
||||
ID: logItem.ID,
|
||||
UserID: logItem.UserID,
|
||||
Path: logItem.Path,
|
||||
Method: logItem.Method,
|
||||
IP: logItem.IP,
|
||||
UserAgent: logItem.UserAgent,
|
||||
Status: logItem.Status,
|
||||
Latency: logItem.Latency,
|
||||
CreatedAt: logItem.CreatedAt.Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
enrichAccessLogsWithUsers(ctx, list)
|
||||
|
||||
return model.AccessLogsResponse{Total: total, List: list}, nil
|
||||
}
|
||||
|
||||
// AccessLogAnalytics aggregates the daily trend of the access log store.
|
||||
func AccessLogAnalytics(ctx context.Context) (model.LogsAnalyticsResponse, error) {
|
||||
rc := GetRiskControlService()
|
||||
if rc == nil {
|
||||
return model.LogsAnalyticsResponse{}, errs.ErrLogStoreUnavailable
|
||||
}
|
||||
|
||||
stats, err := rc.QueryAccessLogStats(ctx, analyticsDays)
|
||||
if err != nil {
|
||||
return model.LogsAnalyticsResponse{}, fmt.Errorf("%s%w", errs.ErrQueryAccessTrendFailed, err)
|
||||
}
|
||||
|
||||
trendList := make([]model.TrendItem, len(stats))
|
||||
for i, st := range stats {
|
||||
trendList[i] = model.TrendItem{
|
||||
Date: st.Date,
|
||||
Count: st.PV,
|
||||
}
|
||||
}
|
||||
|
||||
return model.LogsAnalyticsResponse{
|
||||
Trend: trendList,
|
||||
Browsers: []model.BrowserItem{},
|
||||
TopUsers: []model.TopUserItem{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// findUserIDsByUsername resolves the user id filter behind a username search term.
|
||||
func findUserIDsByUsername(ctx context.Context, username string) ([]uint64, error) {
|
||||
if userSvc := GetUserService(ctx); userSvc != nil {
|
||||
users, _, err := userSvc.ListUsers(ctx, 1, userQueryMaxLimit, username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errs.ErrQueryUserFailed, err)
|
||||
}
|
||||
ids := make([]uint64, 0, len(users))
|
||||
for _, u := range users {
|
||||
ids = append(ids, u.ID)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
ids, err := repository.SearchUserIDsByUsername(ctx, username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errs.ErrQueryUserFailed, err)
|
||||
}
|
||||
return ids, nil
|
||||
}
|
||||
|
||||
const userQueryMaxLimit = 100
|
||||
|
||||
func buildAccessLogFilter(ctx context.Context, q model.AccessLogQuery) (contracts.AccessLogFilterDTO, error) {
|
||||
filter := contracts.AccessLogFilterDTO{}
|
||||
|
||||
if q.Username != "" {
|
||||
userIDs, err := findUserIDsByUsername(ctx, q.Username)
|
||||
if err != nil {
|
||||
return filter, err
|
||||
}
|
||||
filter.UserIDs = userIDs
|
||||
}
|
||||
|
||||
if q.Path != "" {
|
||||
filter.Path = q.Path
|
||||
}
|
||||
|
||||
if q.StartTime != "" {
|
||||
if t, err := parseAccessLogTime(q.StartTime); err == nil {
|
||||
filter.StartTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
if q.EndTime != "" {
|
||||
if t, err := parseAccessLogTime(q.EndTime); err == nil {
|
||||
filter.EndTime = &t
|
||||
}
|
||||
}
|
||||
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
func parseAccessLogTime(value string) (time.Time, error) {
|
||||
if t, err := time.Parse(time.RFC3339, value); err == nil {
|
||||
return t, nil
|
||||
}
|
||||
return time.Parse("2006-01-02 15:04:05", value)
|
||||
}
|
||||
|
||||
// enrichAccessLogsWithUsers attaches usernames and nicknames to access log rows.
|
||||
func enrichAccessLogsWithUsers(ctx context.Context, list []model.AccessLogItem) {
|
||||
if len(list) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
userIDs := make([]uint64, 0, len(list))
|
||||
seen := make(map[uint64]struct{}, len(list))
|
||||
for _, item := range list {
|
||||
if _, ok := seen[item.UserID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item.UserID] = struct{}{}
|
||||
userIDs = append(userIDs, item.UserID)
|
||||
}
|
||||
|
||||
userMap := make(map[uint64]repository.UserDisplayName, len(userIDs))
|
||||
if userSvc := GetUserService(ctx); userSvc != nil {
|
||||
for _, uid := range userIDs {
|
||||
if u, err := userSvc.GetUserByID(ctx, uid); err == nil && u != nil {
|
||||
userMap[uid] = repository.UserDisplayName{Username: u.Username, Nickname: u.Nickname}
|
||||
}
|
||||
}
|
||||
} else if names, err := repository.LoadUserDisplayNames(ctx, userIDs); err == nil {
|
||||
userMap = names
|
||||
}
|
||||
|
||||
for i := range list {
|
||||
if info, ok := userMap[list[i].UserID]; ok {
|
||||
list[i].Username = info.Username
|
||||
list[i].Nickname = info.Nickname
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user