176 lines
6.0 KiB
Go
176 lines
6.0 KiB
Go
package auth
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"strings"
|
|
"time"
|
|
|
|
"hyapp/pkg/appcode"
|
|
authdomain "hyapp/services/user-service/internal/domain/auth"
|
|
)
|
|
|
|
func (r *Repository) CreateLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error {
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO login_ip_risk_jobs (
|
|
app_code, job_id, session_id, user_id, client_ip, client_ip_hash, channel, platform, language, timezone,
|
|
request_id, edge_country_code, status, decision, country_code, failure_reason, locked_by, locked_until_ms,
|
|
attempt_count, created_at_ms, updated_at_ms
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, '', '', '', NULL, NULL, 0, ?, ?)
|
|
`, appcode.Normalize(job.AppCode), job.JobID, job.SessionID, job.UserID, job.ClientIP, job.ClientIPHash, job.Channel, job.Platform, job.Language, job.Timezone, job.RequestID, job.EdgeCountryCode, job.Status, job.CreatedAtMs, job.UpdatedAtMs)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) ClaimLoginIPRiskJobs(ctx context.Context, workerID string, nowMs int64, lockTTL time.Duration, batchSize int) ([]authdomain.LoginIPRiskJob, error) {
|
|
if batchSize <= 0 {
|
|
batchSize = 100
|
|
}
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT app_code, job_id, session_id, user_id, client_ip, client_ip_hash, channel, platform, language, timezone,
|
|
request_id, edge_country_code, status, decision, country_code, failure_reason,
|
|
COALESCE(locked_by, ''), COALESCE(locked_until_ms, 0), attempt_count, created_at_ms, updated_at_ms
|
|
FROM login_ip_risk_jobs
|
|
WHERE app_code = ?
|
|
AND status IN (?, ?)
|
|
AND (locked_until_ms IS NULL OR locked_until_ms <= ?)
|
|
ORDER BY updated_at_ms ASC, job_id ASC
|
|
LIMIT ?
|
|
`, appcode.FromContext(ctx), authdomain.LoginIPRiskStatusPending, authdomain.LoginIPRiskStatusFailed, nowMs, batchSize)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
candidates := make([]authdomain.LoginIPRiskJob, 0, batchSize)
|
|
for rows.Next() {
|
|
job, err := scanLoginIPRiskJob(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
candidates = append(candidates, job)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
claimed := make([]authdomain.LoginIPRiskJob, 0, len(candidates))
|
|
lockUntilMs := nowMs + lockTTL.Milliseconds()
|
|
for _, job := range candidates {
|
|
result, err := r.db.ExecContext(ctx, `
|
|
UPDATE login_ip_risk_jobs
|
|
SET status = ?, locked_by = ?, locked_until_ms = ?, attempt_count = attempt_count + 1, updated_at_ms = ?
|
|
WHERE app_code = ? AND job_id = ?
|
|
AND status IN (?, ?)
|
|
AND (locked_until_ms IS NULL OR locked_until_ms <= ?)
|
|
`, authdomain.LoginIPRiskStatusRunning, workerID, lockUntilMs, nowMs, job.AppCode, job.JobID, authdomain.LoginIPRiskStatusPending, authdomain.LoginIPRiskStatusFailed, nowMs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
affected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if affected == 1 {
|
|
job.Status = authdomain.LoginIPRiskStatusRunning
|
|
job.LockedBy = workerID
|
|
job.LockedUntilMs = lockUntilMs
|
|
job.AttemptCount++
|
|
job.UpdatedAtMs = nowMs
|
|
claimed = append(claimed, job)
|
|
}
|
|
}
|
|
|
|
return claimed, nil
|
|
}
|
|
|
|
func (r *Repository) CompleteLoginIPRiskJob(ctx context.Context, job authdomain.LoginIPRiskJob) error {
|
|
_, err := r.db.ExecContext(ctx, `
|
|
UPDATE login_ip_risk_jobs
|
|
SET status = ?, decision = ?, country_code = ?, failure_reason = ?, locked_by = NULL, locked_until_ms = NULL, updated_at_ms = ?
|
|
WHERE app_code = ? AND job_id = ?
|
|
`, job.Status, job.Decision, job.CountryCode, job.FailureReason, job.UpdatedAtMs, appcode.Normalize(job.AppCode), job.JobID)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) CreateLoginIPRiskDecision(ctx context.Context, decision authdomain.LoginIPRiskDecision) error {
|
|
_, err := r.db.ExecContext(ctx, `
|
|
INSERT INTO login_ip_risk_decisions (
|
|
app_code, decision_id, client_ip, client_ip_hash, decision, country_code, confidence,
|
|
provider_attempts_json, decided_at_ms, expires_at_ms, created_at_ms
|
|
)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, CAST(? AS JSON), ?, ?, ?)
|
|
`, appcode.Normalize(decision.AppCode), decision.DecisionID, decision.ClientIP, decision.ClientIPHash, decision.Decision, decision.CountryCode, decision.Confidence, decision.ProviderAttemptsJSON, decision.DecidedAtMs, decision.ExpiresAtMs, decision.CreatedAtMs)
|
|
return err
|
|
}
|
|
|
|
func (r *Repository) ListEnabledLoginRiskCountryBlocks(ctx context.Context) ([]authdomain.LoginRiskCountryBlock, error) {
|
|
rows, err := r.db.QueryContext(ctx, `
|
|
SELECT app_code, block_id, keyword, country_code, enabled, created_at_ms, updated_at_ms
|
|
FROM login_risk_country_blocks
|
|
WHERE app_code = ? AND enabled = 1
|
|
ORDER BY country_code ASC, keyword ASC, block_id ASC
|
|
`, appcode.FromContext(ctx))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
blocks := make([]authdomain.LoginRiskCountryBlock, 0)
|
|
for rows.Next() {
|
|
var block authdomain.LoginRiskCountryBlock
|
|
if err := rows.Scan(&block.AppCode, &block.BlockID, &block.Keyword, &block.CountryCode, &block.Enabled, &block.CreatedAtMs, &block.UpdatedAtMs); err != nil {
|
|
return nil, err
|
|
}
|
|
block.CountryCode = normalizeCountryCode(block.CountryCode)
|
|
blocks = append(blocks, block)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return blocks, nil
|
|
}
|
|
|
|
type loginIPRiskJobScanner interface {
|
|
Scan(dest ...any) error
|
|
}
|
|
|
|
func normalizeCountryCode(country string) string {
|
|
country = strings.ToUpper(strings.TrimSpace(country))
|
|
if len(country) < 2 || len(country) > 3 {
|
|
return ""
|
|
}
|
|
return country
|
|
}
|
|
|
|
func scanLoginIPRiskJob(scanner loginIPRiskJobScanner) (authdomain.LoginIPRiskJob, error) {
|
|
var job authdomain.LoginIPRiskJob
|
|
err := scanner.Scan(
|
|
&job.AppCode,
|
|
&job.JobID,
|
|
&job.SessionID,
|
|
&job.UserID,
|
|
&job.ClientIP,
|
|
&job.ClientIPHash,
|
|
&job.Channel,
|
|
&job.Platform,
|
|
&job.Language,
|
|
&job.Timezone,
|
|
&job.RequestID,
|
|
&job.EdgeCountryCode,
|
|
&job.Status,
|
|
&job.Decision,
|
|
&job.CountryCode,
|
|
&job.FailureReason,
|
|
&job.LockedBy,
|
|
&job.LockedUntilMs,
|
|
&job.AttemptCount,
|
|
&job.CreatedAtMs,
|
|
&job.UpdatedAtMs,
|
|
)
|
|
if err == sql.ErrNoRows {
|
|
return authdomain.LoginIPRiskJob{}, err
|
|
}
|
|
return job, err
|
|
}
|