feat(auth): implement user and device authentication

This commit is contained in:
QiuSW
2026-07-26 15:18:48 +08:00
parent c5d3b215ff
commit 49db5b8305
66 changed files with 6216 additions and 271 deletions
+45 -24
View File
@@ -29,6 +29,7 @@ type TaskService struct {
type CreateTaskCommand struct {
CreatorSubject string
ActorUserID string
IdempotencyKey string
SourceRef *string
Title string
@@ -61,6 +62,7 @@ type TaskPage struct {
type CancelTaskCommand struct {
CreatorSubject string
ActorUserID string
TaskID string
Reason string
}
@@ -91,6 +93,7 @@ func (s *TaskService) Create(
return CreateTaskResult{}, err
}
command.CreatorSubject = strings.TrimSpace(command.CreatorSubject)
command.ActorUserID = strings.TrimSpace(command.ActorUserID)
command.Title = strings.TrimSpace(command.Title)
command.SKU = strings.TrimSpace(command.SKU)
command.ImageAssetID = strings.TrimSpace(command.ImageAssetID)
@@ -105,6 +108,13 @@ func (s *TaskService) Create(
map[string]string{"image_asset_id": "must be a UUID"},
)
}
if !isUUID(command.ActorUserID) {
return CreateTaskResult{}, invalidError(
"TASK_VALIDATION_FAILED",
"task validation failed",
map[string]string{"actor_user_id": "must be a UUID"},
)
}
if err := domain.ValidateTaskInput(
command.CreatorSubject,
command.SourceRef,
@@ -156,28 +166,31 @@ func (s *TaskService) Create(
)
}
now := s.clock.Now().UTC()
actorUserID := command.ActorUserID
task := domain.PurchaseTask{
ID: taskID,
CreatorSubject: command.CreatorSubject,
SourceRef: command.SourceRef,
Title: command.Title,
Description: command.Description,
SKU: command.SKU,
ImageAssetID: command.ImageAssetID,
Quantity: command.Quantity,
MaxBudgetCents: budget,
Currency: domain.CurrencyCNY,
Status: domain.TaskStatusPending,
Version: 1,
CreatedAt: now,
UpdatedAt: now,
ID: taskID,
CreatorSubject: command.CreatorSubject,
CreatedByUserID: &actorUserID,
SourceRef: command.SourceRef,
Title: command.Title,
Description: command.Description,
SKU: command.SKU,
ImageAssetID: command.ImageAssetID,
Quantity: command.Quantity,
MaxBudgetCents: budget,
Currency: domain.CurrencyCNY,
Status: domain.TaskStatusPending,
Version: 1,
CreatedAt: now,
UpdatedAt: now,
}
event := domain.TaskEvent{
ID: eventID,
TaskID: taskID,
Type: "TASK_CREATED",
Message: "task created",
OccurredAt: now,
ID: eventID,
TaskID: taskID,
ActorUserID: &actorUserID,
Type: "TASK_CREATED",
Message: "task created",
OccurredAt: now,
}
requestHash, err := hashCreateTaskCommand(command, budget)
if err != nil {
@@ -323,6 +336,7 @@ func (s *TaskService) Cancel(
command CancelTaskCommand,
) (domain.PurchaseTask, error) {
command.CreatorSubject = strings.TrimSpace(command.CreatorSubject)
command.ActorUserID = strings.TrimSpace(command.ActorUserID)
command.TaskID = strings.TrimSpace(command.TaskID)
command.Reason = strings.TrimSpace(command.Reason)
fields := make(map[string]string)
@@ -332,6 +346,9 @@ func (s *TaskService) Cancel(
if !isUUID(command.TaskID) {
fields["task_id"] = "must be a UUID"
}
if !isUUID(command.ActorUserID) {
fields["actor_user_id"] = "must be a UUID"
}
if len([]byte(command.Reason)) > domain.MaxCancelReasonBytes {
fields["reason"] = "too long"
}
@@ -352,12 +369,14 @@ func (s *TaskService) Cancel(
)
}
now := s.clock.Now().UTC()
actorUserID := command.ActorUserID
event := domain.TaskEvent{
ID: eventID,
TaskID: command.TaskID,
Type: "TASK_CANCELED",
Message: "task canceled",
OccurredAt: now,
ID: eventID,
TaskID: command.TaskID,
ActorUserID: &actorUserID,
Type: "TASK_CANCELED",
Message: "task canceled",
OccurredAt: now,
}
task, err := s.repository.CancelPendingTask(
ctx,
@@ -378,6 +397,7 @@ func hashCreateTaskCommand(
budget *int64,
) (string, error) {
payload := struct {
ActorUserID string `json:"actor_user_id"`
SourceRef *string `json:"source_ref"`
Title string `json:"title"`
Description string `json:"description"`
@@ -386,6 +406,7 @@ func hashCreateTaskCommand(
Quantity int `json:"quantity"`
MaxBudgetCents *int64 `json:"max_budget_cents"`
}{
ActorUserID: command.ActorUserID,
SourceRef: command.SourceRef,
Title: command.Title,
Description: command.Description,