Compare commits
5 Commits
92f278d38b
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 62cf68b49b | |||
| e818ac8764 | |||
| 98d8525fa9 | |||
| 19777df2ed | |||
| d586bbeabb |
@@ -0,0 +1,579 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
_ "github.com/go-sql-driver/mysql"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type appConfig struct {
|
||||||
|
MySQL struct {
|
||||||
|
Addr string `yaml:"Addr"`
|
||||||
|
Username string `yaml:"Username"`
|
||||||
|
Password string `yaml:"Password"`
|
||||||
|
Dbname string `yaml:"Dbname"`
|
||||||
|
Config string `yaml:"Config"`
|
||||||
|
} `yaml:"MySQL"`
|
||||||
|
Redis struct {
|
||||||
|
Host string `yaml:"Host"`
|
||||||
|
Pass string `yaml:"Pass"`
|
||||||
|
DB int `yaml:"DB"`
|
||||||
|
} `yaml:"Redis"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type userRow struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
ReferCode string `json:"refer_code"`
|
||||||
|
Balance int64 `json:"balance"`
|
||||||
|
Commission int64 `json:"commission"`
|
||||||
|
GiftAmount int64 `json:"gift_amount"`
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
IsAdmin bool `json:"is_admin"`
|
||||||
|
ValidEmail bool `json:"valid_email"`
|
||||||
|
MemberStatus string `json:"member_status"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
DeletedAt sql.NullTime `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type authMethod struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
AuthType string `json:"auth_type"`
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
Verified bool `json:"verified"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type deviceInfo struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
IP string `json:"ip"`
|
||||||
|
UserAgent string `json:"user_agent"`
|
||||||
|
Identifier string `json:"identifier"`
|
||||||
|
ShortCode string `json:"short_code"`
|
||||||
|
Online bool `json:"online"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type subscribeInfo struct {
|
||||||
|
ID int64 `json:"id"`
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
OrderID int64 `json:"order_id"`
|
||||||
|
SubscribeID int64 `json:"subscribe_id"`
|
||||||
|
Token string `json:"token"`
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
Status uint8 `json:"status"`
|
||||||
|
StartTime time.Time `json:"start_time"`
|
||||||
|
ExpireTime time.Time `json:"expire_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type familyInfo struct {
|
||||||
|
FamilyID int64 `json:"family_id"`
|
||||||
|
OwnerUserID int64 `json:"owner_user_id"`
|
||||||
|
IsOwner bool `json:"is_owner"`
|
||||||
|
MemberCount int64 `json:"member_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type userSummary struct {
|
||||||
|
User userRow `json:"user"`
|
||||||
|
AuthMethods []authMethod `json:"auth_methods"`
|
||||||
|
Devices []deviceInfo `json:"devices"`
|
||||||
|
Subscriptions []subscribeInfo `json:"subscriptions"`
|
||||||
|
Family *familyInfo `json:"family,omitempty"`
|
||||||
|
OrderCount int64 `json:"order_count"`
|
||||||
|
TicketCount int64 `json:"ticket_count"`
|
||||||
|
TrafficLogCount int64 `json:"traffic_log_count"`
|
||||||
|
SystemLogCount int64 `json:"system_log_count"`
|
||||||
|
WithdrawalCount int64 `json:"withdrawal_count"`
|
||||||
|
IAPTransactionCount int64 `json:"iap_transaction_count"`
|
||||||
|
LogMessageCount int64 `json:"log_message_count"`
|
||||||
|
OnlineRecordCount int64 `json:"online_record_count"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type deleteResult struct {
|
||||||
|
UserID int64 `json:"user_id"`
|
||||||
|
DeletedDBRows []string `json:"deleted_db_rows"`
|
||||||
|
DeletedRedisKeys int `json:"deleted_redis_keys"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func must(err error) {
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
ctx := context.Background()
|
||||||
|
cfg := loadConfig("/Users/Apple/code_vpn/vpn/ppanel-server/etc/ppanel.yaml")
|
||||||
|
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s?%s", cfg.MySQL.Username, cfg.MySQL.Password, cfg.MySQL.Addr, cfg.MySQL.Dbname, cfg.MySQL.Config)
|
||||||
|
db, err := sql.Open("mysql", dsn)
|
||||||
|
must(err)
|
||||||
|
defer db.Close()
|
||||||
|
must(db.PingContext(ctx))
|
||||||
|
|
||||||
|
rdb := redis.NewClient(&redis.Options{
|
||||||
|
Addr: cfg.Redis.Host,
|
||||||
|
Password: cfg.Redis.Pass,
|
||||||
|
DB: cfg.Redis.DB,
|
||||||
|
})
|
||||||
|
defer rdb.Close()
|
||||||
|
must(rdb.Ping(ctx).Err())
|
||||||
|
|
||||||
|
targetUserIDs, err := findTargetUsers(ctx, db)
|
||||||
|
must(err)
|
||||||
|
if len(targetUserIDs) == 0 {
|
||||||
|
fmt.Println(`{"matched_users":[],"deleted":[]}`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
summaries := make([]userSummary, 0, len(targetUserIDs))
|
||||||
|
for _, userID := range targetUserIDs {
|
||||||
|
summary, sumErr := collectSummary(ctx, db, userID)
|
||||||
|
must(sumErr)
|
||||||
|
summaries = append(summaries, summary)
|
||||||
|
}
|
||||||
|
|
||||||
|
before, err := json.MarshalIndent(map[string]interface{}{
|
||||||
|
"matched_users": summaries,
|
||||||
|
}, "", " ")
|
||||||
|
must(err)
|
||||||
|
fmt.Println(string(before))
|
||||||
|
|
||||||
|
results := make([]deleteResult, 0, len(targetUserIDs))
|
||||||
|
for _, summary := range summaries {
|
||||||
|
result, delErr := deleteUser(ctx, db, rdb, summary)
|
||||||
|
must(delErr)
|
||||||
|
results = append(results, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
after, err := json.MarshalIndent(map[string]interface{}{
|
||||||
|
"deleted": results,
|
||||||
|
}, "", " ")
|
||||||
|
must(err)
|
||||||
|
fmt.Println(string(after))
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadConfig(path string) appConfig {
|
||||||
|
content, err := os.ReadFile(path)
|
||||||
|
must(err)
|
||||||
|
var cfg appConfig
|
||||||
|
must(yaml.Unmarshal(content, &cfg))
|
||||||
|
return cfg
|
||||||
|
}
|
||||||
|
|
||||||
|
func findTargetUsers(ctx context.Context, db *sql.DB) ([]int64, error) {
|
||||||
|
rows, err := db.QueryContext(ctx, `
|
||||||
|
SELECT DISTINCT user_id
|
||||||
|
FROM user_device
|
||||||
|
WHERE user_agent LIKE ?
|
||||||
|
ORDER BY user_id ASC
|
||||||
|
`, "%999%")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var ids []int64
|
||||||
|
for rows.Next() {
|
||||||
|
var id int64
|
||||||
|
if err := rows.Scan(&id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ids = append(ids, id)
|
||||||
|
}
|
||||||
|
return ids, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func collectSummary(ctx context.Context, db *sql.DB, userID int64) (userSummary, error) {
|
||||||
|
var summary userSummary
|
||||||
|
summary.User.ID = userID
|
||||||
|
|
||||||
|
err := db.QueryRowContext(ctx, `
|
||||||
|
SELECT id, refer_code, balance, commission, gift_amount, enable, is_admin, valid_email, member_status, created_at, deleted_at
|
||||||
|
FROM user
|
||||||
|
WHERE id = ?
|
||||||
|
`, userID).Scan(
|
||||||
|
&summary.User.ID,
|
||||||
|
&summary.User.ReferCode,
|
||||||
|
&summary.User.Balance,
|
||||||
|
&summary.User.Commission,
|
||||||
|
&summary.User.GiftAmount,
|
||||||
|
&summary.User.Enable,
|
||||||
|
&summary.User.IsAdmin,
|
||||||
|
&summary.User.ValidEmail,
|
||||||
|
&summary.User.MemberStatus,
|
||||||
|
&summary.User.CreatedAt,
|
||||||
|
&summary.User.DeletedAt,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
|
||||||
|
summary.AuthMethods, err = queryAuthMethods(ctx, db, userID)
|
||||||
|
if err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
summary.Devices, err = queryDevices(ctx, db, userID)
|
||||||
|
if err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
summary.Subscriptions, err = querySubscriptions(ctx, db, userID)
|
||||||
|
if err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
summary.Family, err = queryFamily(ctx, db, userID)
|
||||||
|
if err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
if summary.OrderCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM `order` WHERE user_id = ?", userID); err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
if summary.TicketCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM ticket WHERE user_id = ?", userID); err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
if summary.TrafficLogCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM traffic_log WHERE user_id = ?", userID); err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
if summary.SystemLogCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM system_logs WHERE object_id = ?", userID); err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
if summary.WithdrawalCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM user_withdrawal WHERE user_id = ?", userID); err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
if summary.IAPTransactionCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM apple_iap_transactions WHERE user_id = ?", userID); err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
if summary.LogMessageCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM log_message WHERE user_id = ?", userID); err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
if summary.OnlineRecordCount, err = queryCount(ctx, db, "SELECT COUNT(*) FROM user_device_online_record WHERE user_id = ?", userID); err != nil {
|
||||||
|
return summary, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryAuthMethods(ctx context.Context, db *sql.DB, userID int64) ([]authMethod, error) {
|
||||||
|
rows, err := db.QueryContext(ctx, `
|
||||||
|
SELECT id, user_id, auth_type, auth_identifier, verified, created_at
|
||||||
|
FROM user_auth_methods
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY id ASC
|
||||||
|
`, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var items []authMethod
|
||||||
|
for rows.Next() {
|
||||||
|
var item authMethod
|
||||||
|
if err := rows.Scan(&item.ID, &item.UserID, &item.AuthType, &item.Identifier, &item.Verified, &item.CreatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
return items, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryDevices(ctx context.Context, db *sql.DB, userID int64) ([]deviceInfo, error) {
|
||||||
|
rows, err := db.QueryContext(ctx, `
|
||||||
|
SELECT id, user_id, ip, user_agent, identifier, short_code, online, enabled, created_at
|
||||||
|
FROM user_device
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY id ASC
|
||||||
|
`, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var items []deviceInfo
|
||||||
|
for rows.Next() {
|
||||||
|
var item deviceInfo
|
||||||
|
if err := rows.Scan(&item.ID, &item.UserID, &item.IP, &item.UserAgent, &item.Identifier, &item.ShortCode, &item.Online, &item.Enabled, &item.CreatedAt); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
return items, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func querySubscriptions(ctx context.Context, db *sql.DB, userID int64) ([]subscribeInfo, error) {
|
||||||
|
rows, err := db.QueryContext(ctx, `
|
||||||
|
SELECT id, user_id, order_id, subscribe_id, token, uuid, status, start_time, expire_time
|
||||||
|
FROM user_subscribe
|
||||||
|
WHERE user_id = ?
|
||||||
|
ORDER BY id ASC
|
||||||
|
`, userID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
var items []subscribeInfo
|
||||||
|
for rows.Next() {
|
||||||
|
var item subscribeInfo
|
||||||
|
if err := rows.Scan(&item.ID, &item.UserID, &item.OrderID, &item.SubscribeID, &item.Token, &item.UUID, &item.Status, &item.StartTime, &item.ExpireTime); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items = append(items, item)
|
||||||
|
}
|
||||||
|
return items, rows.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryFamily(ctx context.Context, db *sql.DB, userID int64) (*familyInfo, error) {
|
||||||
|
var info familyInfo
|
||||||
|
err := db.QueryRowContext(ctx, `
|
||||||
|
SELECT ufm.family_id, uf.owner_user_id
|
||||||
|
FROM user_family_member ufm
|
||||||
|
JOIN user_family uf ON uf.id = ufm.family_id AND uf.deleted_at IS NULL
|
||||||
|
WHERE ufm.user_id = ? AND ufm.deleted_at IS NULL
|
||||||
|
LIMIT 1
|
||||||
|
`, userID).Scan(&info.FamilyID, &info.OwnerUserID)
|
||||||
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
info.IsOwner = info.OwnerUserID == userID
|
||||||
|
memberCount, err := queryCount(ctx, db, `
|
||||||
|
SELECT COUNT(*)
|
||||||
|
FROM user_family_member
|
||||||
|
WHERE family_id = ? AND deleted_at IS NULL
|
||||||
|
`, info.FamilyID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
info.MemberCount = memberCount
|
||||||
|
return &info, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func queryCount(ctx context.Context, db *sql.DB, q string, arg interface{}) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
err := db.QueryRowContext(ctx, q, arg).Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteUser(ctx context.Context, db *sql.DB, rdb *redis.Client, summary userSummary) (deleteResult, error) {
|
||||||
|
result := deleteResult{UserID: summary.User.ID}
|
||||||
|
|
||||||
|
tx, err := db.BeginTx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
defer tx.Rollback()
|
||||||
|
|
||||||
|
if summary.Family != nil {
|
||||||
|
if summary.Family.IsOwner {
|
||||||
|
if res, err := tx.ExecContext(ctx, `DELETE FROM user_family_member WHERE family_id = ?`, summary.Family.FamilyID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else {
|
||||||
|
result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_family_member=%d", rowsAffected(res)))
|
||||||
|
}
|
||||||
|
if res, err := tx.ExecContext(ctx, `DELETE FROM user_family WHERE id = ?`, summary.Family.FamilyID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else {
|
||||||
|
result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_family=%d", rowsAffected(res)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if res, err := tx.ExecContext(ctx, `DELETE FROM user_family_member WHERE user_id = ? AND family_id = ?`, summary.User.ID, summary.Family.FamilyID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else {
|
||||||
|
result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_family_member=%d", rowsAffected(res)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if res, err := tx.ExecContext(ctx, `DELETE FROM user_auth_methods WHERE user_id = ?`, summary.User.ID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_auth_methods=%d", rowsAffected(res))) }
|
||||||
|
|
||||||
|
if res, err := tx.ExecContext(ctx, `DELETE FROM user_subscribe WHERE user_id = ?`, summary.User.ID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_subscribe=%d", rowsAffected(res))) }
|
||||||
|
|
||||||
|
if res, err := tx.ExecContext(ctx, `DELETE FROM user_device WHERE user_id = ?`, summary.User.ID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_device=%d", rowsAffected(res))) }
|
||||||
|
|
||||||
|
if res, err := tx.ExecContext(ctx, `DELETE FROM user_device_online_record WHERE user_id = ?`, summary.User.ID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_device_online_record=%d", rowsAffected(res))) }
|
||||||
|
|
||||||
|
if res, err := tx.ExecContext(ctx, `DELETE FROM user_withdrawal WHERE user_id = ?`, summary.User.ID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user_withdrawal=%d", rowsAffected(res))) }
|
||||||
|
|
||||||
|
if res, err := tx.ExecContext(ctx, "DELETE FROM `order` WHERE user_id = ?", summary.User.ID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("order=%d", rowsAffected(res))) }
|
||||||
|
|
||||||
|
if res, err := tx.ExecContext(ctx, `DELETE FROM traffic_log WHERE user_id = ?`, summary.User.ID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("traffic_log=%d", rowsAffected(res))) }
|
||||||
|
|
||||||
|
if res, err := tx.ExecContext(ctx, `DELETE FROM system_logs WHERE object_id = ?`, summary.User.ID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("system_logs=%d", rowsAffected(res))) }
|
||||||
|
|
||||||
|
var ticketIDs []int64
|
||||||
|
ticketRows, err := tx.QueryContext(ctx, `SELECT id FROM ticket WHERE user_id = ?`, summary.User.ID)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
for ticketRows.Next() {
|
||||||
|
var id int64
|
||||||
|
if err := ticketRows.Scan(&id); err != nil {
|
||||||
|
ticketRows.Close()
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
ticketIDs = append(ticketIDs, id)
|
||||||
|
}
|
||||||
|
ticketRows.Close()
|
||||||
|
|
||||||
|
if len(ticketIDs) > 0 {
|
||||||
|
holders := strings.TrimSuffix(strings.Repeat("?,", len(ticketIDs)), ",")
|
||||||
|
args := make([]interface{}, 0, len(ticketIDs))
|
||||||
|
for _, id := range ticketIDs {
|
||||||
|
args = append(args, id)
|
||||||
|
}
|
||||||
|
if res, err := tx.ExecContext(ctx, "DELETE FROM ticket_follow WHERE ticket_id IN ("+holders+")", args...); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("ticket_follow=%d", rowsAffected(res))) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if res, err := tx.ExecContext(ctx, `DELETE FROM ticket WHERE user_id = ?`, summary.User.ID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("ticket=%d", rowsAffected(res))) }
|
||||||
|
|
||||||
|
if res, err := tx.ExecContext(ctx, `DELETE FROM apple_iap_transactions WHERE user_id = ?`, summary.User.ID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("apple_iap_transactions=%d", rowsAffected(res))) }
|
||||||
|
|
||||||
|
if res, err := tx.ExecContext(ctx, `DELETE FROM log_message WHERE user_id = ?`, summary.User.ID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("log_message=%d", rowsAffected(res))) }
|
||||||
|
|
||||||
|
if res, err := tx.ExecContext(ctx, `DELETE FROM user WHERE id = ?`, summary.User.ID); err != nil {
|
||||||
|
return result, err
|
||||||
|
} else { result.DeletedDBRows = append(result.DeletedDBRows, fmt.Sprintf("user=%d", rowsAffected(res))) }
|
||||||
|
|
||||||
|
if err := tx.Commit(); err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
|
||||||
|
redisKeys, err := cleanupRedis(ctx, rdb, summary)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
result.DeletedRedisKeys = len(redisKeys)
|
||||||
|
sort.Strings(result.DeletedDBRows)
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupRedis(ctx context.Context, rdb *redis.Client, summary userSummary) ([]string, error) {
|
||||||
|
keySet := map[string]struct{}{
|
||||||
|
fmt.Sprintf("cache:user:id:%d", summary.User.ID): {},
|
||||||
|
fmt.Sprintf("cache:user:subscribe:user:%d", summary.User.ID): {},
|
||||||
|
fmt.Sprintf("cache:user:subscribe:user:%d:all", summary.User.ID): {},
|
||||||
|
fmt.Sprintf("auth:user_sessions:%d", summary.User.ID): {},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, am := range summary.AuthMethods {
|
||||||
|
if am.AuthType == "email" && am.Identifier != "" {
|
||||||
|
keySet[fmt.Sprintf("cache:user:email:%s", am.Identifier)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, sub := range summary.Subscriptions {
|
||||||
|
keySet[fmt.Sprintf("cache:user:subscribe:id:%d", sub.ID)] = struct{}{}
|
||||||
|
if sub.Token != "" {
|
||||||
|
keySet[fmt.Sprintf("cache:user:subscribe:token:%s", sub.Token)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, device := range summary.Devices {
|
||||||
|
keySet[fmt.Sprintf("cache:user:device:id:%d", device.ID)] = struct{}{}
|
||||||
|
if device.Identifier != "" {
|
||||||
|
keySet[fmt.Sprintf("cache:user:device:number:%s", device.Identifier)] = struct{}{}
|
||||||
|
keySet[fmt.Sprintf("auth:device_identifier:%s", device.Identifier)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionsKey := fmt.Sprintf("auth:user_sessions:%d", summary.User.ID)
|
||||||
|
sessionIDs, err := rdb.ZRange(ctx, sessionsKey, 0, -1).Result()
|
||||||
|
if err != nil && err != redis.Nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, sessionID := range sessionIDs {
|
||||||
|
if sessionID == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
keySet[fmt.Sprintf("auth:session_id:%s", sessionID)] = struct{}{}
|
||||||
|
keySet[fmt.Sprintf("auth:session_id:detail:%s", sessionID)] = struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var cursor uint64
|
||||||
|
for {
|
||||||
|
keys, nextCursor, scanErr := rdb.Scan(ctx, cursor, "auth:session_id:*", 200).Result()
|
||||||
|
if scanErr != nil {
|
||||||
|
return nil, scanErr
|
||||||
|
}
|
||||||
|
for _, key := range keys {
|
||||||
|
if strings.Contains(key, ":detail:") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
value, getErr := rdb.Get(ctx, key).Result()
|
||||||
|
if getErr != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if value == fmt.Sprintf("%d", summary.User.ID) {
|
||||||
|
keySet[key] = struct{}{}
|
||||||
|
sessionID := strings.TrimPrefix(key, "auth:session_id:")
|
||||||
|
if sessionID != "" {
|
||||||
|
keySet[fmt.Sprintf("auth:session_id:detail:%s", sessionID)] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cursor = nextCursor
|
||||||
|
if cursor == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
keys := make([]string, 0, len(keySet))
|
||||||
|
for key := range keySet {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
if len(keys) == 0 {
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
if err := rdb.Del(ctx, keys...).Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return keys, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func rowsAffected(res sql.Result) int64 {
|
||||||
|
if res == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
n, err := res.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
|
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/config"
|
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/pkg/tool"
|
"github.com/perfect-panel/server/pkg/tool"
|
||||||
)
|
)
|
||||||
@@ -17,7 +16,7 @@ func Register(ctx *svc.ServiceContext) {
|
|||||||
logger.Errorf("[Init Register Config] Get Register Config Error: %s", err.Error())
|
logger.Errorf("[Init Register Config] Get Register Config Error: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var registerConfig config.RegisterConfig
|
registerConfig := ctx.Config.Register
|
||||||
tool.SystemConfigSliceReflectToStruct(configs, ®isterConfig)
|
tool.SystemConfigSliceReflectToStruct(configs, ®isterConfig)
|
||||||
ctx.Config.Register = registerConfig
|
ctx.Config.Register = registerConfig
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,15 +84,15 @@ type SubscribeConfig struct {
|
|||||||
type RegisterConfig struct {
|
type RegisterConfig struct {
|
||||||
StopRegister bool `yaml:"StopRegister" default:"false"`
|
StopRegister bool `yaml:"StopRegister" default:"false"`
|
||||||
EnableTrial bool `yaml:"EnableTrial" default:"false"`
|
EnableTrial bool `yaml:"EnableTrial" default:"false"`
|
||||||
|
EnableTrialEmailWhitelist bool `yaml:"EnableTrialEmailWhitelist" default:"true"`
|
||||||
TrialSubscribe int64 `yaml:"TrialSubscribe" default:"0"`
|
TrialSubscribe int64 `yaml:"TrialSubscribe" default:"0"`
|
||||||
TrialTime int64 `yaml:"TrialTime" default:"0"`
|
TrialTime int64 `yaml:"TrialTime" default:"0"`
|
||||||
TrialTimeUnit string `yaml:"TrialTimeUnit" default:""`
|
TrialTimeUnit string `yaml:"TrialTimeUnit" default:""`
|
||||||
|
TrialEmailDomainWhitelist string `yaml:"TrialEmailDomainWhitelist" default:""`
|
||||||
IpRegisterLimit int64 `yaml:"IpRegisterLimit" default:"0"`
|
IpRegisterLimit int64 `yaml:"IpRegisterLimit" default:"0"`
|
||||||
IpRegisterLimitDuration int64 `yaml:"IpRegisterLimitDuration" default:"0"`
|
IpRegisterLimitDuration int64 `yaml:"IpRegisterLimitDuration" default:"0"`
|
||||||
EnableIpRegisterLimit bool `yaml:"EnableIpRegisterLimit" default:"false"`
|
EnableIpRegisterLimit bool `yaml:"EnableIpRegisterLimit" default:"false"`
|
||||||
DeviceLimit int64 `yaml:"DeviceLimit" default:"2"`
|
DeviceLimit int64 `yaml:"DeviceLimit" default:"2"`
|
||||||
EnableTrialEmailWhitelist bool `yaml:"EnableTrialEmailWhitelist" default:"false"`
|
|
||||||
TrialEmailDomainWhitelist string `yaml:"TrialEmailDomainWhitelist" default:""`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type EmailConfig struct {
|
type EmailConfig struct {
|
||||||
|
|||||||
@@ -26,7 +26,19 @@ func NewGetRegisterConfigLogic(ctx context.Context, svcCtx *svc.ServiceContext)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *GetRegisterConfigLogic) GetRegisterConfig() (*types.RegisterConfig, error) {
|
func (l *GetRegisterConfigLogic) GetRegisterConfig() (*types.RegisterConfig, error) {
|
||||||
resp := &types.RegisterConfig{}
|
resp := &types.RegisterConfig{
|
||||||
|
StopRegister: l.svcCtx.Config.Register.StopRegister,
|
||||||
|
EnableTrial: l.svcCtx.Config.Register.EnableTrial,
|
||||||
|
EnableTrialEmailWhitelist: l.svcCtx.Config.Register.EnableTrialEmailWhitelist,
|
||||||
|
TrialSubscribe: l.svcCtx.Config.Register.TrialSubscribe,
|
||||||
|
TrialTime: l.svcCtx.Config.Register.TrialTime,
|
||||||
|
TrialTimeUnit: l.svcCtx.Config.Register.TrialTimeUnit,
|
||||||
|
TrialEmailDomainWhitelist: l.svcCtx.Config.Register.TrialEmailDomainWhitelist,
|
||||||
|
EnableIpRegisterLimit: l.svcCtx.Config.Register.EnableIpRegisterLimit,
|
||||||
|
IpRegisterLimit: l.svcCtx.Config.Register.IpRegisterLimit,
|
||||||
|
IpRegisterLimitDuration: l.svcCtx.Config.Register.IpRegisterLimitDuration,
|
||||||
|
DeviceLimit: l.svcCtx.Config.Register.DeviceLimit,
|
||||||
|
}
|
||||||
|
|
||||||
// get register config from database
|
// get register config from database
|
||||||
configs, err := l.svcCtx.SystemModel.GetRegisterConfig(l.ctx)
|
configs, err := l.svcCtx.SystemModel.GetRegisterConfig(l.ctx)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
|
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/initialize"
|
"github.com/perfect-panel/server/initialize"
|
||||||
"github.com/perfect-panel/server/internal/config"
|
"github.com/perfect-panel/server/internal/config"
|
||||||
@@ -44,8 +45,28 @@ func (l *UpdateRegisterConfigLogic) UpdateRegisterConfig(req *types.RegisterConf
|
|||||||
fieldName := t.Field(i).Name
|
fieldName := t.Field(i).Name
|
||||||
// Get the field value to string
|
// Get the field value to string
|
||||||
fieldValue := tool.ConvertValueToString(v.Field(i))
|
fieldValue := tool.ConvertValueToString(v.Field(i))
|
||||||
// Update the site config
|
|
||||||
err = db.Model(&system.System{}).Where("`category` = 'register' and `key` = ?", fieldName).Update("value", fieldValue).Error
|
var existing system.System
|
||||||
|
queryErr := db.Where("`category` = ? and `key` = ?", "register", fieldName).First(&existing).Error
|
||||||
|
if queryErr != nil && !errors.Is(queryErr, gorm.ErrRecordNotFound) {
|
||||||
|
return queryErr
|
||||||
|
}
|
||||||
|
if errors.Is(queryErr, gorm.ErrRecordNotFound) {
|
||||||
|
fieldValue = l.defaultRegisterFieldValue(fieldName, fieldValue)
|
||||||
|
}
|
||||||
|
|
||||||
|
record := &system.System{
|
||||||
|
Category: "register",
|
||||||
|
Key: fieldName,
|
||||||
|
}
|
||||||
|
assignments := map[string]interface{}{
|
||||||
|
"value": fieldValue,
|
||||||
|
"type": inferRegisterSystemValueType(t.Field(i).Type.Kind()),
|
||||||
|
"desc": fieldName,
|
||||||
|
}
|
||||||
|
err = db.Where("`category` = ? and `key` = ?", "register", fieldName).
|
||||||
|
Assign(assignments).
|
||||||
|
FirstOrCreate(record).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -63,3 +84,28 @@ func (l *UpdateRegisterConfigLogic) UpdateRegisterConfig(req *types.RegisterConf
|
|||||||
initialize.Register(l.svcCtx)
|
initialize.Register(l.svcCtx)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func inferRegisterSystemValueType(kind reflect.Kind) string {
|
||||||
|
switch kind {
|
||||||
|
case reflect.Bool:
|
||||||
|
return "bool"
|
||||||
|
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||||
|
return "int"
|
||||||
|
default:
|
||||||
|
return "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *UpdateRegisterConfigLogic) defaultRegisterFieldValue(fieldName, fieldValue string) string {
|
||||||
|
switch fieldName {
|
||||||
|
case "EnableTrialEmailWhitelist":
|
||||||
|
return "true"
|
||||||
|
case "TrialEmailDomainWhitelist":
|
||||||
|
if strings.TrimSpace(fieldValue) != "" {
|
||||||
|
return fieldValue
|
||||||
|
}
|
||||||
|
return l.svcCtx.Config.Register.TrialEmailDomainWhitelist
|
||||||
|
default:
|
||||||
|
return fieldValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types.
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
rc := l.svcCtx.Config.Register
|
rc := l.svcCtx.Config.Register
|
||||||
if rc.EnableTrial && (!rc.EnableTrialEmailWhitelist || IsEmailDomainWhitelisted(req.Email, rc.TrialEmailDomainWhitelist)) {
|
if ShouldGrantTrialForEmail(rc, req.Email) {
|
||||||
if err = l.activeTrial(userInfo.Id); err != nil {
|
if err = l.activeTrial(userInfo.Id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/config"
|
"github.com/perfect-panel/server/internal/config"
|
||||||
|
authlogic "github.com/perfect-panel/server/internal/logic/auth"
|
||||||
"github.com/perfect-panel/server/internal/model/auth"
|
"github.com/perfect-panel/server/internal/model/auth"
|
||||||
"github.com/perfect-panel/server/internal/model/log"
|
"github.com/perfect-panel/server/internal/model/log"
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
@@ -393,10 +395,14 @@ func (l *OAuthLoginGetTokenLogic) register(email, avatar, method, openid, reques
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if l.svcCtx.Config.Register.EnableTrial {
|
rc := l.svcCtx.Config.Register
|
||||||
|
shouldActivateTrial := email != "" && authlogic.ShouldGrantTrialForEmail(rc, email)
|
||||||
|
|
||||||
|
if shouldActivateTrial {
|
||||||
l.Debugw("activating trial subscription",
|
l.Debugw("activating trial subscription",
|
||||||
logger.Field("request_id", requestID),
|
logger.Field("request_id", requestID),
|
||||||
logger.Field("user_id", userInfo.Id),
|
logger.Field("user_id", userInfo.Id),
|
||||||
|
logger.Field("email", email),
|
||||||
)
|
)
|
||||||
var trialErr error
|
var trialErr error
|
||||||
trialSubscribe, trialErr = l.activeTrial(userInfo.Id, requestID)
|
trialSubscribe, trialErr = l.activeTrial(userInfo.Id, requestID)
|
||||||
@@ -882,3 +888,22 @@ func (l *OAuthLoginGetTokenLogic) activeTrial(uid int64, requestID string) (*use
|
|||||||
)
|
)
|
||||||
return userSub, nil
|
return userSub, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isEmailDomainWhitelisted checks if the email's domain is in the comma-separated whitelist.
|
||||||
|
// Returns false if the email format is invalid.
|
||||||
|
func (l *OAuthLoginGetTokenLogic) isEmailDomainWhitelisted(email, whitelistCSV string) bool {
|
||||||
|
if whitelistCSV == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
parts := strings.SplitN(email, "@", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
domain := strings.ToLower(strings.TrimSpace(parts[1]))
|
||||||
|
for _, d := range strings.Split(whitelistCSV, ",") {
|
||||||
|
if strings.ToLower(strings.TrimSpace(d)) == domain {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import "strings"
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/perfect-panel/server/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
// IsEmailDomainWhitelisted checks if the email's domain is in the comma-separated whitelist.
|
// IsEmailDomainWhitelisted checks if the email's domain is in the comma-separated whitelist.
|
||||||
// Returns false if the email format is invalid.
|
// Returns false if the email format is invalid.
|
||||||
@@ -20,3 +24,16 @@ func IsEmailDomainWhitelisted(email, whitelistCSV string) bool {
|
|||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ShouldGrantTrialForEmail(register config.RegisterConfig, email string) bool {
|
||||||
|
if !register.EnableTrial {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !register.EnableTrialEmailWhitelist {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if register.TrialEmailDomainWhitelist == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return IsEmailDomainWhitelisted(email, register.TrialEmailDomainWhitelist)
|
||||||
|
}
|
||||||
|
|||||||
@@ -148,7 +148,7 @@ func (l *UserRegisterLogic) UserRegister(req *types.UserRegisterRequest) (resp *
|
|||||||
|
|
||||||
// Activate trial subscription after transaction success (moved outside transaction to reduce lock time)
|
// Activate trial subscription after transaction success (moved outside transaction to reduce lock time)
|
||||||
rc := l.svcCtx.Config.Register
|
rc := l.svcCtx.Config.Register
|
||||||
if rc.EnableTrial && (!rc.EnableTrialEmailWhitelist || IsEmailDomainWhitelisted(req.Email, rc.TrialEmailDomainWhitelist)) {
|
if ShouldGrantTrialForEmail(rc, req.Email) {
|
||||||
trialSubscribe, err = l.activeTrial(userInfo.Id)
|
trialSubscribe, err = l.activeTrial(userInfo.Id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Errorw("Failed to activate trial subscription", logger.Field("error", err.Error()))
|
l.Errorw("Failed to activate trial subscription", logger.Field("error", err.Error()))
|
||||||
|
|||||||
@@ -8,14 +8,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/perfect-panel/server/internal/config"
|
"github.com/perfect-panel/server/internal/config"
|
||||||
"github.com/perfect-panel/server/internal/logic/auth"
|
|
||||||
"github.com/perfect-panel/server/internal/model/user"
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
"github.com/perfect-panel/server/internal/svc"
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
"github.com/perfect-panel/server/internal/types"
|
"github.com/perfect-panel/server/internal/types"
|
||||||
"github.com/perfect-panel/server/pkg/constant"
|
"github.com/perfect-panel/server/pkg/constant"
|
||||||
"github.com/perfect-panel/server/pkg/jwt"
|
"github.com/perfect-panel/server/pkg/jwt"
|
||||||
"github.com/perfect-panel/server/pkg/logger"
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
"github.com/perfect-panel/server/pkg/tool"
|
|
||||||
"github.com/perfect-panel/server/pkg/uuidx"
|
"github.com/perfect-panel/server/pkg/uuidx"
|
||||||
"github.com/perfect-panel/server/pkg/xerr"
|
"github.com/perfect-panel/server/pkg/xerr"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
@@ -129,8 +127,7 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
// Grant trial subscription if email domain is whitelisted
|
tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, emailUser.Id, req.Email)
|
||||||
l.tryGrantTrialOnEmailBind(emailUser.Id, req.Email)
|
|
||||||
return &types.BindEmailWithVerificationResponse{
|
return &types.BindEmailWithVerificationResponse{
|
||||||
Success: true,
|
Success: true,
|
||||||
Message: "email user created and joined family",
|
Message: "email user created and joined family",
|
||||||
@@ -158,8 +155,7 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Grant trial subscription if email domain is whitelisted
|
tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, existingMethod.UserId, req.Email)
|
||||||
l.tryGrantTrialOnEmailBind(existingMethod.UserId, req.Email)
|
|
||||||
|
|
||||||
return &types.BindEmailWithVerificationResponse{
|
return &types.BindEmailWithVerificationResponse{
|
||||||
Success: true,
|
Success: true,
|
||||||
@@ -207,74 +203,3 @@ func (l *BindEmailWithVerificationLogic) refreshBindSessionToken(userId int64) (
|
|||||||
|
|
||||||
return token, nil
|
return token, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryGrantTrialOnEmailBind grants trial subscription to the email user (family owner)
|
|
||||||
// if EnableTrialEmailWhitelist is on and the email domain matches.
|
|
||||||
func (l *BindEmailWithVerificationLogic) tryGrantTrialOnEmailBind(ownerUserId int64, email string) {
|
|
||||||
rc := l.svcCtx.Config.Register
|
|
||||||
if !rc.EnableTrial || !rc.EnableTrialEmailWhitelist {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !auth.IsEmailDomainWhitelisted(email, rc.TrialEmailDomainWhitelist) {
|
|
||||||
l.Infow("email domain not in trial whitelist, skip",
|
|
||||||
logger.Field("email", email),
|
|
||||||
logger.Field("owner_user_id", ownerUserId),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Anti-duplicate: check if owner already has trial subscription
|
|
||||||
var count int64
|
|
||||||
if err := l.svcCtx.DB.WithContext(l.ctx).
|
|
||||||
Model(&user.Subscribe{}).
|
|
||||||
Where("user_id = ? AND subscribe_id = ?", ownerUserId, rc.TrialSubscribe).
|
|
||||||
Count(&count).Error; err != nil {
|
|
||||||
l.Errorw("failed to check existing trial", logger.Field("error", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if count > 0 {
|
|
||||||
l.Infow("trial already granted, skip",
|
|
||||||
logger.Field("owner_user_id", ownerUserId),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
sub, err := l.svcCtx.SubscribeModel.FindOne(l.ctx, rc.TrialSubscribe)
|
|
||||||
if err != nil {
|
|
||||||
l.Errorw("failed to find trial subscribe template", logger.Field("error", err.Error()))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
userSub := &user.Subscribe{
|
|
||||||
UserId: ownerUserId,
|
|
||||||
OrderId: 0,
|
|
||||||
SubscribeId: sub.Id,
|
|
||||||
StartTime: time.Now(),
|
|
||||||
ExpireTime: tool.AddTime(rc.TrialTimeUnit, rc.TrialTime, time.Now()),
|
|
||||||
Traffic: sub.Traffic,
|
|
||||||
Download: 0,
|
|
||||||
Upload: 0,
|
|
||||||
Token: uuidx.NewUUID().String(),
|
|
||||||
UUID: uuidx.NewUUID().String(),
|
|
||||||
Status: 1,
|
|
||||||
}
|
|
||||||
if err = l.svcCtx.UserModel.InsertSubscribe(l.ctx, userSub); err != nil {
|
|
||||||
l.Errorw("failed to insert trial subscribe",
|
|
||||||
logger.Field("error", err.Error()),
|
|
||||||
logger.Field("owner_user_id", ownerUserId),
|
|
||||||
)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertSubscribe auto-clears user subscribe cache via execSubscribeMutation.
|
|
||||||
// Clear server cache so nodes pick up the new subscription.
|
|
||||||
if err = l.svcCtx.NodeModel.ClearServerAllCache(l.ctx); err != nil {
|
|
||||||
l.Errorw("ClearServerAllCache error", logger.Field("error", err.Error()))
|
|
||||||
}
|
|
||||||
|
|
||||||
l.Infow("trial granted on email bind",
|
|
||||||
logger.Field("owner_user_id", ownerUserId),
|
|
||||||
logger.Field("email", email),
|
|
||||||
logger.Field("subscribe_id", sub.Id),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package user
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/perfect-panel/server/internal/logic/auth"
|
||||||
|
"github.com/perfect-panel/server/internal/model/user"
|
||||||
|
"github.com/perfect-panel/server/internal/svc"
|
||||||
|
"github.com/perfect-panel/server/pkg/logger"
|
||||||
|
"github.com/perfect-panel/server/pkg/tool"
|
||||||
|
"github.com/perfect-panel/server/pkg/uuidx"
|
||||||
|
)
|
||||||
|
|
||||||
|
func tryGrantTrialOnEmailBind(ctx context.Context, svcCtx *svc.ServiceContext, log logger.Logger, ownerUserId int64, email string) {
|
||||||
|
rc := svcCtx.Config.Register
|
||||||
|
if !auth.ShouldGrantTrialForEmail(rc, email) {
|
||||||
|
if rc.EnableTrial && rc.EnableTrialEmailWhitelist {
|
||||||
|
log.Infow("email domain not in trial whitelist, skip",
|
||||||
|
logger.Field("email", email),
|
||||||
|
logger.Field("owner_user_id", ownerUserId),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
if err := svcCtx.DB.WithContext(ctx).
|
||||||
|
Model(&user.Subscribe{}).
|
||||||
|
Where("user_id = ? AND subscribe_id = ?", ownerUserId, rc.TrialSubscribe).
|
||||||
|
Count(&count).Error; err != nil {
|
||||||
|
log.Errorw("failed to check existing trial", logger.Field("error", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
log.Infow("trial already granted, skip",
|
||||||
|
logger.Field("owner_user_id", ownerUserId),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, err := svcCtx.SubscribeModel.FindOne(ctx, rc.TrialSubscribe)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorw("failed to find trial subscribe template", logger.Field("error", err.Error()))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userSub := &user.Subscribe{
|
||||||
|
UserId: ownerUserId,
|
||||||
|
OrderId: 0,
|
||||||
|
SubscribeId: sub.Id,
|
||||||
|
StartTime: time.Now(),
|
||||||
|
ExpireTime: tool.AddTime(rc.TrialTimeUnit, rc.TrialTime, time.Now()),
|
||||||
|
Traffic: sub.Traffic,
|
||||||
|
Download: 0,
|
||||||
|
Upload: 0,
|
||||||
|
Token: uuidx.NewUUID().String(),
|
||||||
|
UUID: uuidx.NewUUID().String(),
|
||||||
|
Status: 1,
|
||||||
|
}
|
||||||
|
if err = svcCtx.UserModel.InsertSubscribe(ctx, userSub); err != nil {
|
||||||
|
log.Errorw("failed to insert trial subscribe",
|
||||||
|
logger.Field("error", err.Error()),
|
||||||
|
logger.Field("owner_user_id", ownerUserId),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if svcCtx.NodeModel != nil {
|
||||||
|
if err = svcCtx.NodeModel.ClearServerAllCache(ctx); err != nil {
|
||||||
|
log.Errorw("ClearServerAllCache error", logger.Field("error", err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infow("trial granted on email bind",
|
||||||
|
logger.Field("owner_user_id", ownerUserId),
|
||||||
|
logger.Field("email", email),
|
||||||
|
logger.Field("subscribe_id", sub.Id),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -77,5 +77,6 @@ func (l *VerifyEmailLogic) VerifyEmail(req *types.VerifyEmailRequest) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateUserAuthMethods error")
|
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateUserAuthMethods error")
|
||||||
}
|
}
|
||||||
|
tryGrantTrialOnEmailBind(l.ctx, l.svcCtx, l.Logger, method.UserId, req.Email)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2269,9 +2269,11 @@ type RedemptionRecord struct {
|
|||||||
type RegisterConfig struct {
|
type RegisterConfig struct {
|
||||||
StopRegister bool `json:"stop_register"`
|
StopRegister bool `json:"stop_register"`
|
||||||
EnableTrial bool `json:"enable_trial"`
|
EnableTrial bool `json:"enable_trial"`
|
||||||
|
EnableTrialEmailWhitelist bool `json:"enable_trial_email_whitelist"`
|
||||||
TrialSubscribe int64 `json:"trial_subscribe"`
|
TrialSubscribe int64 `json:"trial_subscribe"`
|
||||||
TrialTime int64 `json:"trial_time"`
|
TrialTime int64 `json:"trial_time"`
|
||||||
TrialTimeUnit string `json:"trial_time_unit"`
|
TrialTimeUnit string `json:"trial_time_unit"`
|
||||||
|
TrialEmailDomainWhitelist string `json:"trial_email_domain_whitelist"`
|
||||||
EnableIpRegisterLimit bool `json:"enable_ip_register_limit"`
|
EnableIpRegisterLimit bool `json:"enable_ip_register_limit"`
|
||||||
IpRegisterLimit int64 `json:"ip_register_limit"`
|
IpRegisterLimit int64 `json:"ip_register_limit"`
|
||||||
IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"`
|
IpRegisterLimitDuration int64 `json:"ip_register_limit_duration"`
|
||||||
|
|||||||
Reference in New Issue
Block a user