initial commit
This commit is contained in:
1
.docker/postgres/docker-entrypoint-initdb/extension.sql
Normal file
1
.docker/postgres/docker-entrypoint-initdb/extension.sql
Normal file
@@ -0,0 +1 @@
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
/.idea
|
||||
/.qodo
|
||||
config.yaml
|
||||
vendor/
|
||||
.vscode
|
||||
.DS_Store
|
||||
/tmp
|
||||
/build
|
||||
13
Dockerfile.mock-oauth
Normal file
13
Dockerfile.mock-oauth
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM golang:1.22-alpine AS builder
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 go build -o /mock-oauth ./cmd/mock-oauth
|
||||
|
||||
FROM alpine:3.19
|
||||
RUN apk --no-cache add ca-certificates
|
||||
COPY --from=builder /mock-oauth /mock-oauth
|
||||
EXPOSE 9999
|
||||
ENV PORT=9999
|
||||
CMD ["/mock-oauth"]
|
||||
72
Makefile
Normal file
72
Makefile
Normal file
@@ -0,0 +1,72 @@
|
||||
# Set GOPATH from `go env`
|
||||
GOPATH:=$(shell go env GOPATH)
|
||||
MIGRATIONS_DIR:=./database/migrations
|
||||
SCHEMA_DIR:=./database/schema
|
||||
DEV_URL:=docker://postgres/18-alpine/test
|
||||
DATABASE_URL:=postgres://alinmeuser:password@localhost:5430/alinmedb?sslmode=disable
|
||||
|
||||
.PHONY: init test generate buf
|
||||
init:
|
||||
@echo "Initializing project..."
|
||||
make buf
|
||||
|
||||
buf:
|
||||
buf generate --verbose
|
||||
# Run unit tests
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
go test ./... -v
|
||||
|
||||
# Generate code using go generate
|
||||
generate:
|
||||
@echo "Running go generate..."
|
||||
go generate ./...
|
||||
|
||||
env:
|
||||
cp -rf ./.env-example ./.env
|
||||
|
||||
lint:
|
||||
@ test -e $(LINT) || $(LINT_DOWNLOAD)
|
||||
@ $(LINT) --version
|
||||
@ $(LINT) run
|
||||
|
||||
protoc:
|
||||
@ protoc \
|
||||
--go_out=api --go_opt=paths=source_relative \
|
||||
--go-grpc_out=api --go-grpc_opt=paths=source_relative \
|
||||
pb/*.proto
|
||||
|
||||
swagger:
|
||||
@echo Starting swagger generating
|
||||
swag init -g main.go
|
||||
|
||||
# Start Postgres in Docker (required before migrate-apply)
|
||||
docker-up:
|
||||
@echo "Starting Postgres in Docker..."
|
||||
docker compose up -d pg
|
||||
@echo "Waiting for Postgres to be ready..."
|
||||
@until docker compose exec pg pg_isready -U alinmeuser -d alinmedb 2>/dev/null; do sleep 1; done
|
||||
@echo "Postgres is ready."
|
||||
|
||||
# Apply migrations to local Postgres
|
||||
migrate-apply:
|
||||
@echo "Applying migrations..."
|
||||
atlas migrate apply --dir "file://$(MIGRATIONS_DIR)" --url "$(DATABASE_URL)"
|
||||
|
||||
# Run Docker + migrations (use for local setup)
|
||||
migrate-local: docker-up migrate-apply
|
||||
@echo "Migrations applied successfully."
|
||||
|
||||
# Run mock OAuth server for local dev (no Docker - use when testing OAuth flow)
|
||||
mock-oauth:
|
||||
@echo "Starting mock OAuth server on http://localhost:9999"
|
||||
go run ./cmd/mock-oauth
|
||||
|
||||
migrate-diff:
|
||||
@echo "Generating migration diff..."
|
||||
atlas migrate diff --dir "file://$(MIGRATIONS_DIR)" --to "file://$(SCHEMA_DIR)" --dev-url "$(DEV_URL)"
|
||||
|
||||
# Update atlas.sum with new migration checksums (run after adding migrations)
|
||||
migrate-hash:
|
||||
@echo "Updating migration checksums..."
|
||||
atlas migrate hash --dir "file://$(MIGRATIONS_DIR)"
|
||||
88
api.http
Normal file
88
api.http
Normal file
@@ -0,0 +1,88 @@
|
||||
@baseUrl = http://localhost:8101/api/v1
|
||||
# Paste tokens here after running "OAuth callback" request below
|
||||
@accessToken = eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiYWxpbm1lLXdlYiJdLCJleHAiOjE3NzIyNzQwMzgsImlhdCI6MTc3MjE4NzYzOCwiaXNzIjoiYWxpbm1lLXNlcnZlciIsInN1YiI6ImVkMDkyYjI2LTlhODQtNDI0YS05MTMyLTkzODg5Yzg2NzE3YyJ9.aMpaoXVb5t0ChT4mBGpoxE4F7DhPq6Olyf5AdrsA0rE
|
||||
@refreshToken =
|
||||
|
||||
### ============================================
|
||||
### OAuth Mock Login Flow
|
||||
### Prerequisites: mock-oauth running (make mock-oauth) + oauth.mock.enabled=true in config
|
||||
### ============================================
|
||||
|
||||
### 1. Get OAuth redirect URL (for mock provider)
|
||||
### User would visit this URL → mock login page → redirect to frontend with code
|
||||
# @name oauth_redirect_url
|
||||
POST {{baseUrl}}/auth/oauth/redirect-url
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"provider": "Mock"
|
||||
}
|
||||
|
||||
### 2. OAuth callback - exchange code for tokens
|
||||
### Mock server always returns code: mock_auth_code_12345 (no browser needed)
|
||||
### Run this after step 1, then paste access_token and refresh_token into variables above for steps 3-4
|
||||
# @name oauth_callback
|
||||
POST {{baseUrl}}/auth/oauth/callback
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"provider": "mock",
|
||||
"code": "mock_auth_code_12345"
|
||||
}
|
||||
|
||||
### ============================================
|
||||
### Protected endpoints (use token from above)
|
||||
### ============================================
|
||||
|
||||
### 3. Get user info (requires auth)
|
||||
GET {{baseUrl}}/account/info
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### 4. Setup profile (for new users - handle required)
|
||||
POST {{baseUrl}}/platform/setup-profile
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"handle": "my-handle-dev",
|
||||
"short_description": "Developer profile",
|
||||
"role_level": "mid"
|
||||
}
|
||||
|
||||
### 5. List profile roles (for setup-profile role_id)
|
||||
GET {{baseUrl}}/platform/profile-roles
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### 6. List skills (for profile skill selection)
|
||||
GET {{baseUrl}}/platform/skills
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### 7. Get discovery overview
|
||||
GET {{baseUrl}}/platform/overview/discovery
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### 8. Get specialist overview
|
||||
GET {{baseUrl}}/platform/overview/specialist
|
||||
Authorization: Bearer {{accessToken}}
|
||||
|
||||
### 9. Verify account
|
||||
POST {{baseUrl}}/platform/verify-account
|
||||
Authorization: Bearer {{accessToken}}
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"email": "user@example.com",
|
||||
"code": "123456"
|
||||
}
|
||||
|
||||
### Refresh token
|
||||
POST {{baseUrl}}/auth/refresh-token
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"refresh_token": "{{refreshToken}}"
|
||||
}
|
||||
|
||||
### Alternative: paste token manually for account/info
|
||||
# GET {{baseUrl}}/account/info
|
||||
# Authorization: Bearer <paste_access_token_here>
|
||||
253
api/pb/barcodemappingpb_v1/barcode-mapping-v1.pb.go
Normal file
253
api/pb/barcodemappingpb_v1/barcode-mapping-v1.pb.go
Normal file
@@ -0,0 +1,253 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.1
|
||||
// protoc v3.11.2
|
||||
// source: barcode-mapping-v1.proto
|
||||
|
||||
package barcodemappingpb_v1
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type GetProductVariationsIDsRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
VendorId int32 `protobuf:"varint,1,opt,name=vendor_id,proto3" json:"vendor_id,omitempty"`
|
||||
Barcodes []string `protobuf:"bytes,2,rep,name=barcodes,proto3" json:"barcodes,omitempty"`
|
||||
}
|
||||
|
||||
func (x *GetProductVariationsIDsRequest) Reset() {
|
||||
*x = GetProductVariationsIDsRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_barcode_mapping_v1_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GetProductVariationsIDsRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetProductVariationsIDsRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetProductVariationsIDsRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_barcode_mapping_v1_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetProductVariationsIDsRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetProductVariationsIDsRequest) Descriptor() ([]byte, []int) {
|
||||
return file_barcode_mapping_v1_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *GetProductVariationsIDsRequest) GetVendorId() int32 {
|
||||
if x != nil {
|
||||
return x.VendorId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *GetProductVariationsIDsRequest) GetBarcodes() []string {
|
||||
if x != nil {
|
||||
return x.Barcodes
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetProductVariationsIDsResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
ProductVariations map[int32]string `protobuf:"bytes,1,rep,name=product_variations,proto3" json:"product_variations,omitempty" protobuf_key:"varint,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
|
||||
}
|
||||
|
||||
func (x *GetProductVariationsIDsResponse) Reset() {
|
||||
*x = GetProductVariationsIDsResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_barcode_mapping_v1_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GetProductVariationsIDsResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetProductVariationsIDsResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetProductVariationsIDsResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_barcode_mapping_v1_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetProductVariationsIDsResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetProductVariationsIDsResponse) Descriptor() ([]byte, []int) {
|
||||
return file_barcode_mapping_v1_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *GetProductVariationsIDsResponse) GetProductVariations() map[int32]string {
|
||||
if x != nil {
|
||||
return x.ProductVariations
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var File_barcode_mapping_v1_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_barcode_mapping_v1_proto_rawDesc = []byte{
|
||||
0x0a, 0x18, 0x62, 0x61, 0x72, 0x63, 0x6f, 0x64, 0x65, 0x2d, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e,
|
||||
0x67, 0x2d, 0x76, 0x31, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x13, 0x62, 0x61, 0x72, 0x63,
|
||||
0x6f, 0x64, 0x65, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x70, 0x62, 0x5f, 0x76, 0x31, 0x22,
|
||||
0x5a, 0x0a, 0x1e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x56, 0x61, 0x72,
|
||||
0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x49, 0x44, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
|
||||
0x74, 0x12, 0x1c, 0x0a, 0x09, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x01,
|
||||
0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x5f, 0x69, 0x64, 0x12,
|
||||
0x1a, 0x0a, 0x08, 0x62, 0x61, 0x72, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28,
|
||||
0x09, 0x52, 0x08, 0x62, 0x61, 0x72, 0x63, 0x6f, 0x64, 0x65, 0x73, 0x22, 0xe4, 0x01, 0x0a, 0x1f,
|
||||
0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x74,
|
||||
0x69, 0x6f, 0x6e, 0x73, 0x49, 0x44, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
|
||||
0x7b, 0x0a, 0x12, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x4b, 0x2e, 0x62, 0x61,
|
||||
0x72, 0x63, 0x6f, 0x64, 0x65, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x70, 0x62, 0x5f, 0x76,
|
||||
0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x56, 0x61, 0x72, 0x69,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x49, 0x44, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
|
||||
0x65, 0x2e, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x74, 0x69,
|
||||
0x6f, 0x6e, 0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x12, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63,
|
||||
0x74, 0x5f, 0x76, 0x61, 0x72, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x1a, 0x44, 0x0a, 0x16,
|
||||
0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e,
|
||||
0x73, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20,
|
||||
0x01, 0x28, 0x05, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75,
|
||||
0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02,
|
||||
0x38, 0x01, 0x32, 0x8b, 0x02, 0x0a, 0x15, 0x42, 0x61, 0x72, 0x63, 0x6f, 0x64, 0x65, 0x4d, 0x61,
|
||||
0x70, 0x70, 0x69, 0x6e, 0x67, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x78, 0x0a, 0x0b,
|
||||
0x47, 0x65, 0x74, 0x42, 0x79, 0x4d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x12, 0x33, 0x2e, 0x62, 0x61,
|
||||
0x72, 0x63, 0x6f, 0x64, 0x65, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x70, 0x62, 0x5f, 0x76,
|
||||
0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x56, 0x61, 0x72, 0x69,
|
||||
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x49, 0x44, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
|
||||
0x1a, 0x34, 0x2e, 0x62, 0x61, 0x72, 0x63, 0x6f, 0x64, 0x65, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e,
|
||||
0x67, 0x70, 0x62, 0x5f, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63,
|
||||
0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x49, 0x44, 0x73, 0x52, 0x65,
|
||||
0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x78, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x42, 0x79, 0x56,
|
||||
0x65, 0x6e, 0x64, 0x6f, 0x72, 0x12, 0x33, 0x2e, 0x62, 0x61, 0x72, 0x63, 0x6f, 0x64, 0x65, 0x6d,
|
||||
0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x70, 0x62, 0x5f, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x50,
|
||||
0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73,
|
||||
0x49, 0x44, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x34, 0x2e, 0x62, 0x61, 0x72,
|
||||
0x63, 0x6f, 0x64, 0x65, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x70, 0x62, 0x5f, 0x76, 0x31,
|
||||
0x2e, 0x47, 0x65, 0x74, 0x50, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x56, 0x61, 0x72, 0x69, 0x61,
|
||||
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x49, 0x44, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65,
|
||||
0x42, 0x22, 0x5a, 0x20, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x67, 0x72, 0x70, 0x63, 0x2f, 0x70, 0x62,
|
||||
0x2f, 0x62, 0x61, 0x72, 0x63, 0x6f, 0x64, 0x65, 0x6d, 0x61, 0x70, 0x70, 0x69, 0x6e, 0x67, 0x70,
|
||||
0x62, 0x5f, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_barcode_mapping_v1_proto_rawDescOnce sync.Once
|
||||
file_barcode_mapping_v1_proto_rawDescData = file_barcode_mapping_v1_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_barcode_mapping_v1_proto_rawDescGZIP() []byte {
|
||||
file_barcode_mapping_v1_proto_rawDescOnce.Do(func() {
|
||||
file_barcode_mapping_v1_proto_rawDescData = protoimpl.X.CompressGZIP(file_barcode_mapping_v1_proto_rawDescData)
|
||||
})
|
||||
return file_barcode_mapping_v1_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_barcode_mapping_v1_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
||||
var file_barcode_mapping_v1_proto_goTypes = []interface{}{
|
||||
(*GetProductVariationsIDsRequest)(nil), // 0: barcodemappingpb_v1.GetProductVariationsIDsRequest
|
||||
(*GetProductVariationsIDsResponse)(nil), // 1: barcodemappingpb_v1.GetProductVariationsIDsResponse
|
||||
nil, // 2: barcodemappingpb_v1.GetProductVariationsIDsResponse.ProductVariationsEntry
|
||||
}
|
||||
var file_barcode_mapping_v1_proto_depIdxs = []int32{
|
||||
2, // 0: barcodemappingpb_v1.GetProductVariationsIDsResponse.product_variations:type_name -> barcodemappingpb_v1.GetProductVariationsIDsResponse.ProductVariationsEntry
|
||||
0, // 1: barcodemappingpb_v1.BarcodeMappingService.GetByMaster:input_type -> barcodemappingpb_v1.GetProductVariationsIDsRequest
|
||||
0, // 2: barcodemappingpb_v1.BarcodeMappingService.GetByVendor:input_type -> barcodemappingpb_v1.GetProductVariationsIDsRequest
|
||||
1, // 3: barcodemappingpb_v1.BarcodeMappingService.GetByMaster:output_type -> barcodemappingpb_v1.GetProductVariationsIDsResponse
|
||||
1, // 4: barcodemappingpb_v1.BarcodeMappingService.GetByVendor:output_type -> barcodemappingpb_v1.GetProductVariationsIDsResponse
|
||||
3, // [3:5] is the sub-list for method output_type
|
||||
1, // [1:3] is the sub-list for method input_type
|
||||
1, // [1:1] is the sub-list for extension type_name
|
||||
1, // [1:1] is the sub-list for extension extendee
|
||||
0, // [0:1] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_barcode_mapping_v1_proto_init() }
|
||||
func file_barcode_mapping_v1_proto_init() {
|
||||
if File_barcode_mapping_v1_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_barcode_mapping_v1_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GetProductVariationsIDsRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_barcode_mapping_v1_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GetProductVariationsIDsResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_barcode_mapping_v1_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 3,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_barcode_mapping_v1_proto_goTypes,
|
||||
DependencyIndexes: file_barcode_mapping_v1_proto_depIdxs,
|
||||
MessageInfos: file_barcode_mapping_v1_proto_msgTypes,
|
||||
}.Build()
|
||||
File_barcode_mapping_v1_proto = out.File
|
||||
file_barcode_mapping_v1_proto_rawDesc = nil
|
||||
file_barcode_mapping_v1_proto_goTypes = nil
|
||||
file_barcode_mapping_v1_proto_depIdxs = nil
|
||||
}
|
||||
19
api/pb/barcodemappingpb_v1/barcode-mapping-v1.proto
Normal file
19
api/pb/barcodemappingpb_v1/barcode-mapping-v1.proto
Normal file
@@ -0,0 +1,19 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package barcodemappingpb_v1;
|
||||
|
||||
option go_package = "/api/grpc/pb/barcodemappingpb_v1";
|
||||
|
||||
service BarcodeMappingService{
|
||||
rpc GetByMaster(GetProductVariationsIDsRequest) returns (GetProductVariationsIDsResponse);
|
||||
rpc GetByVendor(GetProductVariationsIDsRequest) returns (GetProductVariationsIDsResponse);
|
||||
}
|
||||
|
||||
message GetProductVariationsIDsRequest {
|
||||
int32 vendor_id = 1 [json_name = "vendor_id"];
|
||||
repeated string barcodes = 2;
|
||||
}
|
||||
|
||||
message GetProductVariationsIDsResponse {
|
||||
map<int32, string> product_variations = 1 [json_name = "product_variations"];
|
||||
}
|
||||
141
api/pb/barcodemappingpb_v1/barcode-mapping-v1_grpc.pb.go
Normal file
141
api/pb/barcodemappingpb_v1/barcode-mapping-v1_grpc.pb.go
Normal file
@@ -0,0 +1,141 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.11.2
|
||||
// source: barcode-mapping-v1.proto
|
||||
|
||||
package barcodemappingpb_v1
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
|
||||
// BarcodeMappingServiceClient is the client API for BarcodeMappingService service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type BarcodeMappingServiceClient interface {
|
||||
GetByMaster(ctx context.Context, in *GetProductVariationsIDsRequest, opts ...grpc.CallOption) (*GetProductVariationsIDsResponse, error)
|
||||
GetByVendor(ctx context.Context, in *GetProductVariationsIDsRequest, opts ...grpc.CallOption) (*GetProductVariationsIDsResponse, error)
|
||||
}
|
||||
|
||||
type barcodeMappingServiceClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewBarcodeMappingServiceClient(cc grpc.ClientConnInterface) BarcodeMappingServiceClient {
|
||||
return &barcodeMappingServiceClient{cc}
|
||||
}
|
||||
|
||||
func (c *barcodeMappingServiceClient) GetByMaster(ctx context.Context, in *GetProductVariationsIDsRequest, opts ...grpc.CallOption) (*GetProductVariationsIDsResponse, error) {
|
||||
out := new(GetProductVariationsIDsResponse)
|
||||
err := c.cc.Invoke(ctx, "/barcodemappingpb_v1.BarcodeMappingService/GetByMaster", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (c *barcodeMappingServiceClient) GetByVendor(ctx context.Context, in *GetProductVariationsIDsRequest, opts ...grpc.CallOption) (*GetProductVariationsIDsResponse, error) {
|
||||
out := new(GetProductVariationsIDsResponse)
|
||||
err := c.cc.Invoke(ctx, "/barcodemappingpb_v1.BarcodeMappingService/GetByVendor", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// BarcodeMappingServiceServer is the server API for BarcodeMappingService service.
|
||||
// All implementations must embed UnimplementedBarcodeMappingServiceServer
|
||||
// for forward compatibility
|
||||
type BarcodeMappingServiceServer interface {
|
||||
GetByMaster(context.Context, *GetProductVariationsIDsRequest) (*GetProductVariationsIDsResponse, error)
|
||||
GetByVendor(context.Context, *GetProductVariationsIDsRequest) (*GetProductVariationsIDsResponse, error)
|
||||
mustEmbedUnimplementedBarcodeMappingServiceServer()
|
||||
}
|
||||
|
||||
// UnimplementedBarcodeMappingServiceServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedBarcodeMappingServiceServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedBarcodeMappingServiceServer) GetByMaster(context.Context, *GetProductVariationsIDsRequest) (*GetProductVariationsIDsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetByMaster not implemented")
|
||||
}
|
||||
func (UnimplementedBarcodeMappingServiceServer) GetByVendor(context.Context, *GetProductVariationsIDsRequest) (*GetProductVariationsIDsResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetByVendor not implemented")
|
||||
}
|
||||
func (UnimplementedBarcodeMappingServiceServer) mustEmbedUnimplementedBarcodeMappingServiceServer() {}
|
||||
|
||||
// UnsafeBarcodeMappingServiceServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to BarcodeMappingServiceServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeBarcodeMappingServiceServer interface {
|
||||
mustEmbedUnimplementedBarcodeMappingServiceServer()
|
||||
}
|
||||
|
||||
func RegisterBarcodeMappingServiceServer(s grpc.ServiceRegistrar, srv BarcodeMappingServiceServer) {
|
||||
s.RegisterService(&BarcodeMappingService_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _BarcodeMappingService_GetByMaster_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetProductVariationsIDsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(BarcodeMappingServiceServer).GetByMaster(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/barcodemappingpb_v1.BarcodeMappingService/GetByMaster",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(BarcodeMappingServiceServer).GetByMaster(ctx, req.(*GetProductVariationsIDsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
func _BarcodeMappingService_GetByVendor_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetProductVariationsIDsRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(BarcodeMappingServiceServer).GetByVendor(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/barcodemappingpb_v1.BarcodeMappingService/GetByVendor",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(BarcodeMappingServiceServer).GetByVendor(ctx, req.(*GetProductVariationsIDsRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// BarcodeMappingService_ServiceDesc is the grpc.ServiceDesc for BarcodeMappingService service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var BarcodeMappingService_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "barcodemappingpb_v1.BarcodeMappingService",
|
||||
HandlerType: (*BarcodeMappingServiceServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "GetByMaster",
|
||||
Handler: _BarcodeMappingService_GetByMaster_Handler,
|
||||
},
|
||||
{
|
||||
MethodName: "GetByVendor",
|
||||
Handler: _BarcodeMappingService_GetByVendor_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "barcode-mapping-v1.proto",
|
||||
}
|
||||
379
api/pb/discount/discount.pb.go
Normal file
379
api/pb/discount/discount.pb.go
Normal file
@@ -0,0 +1,379 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.28.1
|
||||
// protoc v3.11.2
|
||||
// source: discount.proto
|
||||
|
||||
package discount
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type GetVendorDiscountRequest struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Discounts []*GetVendorDiscountRequest_DiscountEntry `protobuf:"bytes,1,rep,name=discounts,proto3" json:"discounts,omitempty"`
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountRequest) Reset() {
|
||||
*x = GetVendorDiscountRequest{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_discount_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountRequest) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetVendorDiscountRequest) ProtoMessage() {}
|
||||
|
||||
func (x *GetVendorDiscountRequest) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_discount_proto_msgTypes[0]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetVendorDiscountRequest.ProtoReflect.Descriptor instead.
|
||||
func (*GetVendorDiscountRequest) Descriptor() ([]byte, []int) {
|
||||
return file_discount_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountRequest) GetDiscounts() []*GetVendorDiscountRequest_DiscountEntry {
|
||||
if x != nil {
|
||||
return x.Discounts
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetVendorDiscountResponse struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
Discounts []*GetVendorDiscountResponse_DiscountEntry `protobuf:"bytes,1,rep,name=discounts,proto3" json:"discounts,omitempty"`
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountResponse) Reset() {
|
||||
*x = GetVendorDiscountResponse{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_discount_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountResponse) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetVendorDiscountResponse) ProtoMessage() {}
|
||||
|
||||
func (x *GetVendorDiscountResponse) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_discount_proto_msgTypes[1]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetVendorDiscountResponse.ProtoReflect.Descriptor instead.
|
||||
func (*GetVendorDiscountResponse) Descriptor() ([]byte, []int) {
|
||||
return file_discount_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountResponse) GetDiscounts() []*GetVendorDiscountResponse_DiscountEntry {
|
||||
if x != nil {
|
||||
return x.Discounts
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type GetVendorDiscountRequest_DiscountEntry struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
ProductId int32 `protobuf:"varint,1,opt,name=product_id,json=productId,proto3" json:"product_id,omitempty"`
|
||||
VendorId int32 `protobuf:"varint,2,opt,name=vendor_id,json=vendorId,proto3" json:"vendor_id,omitempty"`
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountRequest_DiscountEntry) Reset() {
|
||||
*x = GetVendorDiscountRequest_DiscountEntry{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_discount_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountRequest_DiscountEntry) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetVendorDiscountRequest_DiscountEntry) ProtoMessage() {}
|
||||
|
||||
func (x *GetVendorDiscountRequest_DiscountEntry) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_discount_proto_msgTypes[2]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetVendorDiscountRequest_DiscountEntry.ProtoReflect.Descriptor instead.
|
||||
func (*GetVendorDiscountRequest_DiscountEntry) Descriptor() ([]byte, []int) {
|
||||
return file_discount_proto_rawDescGZIP(), []int{0, 0}
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountRequest_DiscountEntry) GetProductId() int32 {
|
||||
if x != nil {
|
||||
return x.ProductId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountRequest_DiscountEntry) GetVendorId() int32 {
|
||||
if x != nil {
|
||||
return x.VendorId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
type GetVendorDiscountResponse_DiscountEntry struct {
|
||||
state protoimpl.MessageState
|
||||
sizeCache protoimpl.SizeCache
|
||||
unknownFields protoimpl.UnknownFields
|
||||
|
||||
ProductId int32 `protobuf:"varint,1,opt,name=product_id,json=productId,proto3" json:"product_id,omitempty"`
|
||||
VendorId int32 `protobuf:"varint,2,opt,name=vendor_id,json=vendorId,proto3" json:"vendor_id,omitempty"`
|
||||
Percent float32 `protobuf:"fixed32,3,opt,name=percent,proto3" json:"percent,omitempty"`
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountResponse_DiscountEntry) Reset() {
|
||||
*x = GetVendorDiscountResponse_DiscountEntry{}
|
||||
if protoimpl.UnsafeEnabled {
|
||||
mi := &file_discount_proto_msgTypes[3]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountResponse_DiscountEntry) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*GetVendorDiscountResponse_DiscountEntry) ProtoMessage() {}
|
||||
|
||||
func (x *GetVendorDiscountResponse_DiscountEntry) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_discount_proto_msgTypes[3]
|
||||
if protoimpl.UnsafeEnabled && x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetVendorDiscountResponse_DiscountEntry.ProtoReflect.Descriptor instead.
|
||||
func (*GetVendorDiscountResponse_DiscountEntry) Descriptor() ([]byte, []int) {
|
||||
return file_discount_proto_rawDescGZIP(), []int{1, 0}
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountResponse_DiscountEntry) GetProductId() int32 {
|
||||
if x != nil {
|
||||
return x.ProductId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountResponse_DiscountEntry) GetVendorId() int32 {
|
||||
if x != nil {
|
||||
return x.VendorId
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *GetVendorDiscountResponse_DiscountEntry) GetPercent() float32 {
|
||||
if x != nil {
|
||||
return x.Percent
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
var File_discount_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_discount_proto_rawDesc = []byte{
|
||||
0x0a, 0x0e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
||||
0x12, 0x08, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xb7, 0x01, 0x0a, 0x18, 0x47,
|
||||
0x65, 0x74, 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74,
|
||||
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x4e, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x63, 0x6f,
|
||||
0x75, 0x6e, 0x74, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x64, 0x69, 0x73,
|
||||
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x44,
|
||||
0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x2e, 0x44,
|
||||
0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x64, 0x69,
|
||||
0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x1a, 0x4b, 0x0a, 0x0d, 0x44, 0x69, 0x73, 0x63, 0x6f,
|
||||
0x75, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x64,
|
||||
0x75, 0x63, 0x74, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x70, 0x72,
|
||||
0x6f, 0x64, 0x75, 0x63, 0x74, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x76, 0x65, 0x6e, 0x64, 0x6f,
|
||||
0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x76, 0x65, 0x6e, 0x64,
|
||||
0x6f, 0x72, 0x49, 0x64, 0x22, 0xd3, 0x01, 0x0a, 0x19, 0x47, 0x65, 0x74, 0x56, 0x65, 0x6e, 0x64,
|
||||
0x6f, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
|
||||
0x73, 0x65, 0x12, 0x4f, 0x0a, 0x09, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x73, 0x18,
|
||||
0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x31, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74,
|
||||
0x2e, 0x47, 0x65, 0x74, 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75,
|
||||
0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x44, 0x69, 0x73, 0x63, 0x6f,
|
||||
0x75, 0x6e, 0x74, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x09, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75,
|
||||
0x6e, 0x74, 0x73, 0x1a, 0x65, 0x0a, 0x0d, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x45,
|
||||
0x6e, 0x74, 0x72, 0x79, 0x12, 0x1d, 0x0a, 0x0a, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63, 0x74, 0x5f,
|
||||
0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x64, 0x75, 0x63,
|
||||
0x74, 0x49, 0x64, 0x12, 0x1b, 0x0a, 0x09, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x5f, 0x69, 0x64,
|
||||
0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x76, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x49, 0x64,
|
||||
0x12, 0x18, 0x0a, 0x07, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28,
|
||||
0x02, 0x52, 0x07, 0x70, 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x32, 0x6e, 0x0a, 0x08, 0x44, 0x69,
|
||||
0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x62, 0x0a, 0x15, 0x47, 0x65, 0x74, 0x56, 0x65, 0x6e,
|
||||
0x64, 0x6f, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x49, 0x6e, 0x66, 0x6f, 0x12,
|
||||
0x22, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x47, 0x65, 0x74, 0x56, 0x65,
|
||||
0x6e, 0x64, 0x6f, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75,
|
||||
0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x64, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x2e, 0x47,
|
||||
0x65, 0x74, 0x56, 0x65, 0x6e, 0x64, 0x6f, 0x72, 0x44, 0x69, 0x73, 0x63, 0x6f, 0x75, 0x6e, 0x74,
|
||||
0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x00, 0x42, 0x03, 0x5a, 0x01, 0x2e, 0x62,
|
||||
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
}
|
||||
|
||||
var (
|
||||
file_discount_proto_rawDescOnce sync.Once
|
||||
file_discount_proto_rawDescData = file_discount_proto_rawDesc
|
||||
)
|
||||
|
||||
func file_discount_proto_rawDescGZIP() []byte {
|
||||
file_discount_proto_rawDescOnce.Do(func() {
|
||||
file_discount_proto_rawDescData = protoimpl.X.CompressGZIP(file_discount_proto_rawDescData)
|
||||
})
|
||||
return file_discount_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_discount_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
|
||||
var file_discount_proto_goTypes = []interface{}{
|
||||
(*GetVendorDiscountRequest)(nil), // 0: discount.GetVendorDiscountRequest
|
||||
(*GetVendorDiscountResponse)(nil), // 1: discount.GetVendorDiscountResponse
|
||||
(*GetVendorDiscountRequest_DiscountEntry)(nil), // 2: discount.GetVendorDiscountRequest.DiscountEntry
|
||||
(*GetVendorDiscountResponse_DiscountEntry)(nil), // 3: discount.GetVendorDiscountResponse.DiscountEntry
|
||||
}
|
||||
var file_discount_proto_depIdxs = []int32{
|
||||
2, // 0: discount.GetVendorDiscountRequest.discounts:type_name -> discount.GetVendorDiscountRequest.DiscountEntry
|
||||
3, // 1: discount.GetVendorDiscountResponse.discounts:type_name -> discount.GetVendorDiscountResponse.DiscountEntry
|
||||
0, // 2: discount.Discount.GetVendorDiscountInfo:input_type -> discount.GetVendorDiscountRequest
|
||||
1, // 3: discount.Discount.GetVendorDiscountInfo:output_type -> discount.GetVendorDiscountResponse
|
||||
3, // [3:4] is the sub-list for method output_type
|
||||
2, // [2:3] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_discount_proto_init() }
|
||||
func file_discount_proto_init() {
|
||||
if File_discount_proto != nil {
|
||||
return
|
||||
}
|
||||
if !protoimpl.UnsafeEnabled {
|
||||
file_discount_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GetVendorDiscountRequest); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_discount_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GetVendorDiscountResponse); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_discount_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GetVendorDiscountRequest_DiscountEntry); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
file_discount_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||
switch v := v.(*GetVendorDiscountResponse_DiscountEntry); i {
|
||||
case 0:
|
||||
return &v.state
|
||||
case 1:
|
||||
return &v.sizeCache
|
||||
case 2:
|
||||
return &v.unknownFields
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: file_discount_proto_rawDesc,
|
||||
NumEnums: 0,
|
||||
NumMessages: 4,
|
||||
NumExtensions: 0,
|
||||
NumServices: 1,
|
||||
},
|
||||
GoTypes: file_discount_proto_goTypes,
|
||||
DependencyIndexes: file_discount_proto_depIdxs,
|
||||
MessageInfos: file_discount_proto_msgTypes,
|
||||
}.Build()
|
||||
File_discount_proto = out.File
|
||||
file_discount_proto_rawDesc = nil
|
||||
file_discount_proto_goTypes = nil
|
||||
file_discount_proto_depIdxs = nil
|
||||
}
|
||||
26
api/pb/discount/discount.proto
Normal file
26
api/pb/discount/discount.proto
Normal file
@@ -0,0 +1,26 @@
|
||||
syntax = "proto3";
|
||||
package discount;
|
||||
|
||||
// import "google/protobuf/empty.proto";
|
||||
option go_package = ".";
|
||||
|
||||
service Discount {
|
||||
rpc GetVendorDiscountInfo(GetVendorDiscountRequest) returns (GetVendorDiscountResponse) {}
|
||||
}
|
||||
|
||||
message GetVendorDiscountRequest {
|
||||
message DiscountEntry {
|
||||
int32 product_id = 1;
|
||||
int32 vendor_id = 2;
|
||||
}
|
||||
repeated DiscountEntry discounts = 1;
|
||||
}
|
||||
|
||||
message GetVendorDiscountResponse {
|
||||
message DiscountEntry {
|
||||
int32 product_id = 1;
|
||||
int32 vendor_id = 2;
|
||||
float percent = 3;
|
||||
}
|
||||
repeated DiscountEntry discounts = 1;
|
||||
}
|
||||
105
api/pb/discount/discount_grpc.pb.go
Normal file
105
api/pb/discount/discount_grpc.pb.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||
// versions:
|
||||
// - protoc-gen-go-grpc v1.2.0
|
||||
// - protoc v3.11.2
|
||||
// source: discount.proto
|
||||
|
||||
package discount
|
||||
|
||||
import (
|
||||
context "context"
|
||||
grpc "google.golang.org/grpc"
|
||||
codes "google.golang.org/grpc/codes"
|
||||
status "google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// This is a compile-time assertion to ensure that this generated file
|
||||
// is compatible with the grpc package it is being compiled against.
|
||||
// Requires gRPC-Go v1.32.0 or later.
|
||||
const _ = grpc.SupportPackageIsVersion7
|
||||
|
||||
// DiscountClient is the client API for Discount service.
|
||||
//
|
||||
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||
type DiscountClient interface {
|
||||
GetVendorDiscountInfo(ctx context.Context, in *GetVendorDiscountRequest, opts ...grpc.CallOption) (*GetVendorDiscountResponse, error)
|
||||
}
|
||||
|
||||
type discountClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
func NewDiscountClient(cc grpc.ClientConnInterface) DiscountClient {
|
||||
return &discountClient{cc}
|
||||
}
|
||||
|
||||
func (c *discountClient) GetVendorDiscountInfo(ctx context.Context, in *GetVendorDiscountRequest, opts ...grpc.CallOption) (*GetVendorDiscountResponse, error) {
|
||||
out := new(GetVendorDiscountResponse)
|
||||
err := c.cc.Invoke(ctx, "/discount.Discount/GetVendorDiscountInfo", in, out, opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// DiscountServer is the server API for Discount service.
|
||||
// All implementations must embed UnimplementedDiscountServer
|
||||
// for forward compatibility
|
||||
type DiscountServer interface {
|
||||
GetVendorDiscountInfo(context.Context, *GetVendorDiscountRequest) (*GetVendorDiscountResponse, error)
|
||||
mustEmbedUnimplementedDiscountServer()
|
||||
}
|
||||
|
||||
// UnimplementedDiscountServer must be embedded to have forward compatible implementations.
|
||||
type UnimplementedDiscountServer struct {
|
||||
}
|
||||
|
||||
func (UnimplementedDiscountServer) GetVendorDiscountInfo(context.Context, *GetVendorDiscountRequest) (*GetVendorDiscountResponse, error) {
|
||||
return nil, status.Errorf(codes.Unimplemented, "method GetVendorDiscountInfo not implemented")
|
||||
}
|
||||
func (UnimplementedDiscountServer) mustEmbedUnimplementedDiscountServer() {}
|
||||
|
||||
// UnsafeDiscountServer may be embedded to opt out of forward compatibility for this service.
|
||||
// Use of this interface is not recommended, as added methods to DiscountServer will
|
||||
// result in compilation errors.
|
||||
type UnsafeDiscountServer interface {
|
||||
mustEmbedUnimplementedDiscountServer()
|
||||
}
|
||||
|
||||
func RegisterDiscountServer(s grpc.ServiceRegistrar, srv DiscountServer) {
|
||||
s.RegisterService(&Discount_ServiceDesc, srv)
|
||||
}
|
||||
|
||||
func _Discount_GetVendorDiscountInfo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||
in := new(GetVendorDiscountRequest)
|
||||
if err := dec(in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if interceptor == nil {
|
||||
return srv.(DiscountServer).GetVendorDiscountInfo(ctx, in)
|
||||
}
|
||||
info := &grpc.UnaryServerInfo{
|
||||
Server: srv,
|
||||
FullMethod: "/discount.Discount/GetVendorDiscountInfo",
|
||||
}
|
||||
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||
return srv.(DiscountServer).GetVendorDiscountInfo(ctx, req.(*GetVendorDiscountRequest))
|
||||
}
|
||||
return interceptor(ctx, in, info, handler)
|
||||
}
|
||||
|
||||
// Discount_ServiceDesc is the grpc.ServiceDesc for Discount service.
|
||||
// It's only intended for direct use with grpc.RegisterService,
|
||||
// and not to be introspected or modified (even as a copy)
|
||||
var Discount_ServiceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "discount.Discount",
|
||||
HandlerType: (*DiscountServer)(nil),
|
||||
Methods: []grpc.MethodDesc{
|
||||
{
|
||||
MethodName: "GetVendorDiscountInfo",
|
||||
Handler: _Discount_GetVendorDiscountInfo_Handler,
|
||||
},
|
||||
},
|
||||
Streams: []grpc.StreamDesc{},
|
||||
Metadata: "discount.proto",
|
||||
}
|
||||
194
cmd/mock-oauth/main.go
Normal file
194
cmd/mock-oauth/main.go
Normal file
@@ -0,0 +1,194 @@
|
||||
// mock-oauth is a minimal OAuth2 server for local development and testing.
|
||||
// It mimics Google/GitHub OAuth2 Authorization Code flow so you can test
|
||||
// login without real OAuth provider credentials.
|
||||
//
|
||||
// Flow:
|
||||
// 1. Frontend gets auth URL from your backend (POST /oauth/redirect-url with provider=mock)
|
||||
// 2. User is redirected to this mock's /authorize
|
||||
// 3. User clicks "Login" → mock redirects to your frontend's redirect_uri with ?code=...&state=...
|
||||
// 4. Frontend sends code to your backend (POST /oauth/callback)
|
||||
// 5. Backend exchanges code for token at this mock's /token, gets user at /userinfo
|
||||
//
|
||||
// Run: go run cmd/mock-oauth/main.go
|
||||
// Default: http://localhost:9999
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultPort = "9999"
|
||||
mockCode = "mock_auth_code_12345"
|
||||
mockAccessToken = "mock_access_token_67890"
|
||||
mockEmail = "dev@example.com"
|
||||
mockName = "Dev User"
|
||||
mockGivenName = "Dev"
|
||||
mockFamilyName = "User"
|
||||
mockID = "mock-user-001"
|
||||
)
|
||||
|
||||
func main() {
|
||||
port := defaultPort
|
||||
if p := strings.TrimSpace(os.Getenv("PORT")); p != "" {
|
||||
port = p
|
||||
}
|
||||
|
||||
http.HandleFunc("/authorize", handleAuthorize)
|
||||
http.HandleFunc("/token", handleToken)
|
||||
http.HandleFunc("/userinfo", handleUserinfo)
|
||||
http.HandleFunc("/", handleRoot)
|
||||
|
||||
addr := ":" + port
|
||||
log.Printf("Mock OAuth server running at http://localhost%s", addr)
|
||||
log.Printf(" /authorize - OAuth2 authorize (redirect_uri, state, client_id)")
|
||||
log.Printf(" /token - OAuth2 token exchange")
|
||||
log.Printf(" /userinfo - User info (Bearer token)")
|
||||
log.Printf("")
|
||||
log.Printf("Configure your app: oauth.mock.base_url=http://localhost%s", addr)
|
||||
if err := http.ListenAndServe(addr, nil); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func handleRoot(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path != "/" {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
_, _ = w.Write([]byte("Mock OAuth2 Server\n\nEndpoints: /authorize, /token, /userinfo"))
|
||||
}
|
||||
|
||||
func handleAuthorize(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
|
||||
_ = r.ParseForm()
|
||||
redirectURI := r.FormValue("redirect_uri")
|
||||
if redirectURI == "" {
|
||||
redirectURI = r.URL.Query().Get("redirect_uri")
|
||||
}
|
||||
state := r.FormValue("state")
|
||||
if state == "" {
|
||||
state = r.URL.Query().Get("state")
|
||||
}
|
||||
clientID := r.FormValue("client_id")
|
||||
if clientID == "" {
|
||||
clientID = r.URL.Query().Get("client_id")
|
||||
}
|
||||
|
||||
if redirectURI == "" {
|
||||
http.Error(w, "redirect_uri is required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if r.FormValue("approve") != "" || r.FormValue("login") != "" {
|
||||
redir, _ := urlAddQuery(redirectURI, map[string]string{
|
||||
"code": mockCode,
|
||||
"state": state,
|
||||
})
|
||||
http.Redirect(w, r, redir, http.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
// Show simple login page
|
||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||
html := fmt.Sprintf(`<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Mock OAuth Login</title></head>
|
||||
<body style="font-family: sans-serif; max-width: 400px; margin: 60px auto; padding: 20px;">
|
||||
<h1>Mock OAuth2</h1>
|
||||
<p>Local development login. Client: %s</p>
|
||||
<form method="post" action="/authorize">
|
||||
<input type="hidden" name="redirect_uri" value="%s" />
|
||||
<input type="hidden" name="state" value="%s" />
|
||||
<input type="hidden" name="client_id" value="%s" />
|
||||
<button type="submit" name="approve" value="1" style="padding: 10px 24px; font-size: 16px;">Login as Dev User</button>
|
||||
</form>
|
||||
</body>
|
||||
</html>`,
|
||||
escapeHTML(clientID),
|
||||
escapeHTML(redirectURI), escapeHTML(state), escapeHTML(clientID))
|
||||
_, _ = w.Write([]byte(html))
|
||||
}
|
||||
|
||||
func urlAddQuery(base string, params map[string]string) (string, error) {
|
||||
u, err := url.Parse(base)
|
||||
if err != nil {
|
||||
return base, err
|
||||
}
|
||||
q := u.Query()
|
||||
for k, v := range params {
|
||||
if v != "" {
|
||||
q.Set(k, v)
|
||||
}
|
||||
}
|
||||
u.RawQuery = q.Encode()
|
||||
return u.String(), nil
|
||||
}
|
||||
|
||||
func escapeHTML(s string) string {
|
||||
s = strings.ReplaceAll(s, "&", "&")
|
||||
s = strings.ReplaceAll(s, "<", "<")
|
||||
s = strings.ReplaceAll(s, ">", ">")
|
||||
s = strings.ReplaceAll(s, "\"", """)
|
||||
return s
|
||||
}
|
||||
|
||||
func handleToken(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
|
||||
return
|
||||
}
|
||||
_ = r.ParseForm()
|
||||
code := r.FormValue("code")
|
||||
grantType := r.FormValue("grant_type")
|
||||
if grantType != "authorization_code" || code != mockCode {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"error": "invalid_grant",
|
||||
"error_description": "invalid code or grant_type",
|
||||
})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"access_token": mockAccessToken,
|
||||
"token_type": "Bearer",
|
||||
"expires_in": 3600,
|
||||
"refresh_token": "mock_refresh_token",
|
||||
})
|
||||
}
|
||||
|
||||
func handleUserinfo(w http.ResponseWriter, r *http.Request) {
|
||||
auth := r.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(auth, "Bearer ") {
|
||||
http.Error(w, "missing or invalid Authorization", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
token := strings.TrimPrefix(auth, "Bearer ")
|
||||
if token != mockAccessToken {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{"error": "invalid_token"})
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"id": mockID,
|
||||
"email": mockEmail,
|
||||
"name": mockName,
|
||||
"given_name": mockGivenName,
|
||||
"family_name": mockFamilyName,
|
||||
})
|
||||
}
|
||||
46
cmd/root.go
Normal file
46
cmd/root.go
Normal file
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// rootCmd represents the base command when called without any subcommands
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "base",
|
||||
Short: "A brief description of your application",
|
||||
Long: `A longer description that spans multiple lines and likely contains
|
||||
examples and usage of using your application. For example:
|
||||
|
||||
Cobra is a CLI library for Go that empowers applications.
|
||||
This application is a tool to generate the needed files
|
||||
to quickly create a Cobra application.`,
|
||||
// Uncomment the following line if your bare application
|
||||
// has an action associated with it:
|
||||
// Run: func(cmd *cobra.Command, args []string) { },
|
||||
}
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
// This is called by main.main(). It only needs to happen once to the rootCmd.
|
||||
func Execute() {
|
||||
err := rootCmd.Execute()
|
||||
if err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Here you will define your flags and configuration settings.
|
||||
// Cobra supports persistent flags, which, if defined here,
|
||||
// will be global for your application.
|
||||
|
||||
// rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.base.yaml)")
|
||||
|
||||
// Cobra also supports local flags, which will only run
|
||||
// when this action is called directly.
|
||||
rootCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
|
||||
}
|
||||
108
cmd/server.go
Normal file
108
cmd/server.go
Normal file
@@ -0,0 +1,108 @@
|
||||
/*
|
||||
Copyright © 2025 NAME HERE <EMAIL ADDRESS>
|
||||
*/
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"base/internal/pkg"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"base/config"
|
||||
"base/internal/application"
|
||||
"base/internal/delivery"
|
||||
"base/internal/repository"
|
||||
"base/internal/server"
|
||||
"base/internal/server/middleware"
|
||||
"base/pkg/metrics"
|
||||
)
|
||||
|
||||
// serverCmd represents the server command
|
||||
var serverCmd = &cobra.Command{
|
||||
Use: "server",
|
||||
Short: "A brief description of your command",
|
||||
Long: `A longer description that spans multiple lines and likely contains examples
|
||||
and usage of using your command. For example:
|
||||
|
||||
Cobra is a CLI library for Go that empowers applications.
|
||||
This application is a tool to generate the needed files
|
||||
to quickly create a Cobra application.`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
fmt.Println("server called")
|
||||
serverInit()
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(serverCmd)
|
||||
}
|
||||
|
||||
func serverInit() {
|
||||
app := fx.New(
|
||||
fx.Supply(metrics.GetMetrics("base", "api", "base-service")),
|
||||
fx.Provide(config.NewConfig),
|
||||
fx.Provide(middleware.NewMiddleware),
|
||||
pkg.Module,
|
||||
application.Module,
|
||||
repository.Module,
|
||||
delivery.Module,
|
||||
server.Server,
|
||||
fx.Invoke(registerHooks),
|
||||
)
|
||||
|
||||
startCtx, startCtxCancel := context.WithTimeout(context.Background(), fx.DefaultTimeout*10)
|
||||
defer startCtxCancel()
|
||||
|
||||
if err := app.Start(startCtx); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Wait for interrupt signal
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-c
|
||||
|
||||
stopCtx, stopCtxCancel := context.WithTimeout(context.Background(), fx.DefaultTimeout)
|
||||
defer stopCtxCancel()
|
||||
|
||||
if err := app.Stop(stopCtx); err != nil {
|
||||
os.Exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
// registerHooks registers lifecycle hooks with fx
|
||||
func registerHooks(
|
||||
lc fx.Lifecycle,
|
||||
l zerolog.Logger,
|
||||
c *config.AppConfig,
|
||||
m *metrics.Metrics,
|
||||
) error {
|
||||
lc.Append(fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
config.PrintConfig(l, c)
|
||||
|
||||
// Start system metrics collection
|
||||
if c.Metrics.Enabled {
|
||||
l.Info().Msg("System metrics collection started")
|
||||
}
|
||||
|
||||
l.Info().Msg("Application started")
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
l.Info().Msg("Shutting down application")
|
||||
l.Info().Msg("Application stopped")
|
||||
|
||||
return nil
|
||||
},
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
113
config.yml
Normal file
113
config.yml
Normal file
@@ -0,0 +1,113 @@
|
||||
environment: local
|
||||
debug: true
|
||||
|
||||
rabbitmq:
|
||||
host: localhost
|
||||
port: 5678
|
||||
user: root
|
||||
password: root
|
||||
vhost: /
|
||||
max_retry_attempts: 3
|
||||
max_connections: 10
|
||||
max_channels: 100
|
||||
connection_timeout: 30s
|
||||
heartbeat_interval: 10s
|
||||
reconnect_delay: 1s
|
||||
max_reconnect_delay: 30s
|
||||
reconnect_attempts: 5
|
||||
enable_auto_reconnect: true
|
||||
log_level: DEBUG
|
||||
prefetch_count: 1
|
||||
retry_ttl: 10s
|
||||
database:
|
||||
user: base
|
||||
password: base123
|
||||
ro_user: base
|
||||
ro_password: base123
|
||||
host: localhost
|
||||
port: "3306"
|
||||
name: base
|
||||
max_idle_conns: 3
|
||||
max_open_conns: 8
|
||||
ro_max_idle_conns: 3
|
||||
ro_max_open_conns: 8
|
||||
conn_max_idle_time: "5m"
|
||||
conn_max_lifetime: "30m"
|
||||
|
||||
pg_database:
|
||||
host: localhost
|
||||
port: 5430
|
||||
user: alinmeuser
|
||||
password: password
|
||||
name: alinmedb
|
||||
ssl_mode: disable
|
||||
# host: alinme-db.postgres.database.azure.com
|
||||
# port: 5432
|
||||
# user: alinmeadmin
|
||||
# password: X5TyF89Ucm5Q7RpQJ13Iffg
|
||||
# name: alinmedb
|
||||
# ssl_mode: require
|
||||
connection_timeout: 1m
|
||||
query_timeout: 60s
|
||||
pool_config:
|
||||
max_conn: 20
|
||||
min_conn: 5
|
||||
max_conn_lifetime: 5m
|
||||
max_conn_idle_time: 30m
|
||||
migrations:
|
||||
enabled: true
|
||||
dir: ./database/migrations
|
||||
|
||||
redis:
|
||||
host: localhost
|
||||
port: "6379"
|
||||
password: "zSdnptloZCsps0EoXqqS"
|
||||
database: 1
|
||||
|
||||
cache:
|
||||
auth:
|
||||
enabled: true
|
||||
ttl: "5m"
|
||||
prefix: "auth"
|
||||
discount:
|
||||
enabled: true
|
||||
ttl: "7m"
|
||||
prefix: "discount"
|
||||
phub:
|
||||
enabled: true
|
||||
ttl: "1h"
|
||||
prefix: "phub"
|
||||
database:
|
||||
enabled: true
|
||||
ttl: "10m"
|
||||
prefix: "db"
|
||||
|
||||
oauth:
|
||||
mock:
|
||||
enabled: true
|
||||
base_url: http://localhost:9999
|
||||
client_id: mock-client
|
||||
client_secret: mock-secret
|
||||
redirect_url: http://localhost:8101/api/v1/auth/oauth/callback/mock
|
||||
google:
|
||||
client_id: ""
|
||||
client_secret: ""
|
||||
redirect_url: ""
|
||||
github:
|
||||
client_id: ""
|
||||
client_secret: ""
|
||||
redirect_url: ""
|
||||
|
||||
server:
|
||||
host: 0.0.0.0
|
||||
port: "8080"
|
||||
|
||||
barcodemapping:
|
||||
host: "localhost"
|
||||
port: "8093"
|
||||
|
||||
syslog:
|
||||
host: "localhost"
|
||||
port: "514"
|
||||
protocol: "udp"
|
||||
log_level: "DEBUG"
|
||||
6
config/azure_blob_storage.go
Normal file
6
config/azure_blob_storage.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package config
|
||||
|
||||
// AzureBlobStorageConfig holds configuration for Azure Blob Storage
|
||||
type AzureBlobStorageConfig struct {
|
||||
BlobEndpoint string `mapstructure:"blob_endpoint"` // e.g. https://account.blob.core.windows.net - base URL for asset/avatar URLs
|
||||
}
|
||||
8
config/azure_communication.go
Normal file
8
config/azure_communication.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
type AzureCommunicationConfig struct {
|
||||
Endpoint string
|
||||
AccessKey string
|
||||
ApiVersion string
|
||||
SenderAddress string
|
||||
}
|
||||
16
config/azure_service_bus.go
Normal file
16
config/azure_service_bus.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package config
|
||||
|
||||
// AzureServiceBusConfig holds configuration for Azure Service Bus
|
||||
type AzureServiceBusConfig struct {
|
||||
// ConnectionString is the Azure Service Bus connection string
|
||||
// If provided, this will be used for authentication
|
||||
ConnectionString string
|
||||
|
||||
// Namespace is the Azure Service Bus namespace
|
||||
// Required when using managed identity authentication
|
||||
Namespace string
|
||||
|
||||
// UseManagedIdentity determines whether to use managed identity for authentication
|
||||
// If true, managed identity will be used instead of connection string
|
||||
UseManagedIdentity bool
|
||||
}
|
||||
508
config/config.go
Normal file
508
config/config.go
Normal file
@@ -0,0 +1,508 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
// AppConfig contains the application configuration
|
||||
type AppConfig struct {
|
||||
Env string
|
||||
|
||||
// Server configuration
|
||||
Port int
|
||||
|
||||
// Application configuration
|
||||
Name string
|
||||
Environment string
|
||||
VendorLockTimeOut int
|
||||
Database DatabaseConfig
|
||||
Redis RedisConfig
|
||||
Server ServerConfig
|
||||
RabbitMQ RabbitMQConfig
|
||||
Syslog SyslogConfig
|
||||
Metrics MetricsConfig
|
||||
AzureCommunicationConfig AzureCommunicationConfig
|
||||
AzureBlobStorage AzureBlobStorageConfig
|
||||
AzureServiceBus AzureServiceBusConfig
|
||||
PgDatabaseConfig PgDatabaseConfig
|
||||
JWT JWTConfig
|
||||
OAuth OAuthConfig
|
||||
}
|
||||
|
||||
// PrintConfig logs the current configuration
|
||||
func PrintConfig(logger zerolog.Logger, config *AppConfig) {
|
||||
logger.
|
||||
Info().
|
||||
Str("port", config.Server.WebPort).
|
||||
Str("name", config.Name).
|
||||
Str("environment", config.Environment).
|
||||
Msg("Application configuration")
|
||||
|
||||
}
|
||||
|
||||
// NewConfig creates a new configuration from environment variables or config file
|
||||
func NewConfig() (*AppConfig, error) {
|
||||
viper.SetConfigName("config")
|
||||
viper.SetConfigType("yaml")
|
||||
viper.AddConfigPath(".")
|
||||
viper.AddConfigPath("./config")
|
||||
|
||||
// Enable automatic environment variable binding
|
||||
viper.AutomaticEnv()
|
||||
|
||||
// Set environment variable prefix and replace dots with underscores
|
||||
viper.SetEnvPrefix("base")
|
||||
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
|
||||
// Set defaults
|
||||
viper.SetDefault("environment", "local")
|
||||
viper.SetDefault("name", "base")
|
||||
viper.SetDefault("debug", true)
|
||||
viper.SetDefault("port", 8080)
|
||||
viper.SetDefault("vendor_lock_time_out", 5)
|
||||
|
||||
// Database defaults
|
||||
viper.SetDefault("database.host", "localhost")
|
||||
viper.SetDefault("database.port", "3306")
|
||||
viper.SetDefault("database.user", "base")
|
||||
viper.SetDefault("database.password", "base123")
|
||||
viper.SetDefault("database.name", "base")
|
||||
viper.SetDefault("database.max_idle_conns", 10)
|
||||
viper.SetDefault("database.max_open_conns", 20)
|
||||
viper.SetDefault("database.conn_max_idle_time", "2m")
|
||||
viper.SetDefault("database.conn_max_lifetime", "9m")
|
||||
viper.SetDefault("database.ro_user", "base")
|
||||
viper.SetDefault("database.ro_password", "base123")
|
||||
viper.SetDefault("database.ro_max_idle_conns", 10)
|
||||
viper.SetDefault("database.ro_max_open_conns", 20)
|
||||
|
||||
// PgDatabase defaults
|
||||
viper.SetDefault("pg_database.host", "localhost")
|
||||
viper.SetDefault("pg_database.port", 5430)
|
||||
viper.SetDefault("pg_database.user", "alinmeuser")
|
||||
viper.SetDefault("pg_database.password", "password")
|
||||
viper.SetDefault("pg_database.name", "alinmedb")
|
||||
viper.SetDefault("pg_database.ssl_mode", "disable")
|
||||
viper.SetDefault("pg_database.connection_timeout", 1*time.Minute)
|
||||
viper.SetDefault("pg_database.query_timeout", 60*time.Second)
|
||||
viper.SetDefault("pg_database.pool_config.max_conn", 20)
|
||||
viper.SetDefault("pg_database.pool_config.min_conn", 5)
|
||||
viper.SetDefault("pg_database.pool_config.max_conn_lifetime", 5*time.Minute)
|
||||
viper.SetDefault("pg_database.pool_config.max_conn_idle_time", 30*time.Minute)
|
||||
viper.SetDefault("pg_database.migrations.enabled", true)
|
||||
viper.SetDefault("pg_database.migrations.dir", "./database/migrations")
|
||||
|
||||
// Redis defaults
|
||||
viper.SetDefault("redis.host", "localhost")
|
||||
viper.SetDefault("redis.port", "6379")
|
||||
viper.SetDefault("redis.password", "")
|
||||
viper.SetDefault("redis.database", 0)
|
||||
viper.SetDefault("redis.url", "")
|
||||
|
||||
// Server defaults
|
||||
viper.SetDefault("server.host", "localhost")
|
||||
viper.SetDefault("server.port", "8080")
|
||||
viper.SetDefault("rpc.host", "0.0.0.0")
|
||||
viper.SetDefault("rpc.port", "8102")
|
||||
viper.SetDefault("web.host", "localhost")
|
||||
viper.SetDefault("web.port", "8101")
|
||||
viper.SetDefault("server.max_file_size_mb", 10) // Default 10MB file size limit
|
||||
viper.SetDefault("server.cleanup_queues", false) // Default to false for production safety
|
||||
|
||||
// JWT defaults
|
||||
viper.SetDefault("jwt.secret", "default-secret-key-change-in-production")
|
||||
viper.SetDefault("jwt.access_token_expiration", "24h")
|
||||
viper.SetDefault("jwt.refresh_token_expiration", "168h") // 7 days
|
||||
|
||||
// OAuth defaults
|
||||
viper.SetDefault("oauth.google.client_id", "")
|
||||
viper.SetDefault("oauth.google.client_secret", "")
|
||||
viper.SetDefault("oauth.google.redirect_url", "")
|
||||
viper.SetDefault("oauth.google.scopes", []string{"openid", "profile", "email"})
|
||||
viper.SetDefault("oauth.github.client_id", "")
|
||||
viper.SetDefault("oauth.github.client_secret", "")
|
||||
viper.SetDefault("oauth.github.redirect_url", "")
|
||||
viper.SetDefault("oauth.github.scopes", []string{"user:email"})
|
||||
viper.SetDefault("oauth.linkedin.client_id", "")
|
||||
viper.SetDefault("oauth.linkedin.client_secret", "")
|
||||
viper.SetDefault("oauth.linkedin.redirect_url", "")
|
||||
viper.SetDefault("oauth.linkedin.scopes", []string{"r_liteprofile", "r_emailaddress"})
|
||||
viper.SetDefault("oauth.mock.enabled", false)
|
||||
viper.SetDefault("oauth.mock.base_url", "http://localhost:9999")
|
||||
viper.SetDefault("oauth.mock.client_id", "mock-client")
|
||||
viper.SetDefault("oauth.mock.client_secret", "mock-secret")
|
||||
viper.SetDefault("oauth.mock.redirect_url", "http://localhost:3000/auth/callback")
|
||||
|
||||
// RabbitMQ defaults
|
||||
viper.SetDefault("rabbitmq.host", "localhost")
|
||||
viper.SetDefault("rabbitmq.port", "5678")
|
||||
viper.SetDefault("rabbitmq.user", "root")
|
||||
viper.SetDefault("rabbitmq.password", "root")
|
||||
viper.SetDefault("rabbitmq.vhost", "/")
|
||||
viper.SetDefault("rabbitmq.exchange", "base_exchange")
|
||||
viper.SetDefault("rabbitmq.internal_exchange", "base_internal_exchange")
|
||||
viper.SetDefault("rabbitmq.max_channels", "1")
|
||||
viper.SetDefault("rabbitmq.max_retry_attempts", 3)
|
||||
viper.SetDefault("rabbitmq.prefetch_count", 5)
|
||||
viper.SetDefault("rabbitmq.retry_ttl", "10s")
|
||||
|
||||
// BarcodeMapping defaults
|
||||
viper.SetDefault("barcodemapping.host", "localhost")
|
||||
viper.SetDefault("barcodemapping.port", "8081")
|
||||
viper.SetDefault("barcodemapping.cache", false)
|
||||
viper.SetDefault("barcodemapping.prefix", "barcodemapping")
|
||||
viper.SetDefault("barcodemapping.ttl", "5m")
|
||||
|
||||
// Discount defaults
|
||||
viper.SetDefault("discount.url", "http://localhost")
|
||||
viper.SetDefault("discount.port", 8082)
|
||||
viper.SetDefault("discount.cache", false)
|
||||
viper.SetDefault("discount.prefix", "discount")
|
||||
viper.SetDefault("discount.ttl", "30s")
|
||||
|
||||
// Syslog defaults
|
||||
viper.SetDefault("syslog.host", "localhost")
|
||||
viper.SetDefault("syslog.port", "1514")
|
||||
viper.SetDefault("syslog.protocol", "udp")
|
||||
viper.SetDefault("syslog.log_level", "INFO")
|
||||
|
||||
// Auth defaults
|
||||
viper.SetDefault("vendorauth.url", "")
|
||||
viper.SetDefault("vendorauth.client_id", "base")
|
||||
viper.SetDefault("vendorauth.client_secret", "")
|
||||
viper.SetDefault("vendorauth.cache", false)
|
||||
viper.SetDefault("vendorauth.prefix", "vendorauth")
|
||||
viper.SetDefault("vendorauth.ttl", "5m")
|
||||
|
||||
viper.SetDefault("authshield.url", "http://chart-shield.auth-shield.svc.cluster.local/auth-shield/authorize")
|
||||
viper.SetDefault("authshield.cache", false)
|
||||
viper.SetDefault("authshield.prefix", "authshield")
|
||||
viper.SetDefault("authshield.ttl", "5m")
|
||||
|
||||
// Metrics defaults
|
||||
viper.SetDefault("metrics.enabled", true)
|
||||
viper.SetDefault("metrics.service_name", "base")
|
||||
viper.SetDefault("metrics.path", "/metrics")
|
||||
viper.SetDefault("metrics.port", "8080")
|
||||
|
||||
// Bind environment variables explicitly for better control
|
||||
bindEnvVars()
|
||||
|
||||
// Try to read config file, but don't fail if it doesn't exist
|
||||
if err := viper.ReadInConfig(); err != nil {
|
||||
var configFileNotFoundError viper.ConfigFileNotFoundError
|
||||
if errors.As(err, &configFileNotFoundError) {
|
||||
fmt.Println("Config file not found, using environment variables and defaults")
|
||||
} else {
|
||||
return nil, fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
config := &AppConfig{
|
||||
Name: viper.GetString("name"),
|
||||
Environment: viper.GetString("environment"),
|
||||
Port: viper.GetInt("port"),
|
||||
VendorLockTimeOut: viper.GetInt("vendor_lock_time_out"),
|
||||
RabbitMQ: RabbitMQConfig{
|
||||
Host: viper.GetString("rabbitmq.host"),
|
||||
Port: viper.GetInt("rabbitmq.port"),
|
||||
User: viper.GetString("rabbitmq.user"),
|
||||
Password: viper.GetString("rabbitmq.password"),
|
||||
VHost: viper.GetString("rabbitmq.vhost"),
|
||||
MaxConnections: viper.GetInt("rabbitmq.max_connections"),
|
||||
MaxChannels: viper.GetInt("rabbitmq.max_channels"),
|
||||
ConnectionTimeout: viper.GetDuration("rabbitmq.connection_timeout"),
|
||||
HeartbeatInterval: viper.GetDuration("rabbitmq.heartbeat_interval"),
|
||||
ReconnectDelay: viper.GetDuration("rabbitmq.reconnect_delay"),
|
||||
MaxReconnectDelay: viper.GetDuration("rabbitmq.max_reconnect_delay"),
|
||||
ReconnectAttempts: viper.GetInt("rabbitmq.reconnect_attempts"),
|
||||
EnableAutoReconnect: viper.GetBool("rabbitmq.enable_auto_reconnect"),
|
||||
LogLevel: viper.GetString("rabbitmq.log_level"),
|
||||
MaxRetryAttempts: viper.GetInt("rabbitmq.max_retry_attempts"),
|
||||
PrefetchCount: viper.GetInt("rabbitmq.prefetch_count"),
|
||||
RetryTTL: viper.GetDuration("rabbitmq.retry_ttl"),
|
||||
},
|
||||
Database: DatabaseConfig{
|
||||
User: viper.GetString("database.user"),
|
||||
Password: viper.GetString("database.password"),
|
||||
Host: viper.GetString("database.host"),
|
||||
Port: viper.GetString("database.port"),
|
||||
Name: viper.GetString("database.name"),
|
||||
MaxIdleConns: viper.GetInt("database.max_idle_conns"),
|
||||
MaxOpenConns: viper.GetInt("database.max_open_conns"),
|
||||
ConnMaxIdleTime: viper.GetString("database.conn_max_idle_time"),
|
||||
ConnMaxLifetime: viper.GetString("database.conn_max_lifetime"),
|
||||
},
|
||||
Redis: RedisConfig{
|
||||
Host: viper.GetString("redis.host"),
|
||||
Port: viper.GetString("redis.port"),
|
||||
Password: viper.GetString("redis.password"),
|
||||
Database: viper.GetInt("redis.database"),
|
||||
URL: viper.GetString("redis.url"),
|
||||
},
|
||||
Server: ServerConfig{
|
||||
Domain: viper.GetString("server.host"),
|
||||
HTTPPort: viper.GetString("server.port"),
|
||||
GRPCPort: viper.GetString("server.port"),
|
||||
RPCHost: viper.GetString("rpc.host"),
|
||||
RPCPort: viper.GetString("rpc.port"),
|
||||
WebHost: viper.GetString("web.host"),
|
||||
WebPort: viper.GetString("web.port"),
|
||||
MaxFileSizeMB: viper.GetInt("server.max_file_size_mb"),
|
||||
CleanupQueues: viper.GetBool("server.cleanup_queues"),
|
||||
JWTSecret: viper.GetString("server.jwt_secret"),
|
||||
},
|
||||
Syslog: SyslogConfig{
|
||||
Host: viper.GetString("syslog.host"),
|
||||
Port: viper.GetString("syslog.port"),
|
||||
Protocol: viper.GetString("syslog.protocol"),
|
||||
LogLevel: viper.GetString("syslog.log_level"),
|
||||
},
|
||||
Metrics: MetricsConfig{
|
||||
Enabled: viper.GetBool("metrics.enabled"),
|
||||
ServiceName: viper.GetString("metrics.service_name"),
|
||||
Path: viper.GetString("metrics.path"),
|
||||
Port: viper.GetString("metrics.port"),
|
||||
},
|
||||
AzureCommunicationConfig: AzureCommunicationConfig{
|
||||
Endpoint: GetStringOrDefault("email.endpoint", ""),
|
||||
AccessKey: GetStringOrDefault("email.access_key", ""),
|
||||
ApiVersion: GetStringOrDefault("email.api_version", "2025-03-01"),
|
||||
SenderAddress: GetStringOrDefault("email.sender_address", "no-reply@alinme.com"),
|
||||
},
|
||||
AzureBlobStorage: AzureBlobStorageConfig{
|
||||
BlobEndpoint: GetStringOrDefault("azure_blob_storage.blob_endpoint", "https://alinmestorage.blob.core.windows.net"),
|
||||
},
|
||||
PgDatabaseConfig: PgDatabaseConfig{
|
||||
Host: viper.GetString("pg_database.host"),
|
||||
Port: viper.GetInt("pg_database.port"),
|
||||
User: viper.GetString("pg_database.user"),
|
||||
Password: viper.GetString("pg_database.password"),
|
||||
Name: viper.GetString("pg_database.name"),
|
||||
SSLMode: viper.GetString("pg_database.ssl_mode"),
|
||||
ConnectionTimeout: viper.GetDuration("pg_database.connection_timeout"),
|
||||
QueryTimeout: viper.GetDuration("pg_database.query_timeout"),
|
||||
PoolConfig: PgPoolConfig{
|
||||
MaxConn: viper.GetInt32("pg_database.pool_config.max_conn"),
|
||||
MinConn: viper.GetInt32("pg_database.pool_config.min_conn"),
|
||||
MaxConnLifetime: viper.GetDuration("pg_database.pool_config.max_conn_lifetime"),
|
||||
MaxConnIdleTime: viper.GetDuration("pg_database.pool_config.max_conn_idle_time"),
|
||||
},
|
||||
Migrations: MigrationsConfig{
|
||||
Enabled: viper.GetBool("pg_database.migrations.enabled"),
|
||||
Dir: viper.GetString("pg_database.migrations.dir"),
|
||||
},
|
||||
},
|
||||
JWT: func() JWTConfig {
|
||||
// Use jwt.secret if set, otherwise fall back to server.jwt_secret for backward compatibility
|
||||
secret := viper.GetString("jwt.secret")
|
||||
if secret == "" || secret == "default-secret-key-change-in-production" {
|
||||
if serverSecret := viper.GetString("server.jwt_secret"); serverSecret != "" {
|
||||
secret = serverSecret
|
||||
}
|
||||
}
|
||||
return JWTConfig{
|
||||
Secret: secret,
|
||||
AccessTokenExpiration: viper.GetDuration("jwt.access_token_expiration"),
|
||||
RefreshTokenExpiration: viper.GetDuration("jwt.refresh_token_expiration"),
|
||||
}
|
||||
}(),
|
||||
OAuth: OAuthConfig{
|
||||
Google: OAuthProviderConfig{
|
||||
ClientID: viper.GetString("oauth.google.client_id"),
|
||||
ClientSecret: viper.GetString("oauth.google.client_secret"),
|
||||
RedirectURL: viper.GetString("oauth.google.redirect_url"),
|
||||
Scopes: viper.GetStringSlice("oauth.google.scopes"),
|
||||
},
|
||||
GitHub: OAuthProviderConfig{
|
||||
ClientID: viper.GetString("oauth.github.client_id"),
|
||||
ClientSecret: viper.GetString("oauth.github.client_secret"),
|
||||
RedirectURL: viper.GetString("oauth.github.redirect_url"),
|
||||
Scopes: viper.GetStringSlice("oauth.github.scopes"),
|
||||
},
|
||||
LinkedIn: OAuthProviderConfig{
|
||||
ClientID: viper.GetString("oauth.linkedin.client_id"),
|
||||
ClientSecret: viper.GetString("oauth.linkedin.client_secret"),
|
||||
RedirectURL: viper.GetString("oauth.linkedin.redirect_url"),
|
||||
Scopes: viper.GetStringSlice("oauth.linkedin.scopes"),
|
||||
},
|
||||
Mock: OAuthMockConfig{
|
||||
Enabled: viper.GetBool("oauth.mock.enabled"),
|
||||
BaseURL: viper.GetString("oauth.mock.base_url"),
|
||||
OAuthProviderConfig: OAuthProviderConfig{
|
||||
ClientID: viper.GetString("oauth.mock.client_id"),
|
||||
ClientSecret: viper.GetString("oauth.mock.client_secret"),
|
||||
RedirectURL: viper.GetString("oauth.mock.redirect_url"),
|
||||
Scopes: viper.GetStringSlice("oauth.mock.scopes"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// bindEnvVars explicitly binds environment variables for better control
|
||||
func bindEnvVars() {
|
||||
// Application level
|
||||
BindEnv("environment", "base_ENVIRONMENT")
|
||||
BindEnv("name", "APP_NAME")
|
||||
BindEnv("debug", "base_DEBUG")
|
||||
BindEnv("port", "base_PORT")
|
||||
BindEnv("vendor_lock_time_out", "base_VENDOR_LOCK_TIMEOUT")
|
||||
|
||||
// Database
|
||||
BindEnv("database.host", "base_DB_HOST")
|
||||
BindEnv("database.port", "base_DB_PORT")
|
||||
BindEnv("database.user", "base_DB_USERNAME")
|
||||
BindEnv("database.password", "base_DB_PASSWORD")
|
||||
BindEnv("database.name", "base_DB_NAME")
|
||||
BindEnv("database.max_idle_conns", "base_DB_MAX_IDLE_CONNS")
|
||||
BindEnv("database.max_open_conns", "base_DB_MAX_OPEN_CONNS")
|
||||
BindEnv("database.conn_max_idle_time", "base_DB_CONN_MAX_IDLE_TIME")
|
||||
BindEnv("database.conn_max_lifetime", "base_DB_CONN_MAX_LIFETIME")
|
||||
BindEnv("database.ro_user", "base_DB_RO_USERNAME")
|
||||
BindEnv("database.ro_password", "base_DB_RO_PASSWORD")
|
||||
BindEnv("database.ro_max_idle_conns", "base_DB_RO_MAX_IDLE_CONNS")
|
||||
BindEnv("database.ro_max_open_conns", "base_DB_RO_MAX_OPEN_CONNS")
|
||||
|
||||
// Redis
|
||||
BindEnv("redis.host", "base_REDIS_HOST")
|
||||
BindEnv("redis.port", "base_REDIS_PORT")
|
||||
BindEnv("redis.password", "base_REDIS_PASSWORD")
|
||||
BindEnv("redis.database", "base_REDIS_DATABASE")
|
||||
BindEnv("redis.url", "base_REDIS_URL")
|
||||
|
||||
// Server
|
||||
BindEnv("server.host", "base_SERVER_HOST")
|
||||
BindEnv("server.port", "base_SERVER_PORT")
|
||||
BindEnv("rpc.host", "base_RPC_HOST")
|
||||
BindEnv("rpc.port", "base_RPC_PORT")
|
||||
BindEnv("web.host", "base_WEB_HOST")
|
||||
BindEnv("web.port", "base_WEB_PORT")
|
||||
BindEnv("server.max_file_size_mb", "base_SERVER_MAX_FILE_SIZE_MB")
|
||||
BindEnv("server.cleanup_queues", "base_SERVER_CLEANUP_QUEUES")
|
||||
BindEnv("server.jwt_secret", "base_SERVER_JWT_SECRET")
|
||||
|
||||
// JWT
|
||||
BindEnv("jwt.secret", "base_JWT_SECRET")
|
||||
BindEnv("jwt.access_token_expiration", "base_JWT_ACCESS_TOKEN_EXPIRATION")
|
||||
BindEnv("jwt.refresh_token_expiration", "base_JWT_REFRESH_TOKEN_EXPIRATION")
|
||||
|
||||
// OAuth
|
||||
BindEnv("oauth.google.client_id", "base_OAUTH_GOOGLE_CLIENT_ID")
|
||||
BindEnv("oauth.google.client_secret", "base_OAUTH_GOOGLE_CLIENT_SECRET")
|
||||
BindEnv("oauth.google.redirect_url", "base_OAUTH_GOOGLE_REDIRECT_URL")
|
||||
BindEnv("oauth.github.client_id", "base_OAUTH_GITHUB_CLIENT_ID")
|
||||
BindEnv("oauth.github.client_secret", "base_OAUTH_GITHUB_CLIENT_SECRET")
|
||||
BindEnv("oauth.github.redirect_url", "base_OAUTH_GITHUB_REDIRECT_URL")
|
||||
BindEnv("oauth.linkedin.client_id", "base_OAUTH_LINKEDIN_CLIENT_ID")
|
||||
BindEnv("oauth.linkedin.client_secret", "base_OAUTH_LINKEDIN_CLIENT_SECRET")
|
||||
BindEnv("oauth.linkedin.redirect_url", "base_OAUTH_LINKEDIN_REDIRECT_URL")
|
||||
BindEnv("oauth.mock.enabled", "base_OAUTH_MOCK_ENABLED")
|
||||
BindEnv("oauth.mock.base_url", "base_OAUTH_MOCK_BASE_URL")
|
||||
BindEnv("oauth.mock.client_id", "base_OAUTH_MOCK_CLIENT_ID")
|
||||
BindEnv("oauth.mock.client_secret", "base_OAUTH_MOCK_CLIENT_SECRET")
|
||||
BindEnv("oauth.mock.redirect_url", "base_OAUTH_MOCK_REDIRECT_URL")
|
||||
|
||||
// RabbitMQ
|
||||
BindEnv("rabbitmq.host", "base_RABBITMQ_HOST")
|
||||
BindEnv("rabbitmq.port", "base_RABBITMQ_PORT")
|
||||
BindEnv("rabbitmq.user", "base_RABBIT_USERNAME")
|
||||
BindEnv("rabbitmq.password", "base_RABBIT_PASSWORD")
|
||||
BindEnv("rabbitmq.vhost", "base_RABBITMQ_VHOST")
|
||||
BindEnv("rabbitmq.max_connections", "base_RABBITMQ_MAX_CONNECTIONS")
|
||||
BindEnv("rabbitmq.max_channels", "base_RABBITMQ_MAX_CHANNELS")
|
||||
BindEnv("rabbitmq.connection_timeout", "base_RABBITMQ_CONNECTION_TIMEOUT")
|
||||
BindEnv("rabbitmq.heartbeat_interval", "base_RABBITMQ_HEARTBEAT_INTERVAL")
|
||||
BindEnv("rabbitmq.reconnect_delay", "base_RABBITMQ_RECONNECT_DELAY")
|
||||
BindEnv("rabbitmq.max_reconnect_delay", "base_RABBITMQ_MAX_RECONNECT_DELAY")
|
||||
BindEnv("rabbitmq.reconnect_attempts", "base_RABBITMQ_RECONNECT_ATTEMPTS")
|
||||
BindEnv("rabbitmq.enable_auto_reconnect", "base_RABBITMQ_ENABLE_AUTO_RECONNECT")
|
||||
BindEnv("rabbitmq.log_level", "base_RABBITMQ_LOG_LEVEL")
|
||||
BindEnv("rabbitmq.max_retry_attempts", "base_RABBITMQ_MAX_RETRY_ATTEMPTS")
|
||||
BindEnv("rabbitmq.prefetch_count", "base_RABBITMQ_PREFETCH_COUNT")
|
||||
BindEnv("rabbitmq.retry_ttl", "base_RABBITMQ_RETRY_TTL")
|
||||
|
||||
// Azure Blob Storage
|
||||
BindEnv("azure_blob_storage.blob_endpoint", "base_AZURE_BLOB_STORAGE_BLOB_ENDPOINT")
|
||||
|
||||
// Azure Service Bus
|
||||
BindEnv("azure_service_bus.connection_string", "base_AZURE_SERVICE_BUS_CONNECTION_STRING")
|
||||
BindEnv("azure_service_bus.namespace", "base_AZURE_SERVICE_BUS_NAMESPACE")
|
||||
BindEnv("azure_service_bus.use_managed_identity", "base_AZURE_SERVICE_BUS_USE_MANAGED_IDENTITY")
|
||||
|
||||
// BarcodeMapping
|
||||
BindEnv("barcodemapping.host", "base_BARCODEMAPPING_HOST")
|
||||
BindEnv("barcodemapping.port", "base_BARCODEMAPPING_PORT")
|
||||
BindEnv("barcodemapping.cache", "base_BARCODEMAPPING_CACHE")
|
||||
BindEnv("barcodemapping.ttl", "base_BARCODEMAPPING_TTL")
|
||||
BindEnv("barcodemapping.prefix", "base_BARCODEMAPPING_PREFIX")
|
||||
|
||||
// Discount
|
||||
BindEnv("discount.url", "base_DISCOUNT_URL")
|
||||
BindEnv("discount.port", "base_DISCOUNT_PORT")
|
||||
BindEnv("discount.cache", "base_DISCOUNT_CACHE")
|
||||
BindEnv("discount.ttl", "base_DISCOUNT_TTL")
|
||||
BindEnv("discount.prefix", "base_DISCOUNT_PREFIX")
|
||||
|
||||
// Syslog
|
||||
BindEnv("syslog.host", "base_SYSLOG_HOST")
|
||||
BindEnv("syslog.port", "base_SYSLOG_PORT")
|
||||
BindEnv("syslog.protocol", "base_SYSLOG_PROTOCOL")
|
||||
BindEnv("syslog.log_level", "base_SYSLOG_LOG_LEVEL")
|
||||
|
||||
// Auth
|
||||
BindEnv("vendorauth.url", "base_VENDOR_AUTH_URL")
|
||||
BindEnv("vendorauth.client_id", "base_VENDOR_AUTH_CLIENT_ID")
|
||||
BindEnv("vendorauth.client_secret", "base_VENDOR_AUTH_SECRET")
|
||||
BindEnv("vendorauth.cache", "base_VENDOR_AUTH_CACHE")
|
||||
BindEnv("vendorauth.prefix", "base_VENDOR_AUTH_PREFIX")
|
||||
BindEnv("vendorauth.ttl", "base_VENDOR_AUTH_TTL")
|
||||
|
||||
BindEnv("authshield.url", "base_AUTH_SHIELD_URL")
|
||||
BindEnv("authshield.cache", "base_AUTH_SHIELD_CACHE")
|
||||
BindEnv("authshield.prefix", "base_AUTH_SHIELD_PREFIX")
|
||||
BindEnv("authshield.ttl", "base_AUTH_SHIELD_TTL")
|
||||
|
||||
// Metrics
|
||||
BindEnv("metrics.enabled", "base_METRICS_ENABLED")
|
||||
BindEnv("metrics.service_name", "base_METRICS_SERVICE_NAME")
|
||||
BindEnv("metrics.path", "base_METRICS_PATH")
|
||||
BindEnv("metrics.port", "base_METRICS_PORT")
|
||||
|
||||
// PgDatabase
|
||||
BindEnv("pg_database.host", "base_PG_DATABASE_HOST")
|
||||
BindEnv("pg_database.port", "base_PG_DATABASE_PORT")
|
||||
BindEnv("pg_database.user", "base_PG_DATABASE_USER")
|
||||
BindEnv("pg_database.password", "base_PG_DATABASE_PASSWORD")
|
||||
BindEnv("pg_database.name", "base_PG_DATABASE_NAME")
|
||||
BindEnv("pg_database.ssl_mode", "base_PG_DATABASE_SSL_MODE")
|
||||
BindEnv("pg_database.connection_timeout", "base_PG_DATABASE_CONNECTION_TIMEOUT")
|
||||
BindEnv("pg_database.query_timeout", "base_PG_DATABASE_QUERY_TIMEOUT")
|
||||
BindEnv("pg_database.pool_config.max_conn", "base_PG_DATABASE_POOL_CONFIG_MAX_CONN")
|
||||
BindEnv("pg_database.pool_config.min_conn", "base_PG_DATABASE_POOL_CONFIG_MIN_CONN")
|
||||
BindEnv("pg_database.pool_config.max_conn_lifetime", "base_PG_DATABASE_POOL_CONFIG_MAX_CONN_LIFETIME")
|
||||
BindEnv("pg_database.pool_config.max_conn_idle_time", "base_PG_DATABASE_POOL_CONFIG_MAX_CONN_IDLE_TIME")
|
||||
BindEnv("pg_database.migrations.enabled", "base_PG_DATABASE_MIGRATIONS_ENABLED")
|
||||
BindEnv("pg_database.migrations.dir", "base_PG_DATABASE_MIGRATIONS_DIR")
|
||||
}
|
||||
|
||||
func BindEnv(input, envKey string) {
|
||||
if err := viper.BindEnv(input, envKey); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func GetStringOrDefault(key string, defaultValue string) string {
|
||||
if viper.IsSet(key) {
|
||||
return viper.GetString(key)
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
40
config/database.go
Normal file
40
config/database.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// DatabaseConfig holds configuration for database
|
||||
type DatabaseConfig struct {
|
||||
User string
|
||||
Password string
|
||||
Host string
|
||||
Port string
|
||||
Name string
|
||||
MaxIdleConns int
|
||||
MaxOpenConns int
|
||||
ConnMaxIdleTime string
|
||||
ConnMaxLifetime string
|
||||
}
|
||||
|
||||
type PgDatabaseConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
Name string
|
||||
SSLMode string
|
||||
ConnectionTimeout time.Duration
|
||||
QueryTimeout time.Duration
|
||||
PoolConfig PgPoolConfig
|
||||
Migrations MigrationsConfig
|
||||
}
|
||||
|
||||
type MigrationsConfig struct {
|
||||
Enabled bool
|
||||
Dir string
|
||||
}
|
||||
type PgPoolConfig struct {
|
||||
MaxConn int32
|
||||
MinConn int32
|
||||
MaxConnLifetime time.Duration
|
||||
MaxConnIdleTime time.Duration
|
||||
}
|
||||
7
config/env.go
Normal file
7
config/env.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package config
|
||||
|
||||
const (
|
||||
Local = "local"
|
||||
Prod = "prod"
|
||||
Stage = "stage"
|
||||
)
|
||||
10
config/jwt.go
Normal file
10
config/jwt.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package config
|
||||
|
||||
import "time"
|
||||
|
||||
// JWTConfig holds configuration for JWT token service
|
||||
type JWTConfig struct {
|
||||
Secret string `mapstructure:"secret"` // JWT secret key for token signing
|
||||
AccessTokenExpiration time.Duration `mapstructure:"access_token_expiration"` // Access token expiration duration
|
||||
RefreshTokenExpiration time.Duration `mapstructure:"refresh_token_expiration"` // Refresh token expiration duration
|
||||
}
|
||||
9
config/metrics.go
Normal file
9
config/metrics.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package config
|
||||
|
||||
// MetricsConfig holds configuration for metrics
|
||||
type MetricsConfig struct {
|
||||
Enabled bool
|
||||
ServiceName string
|
||||
Path string
|
||||
Port string
|
||||
}
|
||||
24
config/oauth.go
Normal file
24
config/oauth.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package config
|
||||
|
||||
// OAuthConfig holds configuration for OAuth providers
|
||||
type OAuthConfig struct {
|
||||
Google OAuthProviderConfig `mapstructure:"google"`
|
||||
GitHub OAuthProviderConfig `mapstructure:"github"`
|
||||
LinkedIn OAuthProviderConfig `mapstructure:"linkedin"`
|
||||
Mock OAuthMockConfig `mapstructure:"mock"`
|
||||
}
|
||||
|
||||
// OAuthMockConfig holds configuration for the mock OAuth server (local dev only)
|
||||
type OAuthMockConfig struct {
|
||||
Enabled bool `mapstructure:"enabled"`
|
||||
BaseURL string `mapstructure:"base_url"` // e.g. http://localhost:9999
|
||||
OAuthProviderConfig
|
||||
}
|
||||
|
||||
// OAuthProviderConfig holds configuration for a single OAuth provider
|
||||
type OAuthProviderConfig struct {
|
||||
ClientID string `mapstructure:"client_id"`
|
||||
ClientSecret string `mapstructure:"client_secret"` // Ov23liSo5eCfXJ11k7mr
|
||||
RedirectURL string `mapstructure:"redirect_url"`
|
||||
Scopes []string `mapstructure:"scopes"`
|
||||
}
|
||||
38
config/rabbit.go
Normal file
38
config/rabbit.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// RabbitMQConfig holds configuration for RabbitMQ
|
||||
type RabbitMQConfig struct {
|
||||
Host string
|
||||
Port int
|
||||
User string
|
||||
Password string
|
||||
VHost string
|
||||
MaxConnections int
|
||||
MaxChannels int
|
||||
ConnectionTimeout time.Duration
|
||||
HeartbeatInterval time.Duration
|
||||
ReconnectDelay time.Duration
|
||||
MaxReconnectDelay time.Duration
|
||||
ReconnectAttempts int
|
||||
EnableAutoReconnect bool
|
||||
LogLevel string
|
||||
MaxRetryAttempts int
|
||||
PrefetchCount int
|
||||
RetryTTL time.Duration
|
||||
}
|
||||
|
||||
// RabbitMQConnectionString returns the formatted RabbitMQ connection URI
|
||||
func (c *AppConfig) RabbitMQConnectionString() string {
|
||||
return fmt.Sprintf("amqp://%s:%s@%s:%d/%s",
|
||||
c.RabbitMQ.User,
|
||||
c.RabbitMQ.Password,
|
||||
c.RabbitMQ.Host,
|
||||
c.RabbitMQ.Port,
|
||||
c.RabbitMQ.VHost,
|
||||
)
|
||||
}
|
||||
10
config/redis.go
Normal file
10
config/redis.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package config
|
||||
|
||||
// RedisConfig holds configuration for Redis
|
||||
type RedisConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
Password string
|
||||
Database int
|
||||
URL string
|
||||
}
|
||||
25
config/server.go
Normal file
25
config/server.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package config
|
||||
|
||||
// ServerConfig holds configuration for HTTP server
|
||||
type ServerConfig struct {
|
||||
Domain string `mapstructure:"domain"`
|
||||
HTTPPort string `mapstructure:"http_port"`
|
||||
GRPCPort string `mapstructure:"grpc_port"`
|
||||
RPCHost string `mapstructure:"rpc_host"`
|
||||
RPCPort string `mapstructure:"rpc_port"`
|
||||
WebHost string `mapstructure:"web_host"`
|
||||
WebPort string `mapstructure:"web_port"`
|
||||
MaxFileSize int64 `mapstructure:"max_file_size"` // Maximum file size in bytes
|
||||
MaxFileSizeMB int `mapstructure:"max_file_size_mb"` // Maximum file size in MB (for convenience)
|
||||
CleanupQueues bool `mapstructure:"cleanup_queues"` // Whether to cleanup existing queues on startup
|
||||
JWTSecret string `mapstructure:"jwt_secret"` // JWT secret key for token signing
|
||||
}
|
||||
|
||||
// GetMaxFileSizeBytes returns the maximum file size in bytes
|
||||
func (s *ServerConfig) GetMaxFileSizeBytes() int64 {
|
||||
if s.MaxFileSize > 0 {
|
||||
return s.MaxFileSize
|
||||
}
|
||||
// Convert MB to bytes if MaxFileSize is not set
|
||||
return int64(s.MaxFileSizeMB) * 1024 * 1024
|
||||
}
|
||||
8
config/syslog.go
Normal file
8
config/syslog.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package config
|
||||
|
||||
type SyslogConfig struct {
|
||||
Host string
|
||||
Port string
|
||||
Protocol string
|
||||
LogLevel string
|
||||
}
|
||||
57
database/migrations/20251228071211.sql
Normal file
57
database/migrations/20251228071211.sql
Normal file
@@ -0,0 +1,57 @@
|
||||
-- Create "users" table
|
||||
CREATE TABLE "public"."users" (
|
||||
"id" uuid NOT NULL,
|
||||
"first_name" text NOT NULL,
|
||||
"last_name" text NOT NULL,
|
||||
"display_name" text NOT NULL,
|
||||
"email" text NOT NULL,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamptz NULL DEFAULT now(),
|
||||
"deleted_at" timestamptz NULL,
|
||||
PRIMARY KEY ("id"),
|
||||
CONSTRAINT "users_email_unique" UNIQUE ("email")
|
||||
);
|
||||
-- Create "accounts" table
|
||||
CREATE TABLE "public"."accounts" (
|
||||
"id" uuid NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"password" text NULL,
|
||||
"access_token" text NULL,
|
||||
"refresh_token" text NULL,
|
||||
"scope" text NULL,
|
||||
"meta" jsonb NULL,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamptz NULL DEFAULT now(),
|
||||
"deleted_at" timestamptz NULL,
|
||||
PRIMARY KEY ("id"),
|
||||
CONSTRAINT "accounts_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
-- Create index "accounts_user_id_idx" to table: "accounts"
|
||||
CREATE INDEX "accounts_user_id_idx" ON "public"."accounts" ("user_id");
|
||||
-- Create "roles" table
|
||||
CREATE TABLE "public"."roles" (
|
||||
"id" uuid NOT NULL,
|
||||
"name" text NOT NULL,
|
||||
"description" text NULL,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamptz NULL DEFAULT now(),
|
||||
"deleted_at" timestamptz NULL,
|
||||
PRIMARY KEY ("id"),
|
||||
CONSTRAINT "roles_name_unique" UNIQUE ("name")
|
||||
);
|
||||
-- Create "user_roles" table
|
||||
CREATE TABLE "public"."user_roles" (
|
||||
"id" uuid NOT NULL,
|
||||
"user_id" uuid NOT NULL,
|
||||
"role_id" uuid NOT NULL,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamptz NULL DEFAULT now(),
|
||||
"deleted_at" timestamptz NULL,
|
||||
PRIMARY KEY ("id"),
|
||||
CONSTRAINT "user_roles_role_id_fk" FOREIGN KEY ("role_id") REFERENCES "public"."roles" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "user_roles_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users" ("id") ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
-- Create index "user_roles_role_id_idx" to table: "user_roles"
|
||||
CREATE INDEX "user_roles_role_id_idx" ON "public"."user_roles" ("role_id");
|
||||
-- Create index "user_roles_user_id_idx" to table: "user_roles"
|
||||
CREATE INDEX "user_roles_user_id_idx" ON "public"."user_roles" ("user_id");
|
||||
21
database/migrations/20260103095921.sql
Normal file
21
database/migrations/20260103095921.sql
Normal file
@@ -0,0 +1,21 @@
|
||||
-- Create "cache_hash" table
|
||||
CREATE TABLE "public"."cache_hash" (
|
||||
"key" text NOT NULL,
|
||||
"field" text NOT NULL,
|
||||
"value" jsonb NOT NULL,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"expires_at" timestamptz NULL,
|
||||
PRIMARY KEY ("key", "field")
|
||||
);
|
||||
-- Create index "idx_cache_hash_expires_at" to table: "cache_hash"
|
||||
CREATE INDEX "idx_cache_hash_expires_at" ON "public"."cache_hash" ("expires_at");
|
||||
-- Create "cache_kv" table
|
||||
CREATE TABLE "public"."cache_kv" (
|
||||
"key" text NOT NULL,
|
||||
"value" jsonb NOT NULL,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"expires_at" timestamptz NULL,
|
||||
PRIMARY KEY ("key")
|
||||
);
|
||||
-- Create index "idx_cache_kv_expires_at" to table: "cache_kv"
|
||||
CREATE INDEX "idx_cache_kv_expires_at" ON "public"."cache_kv" ("expires_at");
|
||||
6
database/migrations/20260103102028.sql
Normal file
6
database/migrations/20260103102028.sql
Normal file
@@ -0,0 +1,6 @@
|
||||
-- Set comment to schema: "public"
|
||||
COMMENT ON SCHEMA "public" IS 'Standard public schema';
|
||||
-- Add new schema named "platform"
|
||||
CREATE SCHEMA "platform";
|
||||
-- Set comment to schema: "platform"
|
||||
COMMENT ON SCHEMA "platform" IS 'Platform schema for cache tables';
|
||||
34
database/migrations/20260108092338.sql
Normal file
34
database/migrations/20260108092338.sql
Normal file
@@ -0,0 +1,34 @@
|
||||
-- Create "profiles" table
|
||||
CREATE TABLE "public"."profiles" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "user_id" uuid NULL, "handle" text NOT NULL, "role_id" uuid NULL, "role_name" character varying(100) NULL, "first_name" text NULL, "last_name" text NULL, "company" text NULL, "short_description" text NULL, "resume_link" text NULL, "cta_enabled" boolean NOT NULL DEFAULT false, "avatar" text NULL, "profile_picture" text NULL, "about" text NULL, "email" text NULL, "phone" text NULL, "visibility_level" text NOT NULL DEFAULT 'public', "page_section_order" jsonb NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "deleted_at" timestamptz NULL, PRIMARY KEY ("id"), CONSTRAINT "profiles_handle_unique" UNIQUE ("handle"));
|
||||
-- Create index "profiles_company_idx" to table: "profiles"
|
||||
CREATE INDEX "profiles_company_idx" ON "public"."profiles" ("company");
|
||||
-- Create index "profiles_deleted_at_idx" to table: "profiles"
|
||||
CREATE INDEX "profiles_deleted_at_idx" ON "public"."profiles" ("deleted_at");
|
||||
-- Create index "profiles_email_idx" to table: "profiles"
|
||||
CREATE INDEX "profiles_email_idx" ON "public"."profiles" ("email");
|
||||
-- Create index "profiles_name_idx" to table: "profiles"
|
||||
CREATE INDEX "profiles_name_idx" ON "public"."profiles" ("first_name", "last_name");
|
||||
-- Create index "profiles_role_id_idx" to table: "profiles"
|
||||
CREATE INDEX "profiles_role_id_idx" ON "public"."profiles" ("role_id");
|
||||
-- Create index "profiles_user_id_idx" to table: "profiles"
|
||||
CREATE INDEX "profiles_user_id_idx" ON "public"."profiles" ("user_id");
|
||||
-- Create "profile_achievements" table
|
||||
CREATE TABLE "public"."profile_achievements" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "profile_id" uuid NOT NULL, "title" text NOT NULL, "value" text NOT NULL, "enabled" boolean NOT NULL DEFAULT true, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "deleted_at" timestamptz NULL, PRIMARY KEY ("id"), CONSTRAINT "profile_achievements_profile_id_fk" FOREIGN KEY ("profile_id") REFERENCES "public"."profiles" ("id") ON UPDATE CASCADE ON DELETE CASCADE);
|
||||
-- Create index "achievements_profile_id_idx" to table: "profile_achievements"
|
||||
CREATE INDEX "achievements_profile_id_idx" ON "public"."profile_achievements" ("profile_id");
|
||||
-- Create index "profile_achievements_deleted_at_idx" to table: "profile_achievements"
|
||||
CREATE INDEX "profile_achievements_deleted_at_idx" ON "public"."profile_achievements" ("deleted_at");
|
||||
-- Create "profile_skills" table
|
||||
CREATE TABLE "public"."profile_skills" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "profile_id" uuid NOT NULL, "skill_name" text NOT NULL, "level" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "deleted_at" timestamptz NULL, PRIMARY KEY ("id"), CONSTRAINT "profile_skills_profile_id_fk" FOREIGN KEY ("profile_id") REFERENCES "public"."profiles" ("id") ON UPDATE CASCADE ON DELETE CASCADE);
|
||||
-- Create index "profile_skills_deleted_at_idx" to table: "profile_skills"
|
||||
CREATE INDEX "profile_skills_deleted_at_idx" ON "public"."profile_skills" ("deleted_at");
|
||||
-- Create index "skills_name_idx" to table: "profile_skills"
|
||||
CREATE INDEX "skills_name_idx" ON "public"."profile_skills" ("skill_name");
|
||||
-- Create index "skills_profile_id_idx" to table: "profile_skills"
|
||||
CREATE INDEX "skills_profile_id_idx" ON "public"."profile_skills" ("profile_id");
|
||||
-- Create "profile_social_links" table
|
||||
CREATE TABLE "public"."profile_social_links" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "profile_id" uuid NOT NULL, "link_type" text NOT NULL, "link" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "deleted_at" timestamptz NULL, PRIMARY KEY ("id"), CONSTRAINT "profile_social_links_profile_id_fk" FOREIGN KEY ("profile_id") REFERENCES "public"."profiles" ("id") ON UPDATE CASCADE ON DELETE CASCADE);
|
||||
-- Create index "profile_social_links_deleted_at_idx" to table: "profile_social_links"
|
||||
CREATE INDEX "profile_social_links_deleted_at_idx" ON "public"."profile_social_links" ("deleted_at");
|
||||
-- Create index "social_links_profile_id_idx" to table: "profile_social_links"
|
||||
CREATE INDEX "social_links_profile_id_idx" ON "public"."profile_social_links" ("profile_id");
|
||||
13
database/migrations/20260226000000_specialist_roles.sql
Normal file
13
database/migrations/20260226000000_specialist_roles.sql
Normal file
@@ -0,0 +1,13 @@
|
||||
-- Create "profile_roles" table (profiles.role_id references this)
|
||||
CREATE TABLE "public"."profile_roles" (
|
||||
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
"title" text NOT NULL,
|
||||
"status" text NOT NULL DEFAULT 'active',
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"deleted_at" timestamptz NULL,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE INDEX "profile_roles_status_idx" ON "public"."profile_roles" ("status");
|
||||
CREATE INDEX "profile_roles_deleted_at_idx" ON "public"."profile_roles" ("deleted_at");
|
||||
35
database/migrations/20260226000001_specialist_roles_seed.sql
Normal file
35
database/migrations/20260226000001_specialist_roles_seed.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
-- Seed profile_roles with initial data
|
||||
INSERT INTO "public"."profile_roles" ("id", "title", "status") VALUES
|
||||
('0199b964-5dc0-7657-9178-2a844e23e5b5', 'Data Scientist', 'featured'),
|
||||
('0199b964-5dc0-7a1a-94c7-d68daf420e50', 'Machine Learning Engineer', 'featured'),
|
||||
('0199b964-5dc0-7759-8221-71f57f5b2b57', 'AI Engineer', 'featured'),
|
||||
('0199b964-5dc0-7b79-a268-331f39c35366', 'Data Engineer', 'featured'),
|
||||
('0199b964-5dc0-7062-b219-11733a1ab94b', 'Data Analyst', 'featured'),
|
||||
('0199b964-5dc0-7434-b105-f2ff49573fe2', 'Business Intelligence Developer', 'featured'),
|
||||
('0199b964-5dc0-77f8-be02-f76937f60ba6', 'MLOps Engineer', 'featured'),
|
||||
('0199b964-5dc0-7107-907c-6c013cbc08b9', 'AI Product Manager', 'active'),
|
||||
('0199b964-5dc0-72f9-8e0f-dfa2950a8182', 'AI Research Scientist', 'active'),
|
||||
('0199b964-5dc0-7177-829b-f3d05081201e', 'Computer Vision Engineer', 'active'),
|
||||
('0199b964-5dc0-74b7-b427-a500ddb9f435', 'NLP Engineer', 'active'),
|
||||
('0199b964-5dc0-780d-876f-a7b4d15b0ef5', 'Data Architect', 'active'),
|
||||
('0199b964-5dc0-7d3f-af44-19dc33f50b21', 'Big Data Engineer', 'active'),
|
||||
('0199b964-5dc0-7600-9a16-74f17be7ce4b', 'Cloud AI/ML Specialist', 'active'),
|
||||
('0199b964-5dc0-73c2-b9a0-78347ae945d7', 'Generative AI Specialist', 'active'),
|
||||
('0199b964-5dc0-70a8-b710-1f424a776083', 'AI Ethics Officer', 'active'),
|
||||
('0199b964-5dc0-7c87-91c0-348e6f8b43d6', 'AI Governance Manager', 'active'),
|
||||
('0199b964-5dc0-7441-b306-bc2e3d4e4152', 'Data Privacy Engineer', 'active'),
|
||||
('0199b964-5dc0-747f-97b4-c4d98a257dee', 'AI Solutions Architect', 'active'),
|
||||
('0199b964-5dc0-7fa5-8fe0-9eb7831554ed', 'Chief Data & AI Officer', 'active'),
|
||||
('0199b964-5dc0-7447-8785-f246ff9ec309', 'AI Developer Advocate', 'active'),
|
||||
('0199b964-5dc0-7b24-9b1b-c7ca8f08527f', 'AI/ML Educator & Trainer', 'active'),
|
||||
('0199b964-5dc0-756f-ab44-48169ecfbb5e', 'Technical Content Creator (AI/ML)', 'active'),
|
||||
('0199b964-5dc0-79d1-9086-c809d8989cac', 'Open Source AI Contributor', 'active'),
|
||||
('0199b964-5dc0-774e-9011-b9fe6c29f52f', 'AI Course Instructor (Udemy, Coursera, etc.)', 'active'),
|
||||
('0199b964-5dc0-7f1d-80a4-96810af9f9ac', 'AI Community Manager', 'active'),
|
||||
('0199b964-5dc0-7352-8553-edd37324ffd9', 'AI Evangelist', 'active'),
|
||||
('0199b964-5dc0-7864-a2b5-473cfd8f7aa0', 'Research Engineer (applied AI research, publishing GitHub repos)', 'active'),
|
||||
('0199b964-5dc0-762e-9a40-0cc112578498', 'Kaggle Competitor / Data Science Challenger', 'active'),
|
||||
('0199b964-5dc0-7e13-a1f4-b4ae76bb0b62', 'AI Startup Founder / Indie Hacker (building projects, sharing repos)', 'active'),
|
||||
('0199b964-5dc0-7035-bf9b-deb415d852fd', 'Freelancer', 'active'),
|
||||
('0199b964-5dc0-7702-b533-72f7c93e19d3', 'Other', 'active')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
54
database/migrations/20260226110000_asset_tables.sql
Normal file
54
database/migrations/20260226110000_asset_tables.sql
Normal file
@@ -0,0 +1,54 @@
|
||||
-- Create asset_categories table
|
||||
CREATE TABLE "public"."asset_categories" (
|
||||
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
"name" text NOT NULL,
|
||||
"icon" text,
|
||||
"color" text,
|
||||
"card_type" text,
|
||||
"featured" boolean NOT NULL DEFAULT false,
|
||||
"description" text,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"deleted_at" timestamptz,
|
||||
PRIMARY KEY ("id")
|
||||
);
|
||||
CREATE INDEX "asset_categories_deleted_at_idx" ON "public"."asset_categories" ("deleted_at");
|
||||
|
||||
-- Create assets table (references profiles and asset_categories)
|
||||
CREATE TABLE "public"."assets" (
|
||||
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
"profile_id" uuid NOT NULL,
|
||||
"status" integer NOT NULL DEFAULT 0,
|
||||
"asset_category_id" uuid NOT NULL,
|
||||
"title" text NOT NULL,
|
||||
"description" text,
|
||||
"link" text,
|
||||
"analytics" jsonb,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"deleted_at" timestamptz,
|
||||
PRIMARY KEY ("id"),
|
||||
CONSTRAINT "assets_profile_id_fk" FOREIGN KEY ("profile_id") REFERENCES "public"."profiles" ("id") ON UPDATE CASCADE ON DELETE CASCADE,
|
||||
CONSTRAINT "assets_asset_category_id_fk" FOREIGN KEY ("asset_category_id") REFERENCES "public"."asset_categories" ("id") ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "assets_profile_id_idx" ON "public"."assets" ("profile_id");
|
||||
CREATE INDEX "assets_category_id_idx" ON "public"."assets" ("asset_category_id");
|
||||
CREATE INDEX "assets_deleted_at_idx" ON "public"."assets" ("deleted_at");
|
||||
|
||||
-- Create asset_artifacts table
|
||||
CREATE TABLE "public"."asset_artifacts" (
|
||||
"id" uuid NOT NULL DEFAULT gen_random_uuid(),
|
||||
"asset_id" uuid NOT NULL,
|
||||
"type" text NOT NULL,
|
||||
"download_url" text,
|
||||
"price" integer NOT NULL DEFAULT 0,
|
||||
"title" text,
|
||||
"description" text,
|
||||
"created_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"updated_at" timestamptz NOT NULL DEFAULT now(),
|
||||
"deleted_at" timestamptz,
|
||||
PRIMARY KEY ("id"),
|
||||
CONSTRAINT "asset_artifacts_asset_id_fk" FOREIGN KEY ("asset_id") REFERENCES "public"."assets" ("id") ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX "asset_artifacts_asset_id_idx" ON "public"."asset_artifacts" ("asset_id");
|
||||
CREATE INDEX "asset_artifacts_deleted_at_idx" ON "public"."asset_artifacts" ("deleted_at");
|
||||
@@ -0,0 +1,2 @@
|
||||
-- Add role_level column to profiles (e.g. Junior, Senior, Lead)
|
||||
ALTER TABLE "public"."profiles" ADD COLUMN IF NOT EXISTS "role_level" text;
|
||||
8
database/migrations/20260227101245.sql
Normal file
8
database/migrations/20260227101245.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- Modify "accounts" table
|
||||
ALTER TABLE "public"."accounts" ADD COLUMN "provider" integer NULL;
|
||||
-- Create index "accounts_provider_idx" to table: "accounts"
|
||||
CREATE INDEX "accounts_provider_idx" ON "public"."accounts" ("provider");
|
||||
-- Modify "users" table
|
||||
ALTER TABLE "public"."users" ADD COLUMN "phone_number" text NULL, ADD COLUMN "email_verified" boolean NOT NULL DEFAULT false, ADD COLUMN "status" integer NOT NULL DEFAULT 0, ADD COLUMN "invitation_code" text NULL;
|
||||
-- Modify "profiles" table
|
||||
ALTER TABLE "public"."profiles" ADD CONSTRAINT "profiles_role_id_profile_roles_fk" FOREIGN KEY ("role_id") REFERENCES "public"."profile_roles" ("id") ON UPDATE CASCADE ON DELETE SET NULL;
|
||||
4
database/migrations/20260227104943.sql
Normal file
4
database/migrations/20260227104943.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- Modify "accounts" table
|
||||
ALTER TABLE "public"."accounts" DROP COLUMN "provider";
|
||||
-- Modify "users" table
|
||||
ALTER TABLE "public"."users" DROP COLUMN "phone_number", DROP COLUMN "email_verified", DROP COLUMN "status", DROP COLUMN "invitation_code";
|
||||
4
database/migrations/20260227114150.sql
Normal file
4
database/migrations/20260227114150.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
-- Create "skills" table
|
||||
CREATE TABLE "public"."skills" ("id" uuid NOT NULL DEFAULT gen_random_uuid(), "name" text NOT NULL, "created_at" timestamptz NOT NULL DEFAULT now(), "updated_at" timestamptz NOT NULL DEFAULT now(), "deleted_at" timestamptz NULL, PRIMARY KEY ("id"));
|
||||
-- Create index "skills_catalog_name_idx" to table: "skills"
|
||||
CREATE INDEX "skills_catalog_name_idx" ON "public"."skills" ("name");
|
||||
12
database/migrations/atlas.sum
Normal file
12
database/migrations/atlas.sum
Normal file
@@ -0,0 +1,12 @@
|
||||
h1:calC/k+XV88oQ++zbEbFg7avX0IvO4wXp6zMR2WkGR4=
|
||||
20251228071211.sql h1:XyIbEsQgrxoGw3oyfpHCAC71FUOkg0vVWEPEB7fTDME=
|
||||
20260103095921.sql h1:QxUqe3CvReDIMsCLa5DqM+RLh+cpSTJdcD3e9gRHnfw=
|
||||
20260103102028.sql h1:lqVe41QhYTMxyDflXliflCsR5nZfasaGqHXKCCL6vj4=
|
||||
20260108092338.sql h1:D1/hl9KLCi4qJFI80q6uAehHHhHCIRsqXEbS9bfm4OE=
|
||||
20260226000000_specialist_roles.sql h1:se79NTcVHJ94KCsVsxDLOQNUFEW84u+8iE3iAJ1iByc=
|
||||
20260226000001_specialist_roles_seed.sql h1:TozU48tMsL/4EL583XwU5nmvngN8EN0kXSw5WvK4Rsk=
|
||||
20260226110000_asset_tables.sql h1:WzgF2uK3oG6cqiOWrNDaB9O7wOA4/RP5XjYwzSuIz80=
|
||||
20260226120000_add_profile_role_level.sql h1:OyV/tGf8oQWCEiuetcOA+LNjCIjhSzn18iPhqOgXQko=
|
||||
20260227101245.sql h1:vkczkkzkU3sg0QsQuAqIY7IuugXONDx9AM4AlDt4EL8=
|
||||
20260227104943.sql h1:1Mb7CE7B8nF1stH/R0YcbrT8urczMr5+sfO+lMSoBPg=
|
||||
20260227114150.sql h1:LFDVdKRD7qFxlADcFYEAXF+UjaUVi3tprr61yF0tb/c=
|
||||
62
database/schema/account.pg.hcl
Normal file
62
database/schema/account.pg.hcl
Normal file
@@ -0,0 +1,62 @@
|
||||
table "accounts" {
|
||||
schema = schema.public
|
||||
|
||||
column "id" {
|
||||
type = uuid
|
||||
null = false
|
||||
}
|
||||
column "user_id" {
|
||||
type = uuid
|
||||
null = false
|
||||
}
|
||||
column "password" {
|
||||
null = true
|
||||
type = text
|
||||
}
|
||||
column "access_token" {
|
||||
null = true
|
||||
type = text
|
||||
}
|
||||
column "refresh_token" {
|
||||
null = true
|
||||
type = text
|
||||
}
|
||||
column "scope" {
|
||||
null = true
|
||||
type = text
|
||||
}
|
||||
column "meta" {
|
||||
null = true
|
||||
type = jsonb
|
||||
}
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
|
||||
column "updated_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
default = sql("now()")
|
||||
}
|
||||
column "deleted_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
|
||||
primary_key {
|
||||
columns = [column.id]
|
||||
}
|
||||
|
||||
foreign_key "accounts_user_id_fk" {
|
||||
columns = [column.user_id]
|
||||
ref_columns = [table.users.column.id]
|
||||
on_delete = CASCADE
|
||||
on_update = CASCADE
|
||||
}
|
||||
|
||||
index "accounts_user_id_idx" {
|
||||
columns = [column.user_id]
|
||||
}
|
||||
}
|
||||
207
database/schema/asset.pg.hcl
Normal file
207
database/schema/asset.pg.hcl
Normal file
@@ -0,0 +1,207 @@
|
||||
table "asset_categories" {
|
||||
schema = schema.public
|
||||
|
||||
column "id" {
|
||||
type = uuid
|
||||
default = sql("gen_random_uuid()")
|
||||
null = false
|
||||
}
|
||||
column "name" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "icon" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "color" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "card_type" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "featured" {
|
||||
type = boolean
|
||||
default = false
|
||||
null = false
|
||||
}
|
||||
column "description" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "updated_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "deleted_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
|
||||
primary_key {
|
||||
columns = [column.id]
|
||||
}
|
||||
|
||||
index "asset_categories_deleted_at_idx" {
|
||||
columns = [column.deleted_at]
|
||||
}
|
||||
}
|
||||
|
||||
table "assets" {
|
||||
schema = schema.public
|
||||
|
||||
column "id" {
|
||||
type = uuid
|
||||
default = sql("gen_random_uuid()")
|
||||
null = false
|
||||
}
|
||||
column "profile_id" {
|
||||
type = uuid
|
||||
null = false
|
||||
}
|
||||
column "status" {
|
||||
type = integer
|
||||
default = 0
|
||||
null = false
|
||||
}
|
||||
column "asset_category_id" {
|
||||
type = uuid
|
||||
null = false
|
||||
}
|
||||
column "title" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "description" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "link" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "analytics" {
|
||||
type = jsonb
|
||||
null = true
|
||||
}
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "updated_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "deleted_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
|
||||
primary_key {
|
||||
columns = [column.id]
|
||||
}
|
||||
|
||||
foreign_key "assets_profile_id_fk" {
|
||||
columns = [column.profile_id]
|
||||
ref_columns = [table.profiles.column.id]
|
||||
on_delete = CASCADE
|
||||
on_update = CASCADE
|
||||
}
|
||||
|
||||
foreign_key "assets_asset_category_id_fk" {
|
||||
columns = [column.asset_category_id]
|
||||
ref_columns = [table.asset_categories.column.id]
|
||||
on_delete = CASCADE
|
||||
on_update = CASCADE
|
||||
}
|
||||
|
||||
index "assets_profile_id_idx" {
|
||||
columns = [column.profile_id]
|
||||
}
|
||||
|
||||
index "assets_category_id_idx" {
|
||||
columns = [column.asset_category_id]
|
||||
}
|
||||
|
||||
index "assets_deleted_at_idx" {
|
||||
columns = [column.deleted_at]
|
||||
}
|
||||
}
|
||||
|
||||
table "asset_artifacts" {
|
||||
schema = schema.public
|
||||
|
||||
column "id" {
|
||||
type = uuid
|
||||
default = sql("gen_random_uuid()")
|
||||
null = false
|
||||
}
|
||||
column "asset_id" {
|
||||
type = uuid
|
||||
null = false
|
||||
}
|
||||
column "type" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "download_url" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "price" {
|
||||
type = integer
|
||||
default = 0
|
||||
null = false
|
||||
}
|
||||
column "title" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "description" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "updated_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "deleted_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
|
||||
primary_key {
|
||||
columns = [column.id]
|
||||
}
|
||||
|
||||
foreign_key "asset_artifacts_asset_id_fk" {
|
||||
columns = [column.asset_id]
|
||||
ref_columns = [table.assets.column.id]
|
||||
on_delete = CASCADE
|
||||
on_update = CASCADE
|
||||
}
|
||||
|
||||
index "asset_artifacts_asset_id_idx" {
|
||||
columns = [column.asset_id]
|
||||
}
|
||||
|
||||
index "asset_artifacts_deleted_at_idx" {
|
||||
columns = [column.deleted_at]
|
||||
}
|
||||
}
|
||||
31
database/schema/cache_hash.pg.hcl
Normal file
31
database/schema/cache_hash.pg.hcl
Normal file
@@ -0,0 +1,31 @@
|
||||
table "cache_hash" {
|
||||
schema = schema.public
|
||||
column "key" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "field" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "value" {
|
||||
type = jsonb
|
||||
null = false
|
||||
}
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
null = false
|
||||
default = sql("now()")
|
||||
}
|
||||
column "expires_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
primary_key {
|
||||
columns = [column.key, column.field]
|
||||
}
|
||||
|
||||
index "idx_cache_hash_expires_at" {
|
||||
columns = [column.expires_at]
|
||||
}
|
||||
}
|
||||
28
database/schema/cache_kv.pg.hcl
Normal file
28
database/schema/cache_kv.pg.hcl
Normal file
@@ -0,0 +1,28 @@
|
||||
table "cache_kv" {
|
||||
schema = schema.public
|
||||
column "key" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "value" {
|
||||
type = jsonb
|
||||
null = false
|
||||
}
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
null = false
|
||||
default = sql("now()")
|
||||
}
|
||||
column "expires_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
|
||||
primary_key {
|
||||
columns = [column.key]
|
||||
}
|
||||
|
||||
index "idx_cache_kv_expires_at" {
|
||||
columns = [column.expires_at]
|
||||
}
|
||||
}
|
||||
3
database/schema/platform.pg.hcl
Normal file
3
database/schema/platform.pg.hcl
Normal file
@@ -0,0 +1,3 @@
|
||||
schema "platform" {
|
||||
comment = "Platform schema for cache tables"
|
||||
}
|
||||
324
database/schema/profile.pg.hcl
Normal file
324
database/schema/profile.pg.hcl
Normal file
@@ -0,0 +1,324 @@
|
||||
table "profiles" {
|
||||
schema = schema.public
|
||||
|
||||
column "id" {
|
||||
type = uuid
|
||||
default = sql("gen_random_uuid()")
|
||||
null = false
|
||||
}
|
||||
column "user_id" {
|
||||
type = uuid
|
||||
null = true
|
||||
}
|
||||
column "handle" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
|
||||
// Hero fields
|
||||
column "role_id" {
|
||||
type = uuid
|
||||
null = true
|
||||
}
|
||||
column "role_name" {
|
||||
type = varchar(100)
|
||||
null = true
|
||||
}
|
||||
column "role_level" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "first_name" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "last_name" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "company" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "short_description" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "resume_link" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "cta_enabled" {
|
||||
type = boolean
|
||||
default = false
|
||||
null = false
|
||||
}
|
||||
column "avatar" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
|
||||
// About fields
|
||||
column "profile_picture" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "about" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
|
||||
// Contact fields
|
||||
column "email" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "phone" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
|
||||
// PageSetting fields
|
||||
column "visibility_level" {
|
||||
type = text
|
||||
default = "public"
|
||||
null = false
|
||||
}
|
||||
|
||||
// Complex data
|
||||
column "page_section_order" {
|
||||
type = jsonb
|
||||
null = true
|
||||
}
|
||||
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
|
||||
column "updated_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
|
||||
column "deleted_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
|
||||
primary_key {
|
||||
columns = [column.id]
|
||||
}
|
||||
|
||||
foreign_key "profiles_role_id_profile_roles_fk" {
|
||||
columns = [column.role_id]
|
||||
ref_columns = [table.profile_roles.column.id]
|
||||
on_delete = SET_NULL
|
||||
on_update = CASCADE
|
||||
}
|
||||
|
||||
unique "profiles_handle_unique" {
|
||||
columns = [column.handle]
|
||||
}
|
||||
|
||||
index "profiles_user_id_idx" {
|
||||
columns = [column.user_id]
|
||||
}
|
||||
|
||||
index "profiles_role_id_idx" {
|
||||
columns = [column.role_id]
|
||||
}
|
||||
|
||||
index "profiles_name_idx" {
|
||||
columns = [column.first_name, column.last_name]
|
||||
}
|
||||
|
||||
index "profiles_company_idx" {
|
||||
columns = [column.company]
|
||||
}
|
||||
|
||||
index "profiles_email_idx" {
|
||||
columns = [column.email]
|
||||
}
|
||||
|
||||
index "profiles_deleted_at_idx" {
|
||||
columns = [column.deleted_at]
|
||||
}
|
||||
}
|
||||
|
||||
table "profile_skills" {
|
||||
schema = schema.public
|
||||
|
||||
column "id" {
|
||||
type = uuid
|
||||
default = sql("gen_random_uuid()")
|
||||
null = false
|
||||
}
|
||||
column "profile_id" {
|
||||
type = uuid
|
||||
null = false
|
||||
}
|
||||
column "skill_name" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "level" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "updated_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "deleted_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
|
||||
primary_key {
|
||||
columns = [column.id]
|
||||
}
|
||||
|
||||
foreign_key "profile_skills_profile_id_fk" {
|
||||
columns = [column.profile_id]
|
||||
ref_columns = [table.profiles.column.id]
|
||||
on_delete = CASCADE
|
||||
on_update = CASCADE
|
||||
}
|
||||
|
||||
index "skills_profile_id_idx" {
|
||||
columns = [column.profile_id]
|
||||
}
|
||||
|
||||
index "skills_name_idx" {
|
||||
columns = [column.skill_name]
|
||||
}
|
||||
|
||||
index "profile_skills_deleted_at_idx" {
|
||||
columns = [column.deleted_at]
|
||||
}
|
||||
}
|
||||
|
||||
table "profile_social_links" {
|
||||
schema = schema.public
|
||||
|
||||
column "id" {
|
||||
type = uuid
|
||||
default = sql("gen_random_uuid()")
|
||||
null = false
|
||||
}
|
||||
column "profile_id" {
|
||||
type = uuid
|
||||
null = false
|
||||
}
|
||||
column "link_type" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "link" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "updated_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "deleted_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
|
||||
primary_key {
|
||||
columns = [column.id]
|
||||
}
|
||||
|
||||
foreign_key "profile_social_links_profile_id_fk" {
|
||||
columns = [column.profile_id]
|
||||
ref_columns = [table.profiles.column.id]
|
||||
on_delete = CASCADE
|
||||
on_update = CASCADE
|
||||
}
|
||||
|
||||
index "social_links_profile_id_idx" {
|
||||
columns = [column.profile_id]
|
||||
}
|
||||
|
||||
index "profile_social_links_deleted_at_idx" {
|
||||
columns = [column.deleted_at]
|
||||
}
|
||||
}
|
||||
|
||||
table "profile_achievements" {
|
||||
schema = schema.public
|
||||
|
||||
column "id" {
|
||||
type = uuid
|
||||
default = sql("gen_random_uuid()")
|
||||
null = false
|
||||
}
|
||||
column "profile_id" {
|
||||
type = uuid
|
||||
null = false
|
||||
}
|
||||
column "title" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "value" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "enabled" {
|
||||
type = boolean
|
||||
default = true
|
||||
null = false
|
||||
}
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "updated_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "deleted_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
|
||||
primary_key {
|
||||
columns = [column.id]
|
||||
}
|
||||
|
||||
foreign_key "profile_achievements_profile_id_fk" {
|
||||
columns = [column.profile_id]
|
||||
ref_columns = [table.profiles.column.id]
|
||||
on_delete = CASCADE
|
||||
on_update = CASCADE
|
||||
}
|
||||
|
||||
index "achievements_profile_id_idx" {
|
||||
columns = [column.profile_id]
|
||||
}
|
||||
|
||||
index "profile_achievements_deleted_at_idx" {
|
||||
columns = [column.deleted_at]
|
||||
}
|
||||
}
|
||||
|
||||
44
database/schema/profile_roles.pg.hcl
Normal file
44
database/schema/profile_roles.pg.hcl
Normal file
@@ -0,0 +1,44 @@
|
||||
table "profile_roles" {
|
||||
schema = schema.public
|
||||
|
||||
column "id" {
|
||||
type = uuid
|
||||
default = sql("gen_random_uuid()")
|
||||
null = false
|
||||
}
|
||||
column "title" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "status" {
|
||||
type = text
|
||||
default = "active"
|
||||
null = false
|
||||
}
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "updated_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "deleted_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
|
||||
primary_key {
|
||||
columns = [column.id]
|
||||
}
|
||||
|
||||
index "profile_roles_status_idx" {
|
||||
columns = [column.status]
|
||||
}
|
||||
|
||||
index "profile_roles_deleted_at_idx" {
|
||||
columns = [column.deleted_at]
|
||||
}
|
||||
}
|
||||
3
database/schema/public.pg.hcl
Normal file
3
database/schema/public.pg.hcl
Normal file
@@ -0,0 +1,3 @@
|
||||
schema "public" {
|
||||
comment = "Standard public schema"
|
||||
}
|
||||
39
database/schema/role.pg.hcl
Normal file
39
database/schema/role.pg.hcl
Normal file
@@ -0,0 +1,39 @@
|
||||
table "roles" {
|
||||
schema = schema.public
|
||||
|
||||
column "id" {
|
||||
type = uuid
|
||||
null = false
|
||||
}
|
||||
column "name" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "description" {
|
||||
type = text
|
||||
null = true
|
||||
}
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
|
||||
column "updated_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
default = sql("now()")
|
||||
}
|
||||
column "deleted_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
|
||||
primary_key {
|
||||
columns = [column.id]
|
||||
}
|
||||
|
||||
unique "roles_name_unique" {
|
||||
columns = [column.name]
|
||||
}
|
||||
}
|
||||
3
database/schema/schema.pg.hcl
Normal file
3
database/schema/schema.pg.hcl
Normal file
@@ -0,0 +1,3 @@
|
||||
schema "public" {
|
||||
|
||||
}
|
||||
35
database/schema/skills.pg.hcl
Normal file
35
database/schema/skills.pg.hcl
Normal file
@@ -0,0 +1,35 @@
|
||||
table "skills" {
|
||||
schema = schema.public
|
||||
|
||||
column "id" {
|
||||
type = uuid
|
||||
default = sql("gen_random_uuid()")
|
||||
null = false
|
||||
}
|
||||
column "name" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "updated_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
column "deleted_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
|
||||
primary_key {
|
||||
columns = [column.id]
|
||||
}
|
||||
|
||||
index "skills_catalog_name_idx" {
|
||||
columns = [column.name]
|
||||
}
|
||||
}
|
||||
45
database/schema/user.pg.hcl
Normal file
45
database/schema/user.pg.hcl
Normal file
@@ -0,0 +1,45 @@
|
||||
table "users" {
|
||||
schema = schema.public
|
||||
|
||||
column "id" {
|
||||
type = uuid
|
||||
null = false
|
||||
}
|
||||
column "first_name" {
|
||||
type = text
|
||||
}
|
||||
column "last_name" {
|
||||
type = text
|
||||
}
|
||||
column "display_name" {
|
||||
type = text
|
||||
}
|
||||
column "email" {
|
||||
type = text
|
||||
null = false
|
||||
}
|
||||
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
|
||||
column "updated_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
default = sql("now()")
|
||||
}
|
||||
column "deleted_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
|
||||
primary_key {
|
||||
columns = [column.id]
|
||||
}
|
||||
|
||||
unique "users_email_unique" {
|
||||
columns = [column.email]
|
||||
}
|
||||
}
|
||||
56
database/schema/user_role.pg.hcl
Normal file
56
database/schema/user_role.pg.hcl
Normal file
@@ -0,0 +1,56 @@
|
||||
table "user_roles" {
|
||||
schema = schema.public
|
||||
|
||||
column "id" {
|
||||
type = uuid
|
||||
null = false
|
||||
}
|
||||
|
||||
column "user_id" {
|
||||
type = uuid
|
||||
null = false
|
||||
}
|
||||
column "role_id" {
|
||||
type = uuid
|
||||
null = false
|
||||
}
|
||||
column "created_at" {
|
||||
type = timestamptz
|
||||
default = sql("now()")
|
||||
null = false
|
||||
}
|
||||
|
||||
column "updated_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
default = sql("now()")
|
||||
}
|
||||
column "deleted_at" {
|
||||
type = timestamptz
|
||||
null = true
|
||||
}
|
||||
|
||||
primary_key {
|
||||
columns = [column.id]
|
||||
}
|
||||
|
||||
foreign_key "user_roles_user_id_fk" {
|
||||
columns = [column.user_id]
|
||||
ref_columns = [table.users.column.id]
|
||||
on_delete = CASCADE
|
||||
on_update = CASCADE
|
||||
}
|
||||
foreign_key "user_roles_role_id_fk" {
|
||||
columns = [column.role_id]
|
||||
ref_columns = [table.roles.column.id]
|
||||
on_delete = CASCADE
|
||||
on_update = CASCADE
|
||||
}
|
||||
|
||||
index "user_roles_user_id_idx" {
|
||||
columns = [column.user_id]
|
||||
}
|
||||
index "user_roles_role_id_idx" {
|
||||
columns = [column.role_id]
|
||||
}
|
||||
}
|
||||
11
database/scripts/seed_mock_role.sql
Normal file
11
database/scripts/seed_mock_role.sql
Normal file
@@ -0,0 +1,11 @@
|
||||
-- Add mocked auth role for dev/mock OAuth users (roles table)
|
||||
-- Use with: psql postgres://alinmeuser:password@localhost:5430/alinmedb -f database/scripts/seed_mock_role.sql
|
||||
INSERT INTO "public"."roles" ("id", "name", "description", "created_at", "updated_at")
|
||||
VALUES (
|
||||
'a0000000-0000-0000-0000-000000000001',
|
||||
'user',
|
||||
'Default role for authenticated users (including mock OAuth dev users)',
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
37
database/scripts/seed_profile_roles_mock_data.sql
Normal file
37
database/scripts/seed_profile_roles_mock_data.sql
Normal file
@@ -0,0 +1,37 @@
|
||||
-- Seed profile_roles with mockRoleData (from internal/repository/postgres/profile/role_mock.go)
|
||||
-- Run: psql postgres://alinmeuser:password@localhost:5430/alinmedb -f database/scripts/seed_profile_roles_mock_data.sql
|
||||
-- First 7: featured, rest: active (matches 20260226000001_specialist_roles_seed.sql)
|
||||
INSERT INTO "public"."profile_roles" ("id", "title", "status") VALUES
|
||||
('0199b964-5dc0-7657-9178-2a844e23e5b5', 'Data Scientist', 'featured'),
|
||||
('0199b964-5dc0-7a1a-94c7-d68daf420e50', 'Machine Learning Engineer', 'featured'),
|
||||
('0199b964-5dc0-7759-8221-71f57f5b2b57', 'AI Engineer', 'featured'),
|
||||
('0199b964-5dc0-7b79-a268-331f39c35366', 'Data Engineer', 'featured'),
|
||||
('0199b964-5dc0-7062-b219-11733a1ab94b', 'Data Analyst', 'featured'),
|
||||
('0199b964-5dc0-7434-b105-f2ff49573fe2', 'Business Intelligence Developer', 'featured'),
|
||||
('0199b964-5dc0-77f8-be02-f76937f60ba6', 'MLOps Engineer', 'featured'),
|
||||
('0199b964-5dc0-7107-907c-6c013cbc08b9', 'AI Product Manager', 'active'),
|
||||
('0199b964-5dc0-72f9-8e0f-dfa2950a8182', 'AI Research Scientist', 'active'),
|
||||
('0199b964-5dc0-7177-829b-f3d05081201e', 'Computer Vision Engineer', 'active'),
|
||||
('0199b964-5dc0-74b7-b427-a500ddb9f435', 'NLP Engineer', 'active'),
|
||||
('0199b964-5dc0-780d-876f-a7b4d15b0ef5', 'Data Architect', 'active'),
|
||||
('0199b964-5dc0-7d3f-af44-19dc33f50b21', 'Big Data Engineer', 'active'),
|
||||
('0199b964-5dc0-7600-9a16-74f17be7ce4b', 'Cloud AI/ML Specialist', 'active'),
|
||||
('0199b964-5dc0-73c2-b9a0-78347ae945d7', 'Generative AI Specialist', 'active'),
|
||||
('0199b964-5dc0-70a8-b710-1f424a776083', 'AI Ethics Officer', 'active'),
|
||||
('0199b964-5dc0-7c87-91c0-348e6f8b43d6', 'AI Governance Manager', 'active'),
|
||||
('0199b964-5dc0-7441-b306-bc2e3d4e4152', 'Data Privacy Engineer', 'active'),
|
||||
('0199b964-5dc0-747f-97b4-c4d98a257dee', 'AI Solutions Architect', 'active'),
|
||||
('0199b964-5dc0-7fa5-8fe0-9eb7831554ed', 'Chief Data & AI Officer', 'active'),
|
||||
('0199b964-5dc0-7447-8785-f246ff9ec309', 'AI Developer Advocate', 'active'),
|
||||
('0199b964-5dc0-7b24-9b1b-c7ca8f08527f', 'AI/ML Educator & Trainer', 'active'),
|
||||
('0199b964-5dc0-756f-ab44-48169ecfbb5e', 'Technical Content Creator (AI/ML)', 'active'),
|
||||
('0199b964-5dc0-79d1-9086-c809d8989cac', 'Open Source AI Contributor', 'active'),
|
||||
('0199b964-5dc0-774e-9011-b9fe6c29f52f', 'AI Course Instructor (Udemy, Coursera, etc.)', 'active'),
|
||||
('0199b964-5dc0-7f1d-80a4-96810af9f9ac', 'AI Community Manager', 'active'),
|
||||
('0199b964-5dc0-7352-8553-edd37324ffd9', 'AI Evangelist', 'active'),
|
||||
('0199b964-5dc0-7864-a2b5-473cfd8f7aa0', 'Research Engineer (applied AI research, publishing GitHub repos)', 'active'),
|
||||
('0199b964-5dc0-762e-9a40-0cc112578498', 'Kaggle Competitor / Data Science Challenger', 'active'),
|
||||
('0199b964-5dc0-7e13-a1f4-b4ae76bb0b62', 'AI Startup Founder / Indie Hacker (building projects, sharing repos)', 'active'),
|
||||
('0199b964-5dc0-7035-bf9b-deb415d852fd', 'Freelancer', 'active'),
|
||||
('0199b964-5dc0-7702-b533-72f7c93e19d3', 'Other', 'active')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
155
database/scripts/seed_skills.sql
Normal file
155
database/scripts/seed_skills.sql
Normal file
@@ -0,0 +1,155 @@
|
||||
-- Seed skills catalog (for profile update skill selection)
|
||||
-- Table is defined in database/schema/skills.pg.hcl - run make migrate-diff then migrate-apply first
|
||||
-- Run: psql postgres://alinmeuser:password@localhost:5430/alinmedb -f database/scripts/seed_skills.sql
|
||||
INSERT INTO "public"."skills" ("id", "name") VALUES
|
||||
('0199d2ba-8be8-70fd-a496-d1600ff12333', 'Python'),
|
||||
('0199d2ba-8be8-7101-81bc-f98d3576ba6e', 'R'),
|
||||
('0199d2ba-8be8-7105-9684-a23a41178cb5', 'SQL'),
|
||||
('0199d2ba-8be8-7109-a761-b9e15f2fb2ab', 'Scala'),
|
||||
('0199d2ba-8be8-710d-b63c-e02c316e5dc3', 'Java'),
|
||||
('0199d2ba-8be8-7111-a1fc-4ab0627faac5', 'C++'),
|
||||
('0199d2ba-8be8-7115-8ee9-8f90305c389e', 'JavaScript'),
|
||||
('0199d2ba-8be8-7116-9d42-925ac82d4ad0', 'TypeScript'),
|
||||
('0199d2ba-8be8-7119-93f9-82cde5c8898c', 'Go'),
|
||||
('0199d2ba-8be8-711d-b49a-60aa06a53764', 'Rust'),
|
||||
('0199d2ba-8be8-7121-af5a-602e7c2ecfc3', 'Pandas'),
|
||||
('0199d2ba-8be8-7122-8494-15ffb02b1e20', 'NumPy'),
|
||||
('0199d2ba-8be8-7124-9f4a-cc45600720ba', 'Matplotlib'),
|
||||
('0199d2ba-8be8-7125-9dbe-71785e32e4ba', 'Seaborn'),
|
||||
('0199d2ba-8be8-7128-b430-b843a3ce2e4a', 'Plotly'),
|
||||
('0199d2ba-8be8-7129-936d-5e2b6b199f19', 'Scikit-learn'),
|
||||
('0199d2ba-8be8-712c-a718-014380b8e1d5', 'TensorFlow'),
|
||||
('0199d2ba-8be8-7134-b32a-e03eab84b57f', 'Keras'),
|
||||
('0199d2ba-8be8-7135-8641-ca42614fdd4c', 'PyTorch'),
|
||||
('0199d2ba-8be8-7138-9974-120cc312d915', 'JAX'),
|
||||
('0199d2ba-8be8-713c-b278-55750c9033d8', 'Hugging Face Transformers'),
|
||||
('0199d2ba-8be8-713d-be12-e97011d461d8', 'LangChain'),
|
||||
('0199d2ba-8be8-713e-94bd-1b472084ebd8', 'LlamaIndex'),
|
||||
('0199d2ba-8be8-7140-a6b0-31e55fbdde00', 'OpenAI API'),
|
||||
('0199d2ba-8be8-7144-b730-f836d14cac63', 'Claude API'),
|
||||
('0199d2ba-8be8-7145-85f6-cbcdd0cfd4aa', 'Gemini API'),
|
||||
('0199d2ba-8be8-7146-86dd-81f95b76d7b6', 'Vector Databases'),
|
||||
('0199d2ba-8be8-7148-99ef-9628166a6a32', 'Pinecone'),
|
||||
('0199d2ba-8be8-7149-96ba-192d5447f8ba', 'Weaviate'),
|
||||
('0199d2ba-8be8-714c-bb23-0b0417143e9b', 'FAISS'),
|
||||
('0199d2ba-8be8-714d-9612-2a1ef25cf4d6', 'ChromaDB'),
|
||||
('0199d2ba-8be8-714e-b77a-e8b1cbac2db8', 'RAG (Retrieval-Augmented Generation)'),
|
||||
('0199d2ba-8be8-714f-bbee-45b07e4c8dc8', 'LLMOps'),
|
||||
('0199d2ba-8be8-7150-924c-6d8e7fb9bafa', 'MLflow'),
|
||||
('0199d2ba-8be8-7153-a992-3a3639dc86fc', 'Weights & Biases (W&B)'),
|
||||
('0199d2ba-8be8-7154-97e5-569b82e04a0f', 'DVC (Data Version Control)'),
|
||||
('0199d2ba-8be8-7157-84a0-7e4abdf735ad', 'Docker'),
|
||||
('0199d2ba-8be8-715b-a0d5-f0856f6ebdb4', 'Kubernetes'),
|
||||
('0199d2ba-8be8-715c-9211-3d02d44f97cb', 'Apache Airflow'),
|
||||
('0199d2ba-8be8-715f-8c96-9c698ae1db57', 'Prefect'),
|
||||
('0199d2ba-8be8-7160-9aa4-ad1de612547e', 'Apache Spark'),
|
||||
('0199d2ba-8be8-7161-b7ed-709bbd7dc190', 'Hadoop'),
|
||||
('0199d2ba-8be8-7163-bebb-d6c51b1c9f64', 'Kafka'),
|
||||
('0199d2ba-8be8-7164-b8b9-ceb730020e51', 'Data Pipeline Development'),
|
||||
('0199d2ba-8be8-7167-9266-1c78ba9bc10d', 'ETL (Extract, Transform, Load)'),
|
||||
('0199d2ba-8be8-7168-88ad-3d571e694161', 'Data Warehousing'),
|
||||
('0199d2ba-8be8-7169-aa76-d6be8b6e084b', 'Data Modeling'),
|
||||
('0199d2ba-8be8-716b-a70d-7466619ef39a', 'Data Architecture'),
|
||||
('0199d2ba-8be8-716c-9c79-3733e2c2ec30', 'AWS Cloud'),
|
||||
('0199d2ba-8be8-716f-b5b0-4162a7d0bf57', 'Azure Cloud'),
|
||||
('0199d2ba-8be8-7170-b862-1b46d780941a', 'Google Cloud Platform (GCP)'),
|
||||
('0199d2ba-8be8-7171-a5d6-823cc6c46e41', 'Cloud AI/ML Services'),
|
||||
('0199d2ba-8be8-7173-ae15-5f4a3398b4cf', 'Data Visualization'),
|
||||
('0199d2ba-8be8-7174-8304-f02588d5b29b', 'Tableau'),
|
||||
('0199d2ba-8be8-7177-9411-1e9ee41180f5', 'Power BI'),
|
||||
('0199d2ba-8be8-7178-9616-3ba751d92df9', 'Looker'),
|
||||
('0199d2ba-8be8-717a-9e72-c687388bbeab', 'Excel for Data Analysis'),
|
||||
('0199d2ba-8be8-717b-838b-043204d8f0c8', 'Statistics'),
|
||||
('0199d2ba-8be8-717c-9a68-1c6149cba552', 'Probability'),
|
||||
('0199d2ba-8be8-717e-b520-6bacb9ea8187', 'Linear Algebra'),
|
||||
('0199d2ba-8be8-717f-ad87-873a66a695af', 'Calculus'),
|
||||
('0199d2ba-8be8-7182-9890-f78acd585f27', 'Machine Learning'),
|
||||
('0199d2ba-8be8-7183-896d-8838cb0f453d', 'Deep Learning'),
|
||||
('0199d2ba-8be8-7184-b9d9-483478a62324', 'Reinforcement Learning'),
|
||||
('0199d2ba-8be8-7186-add1-804f8b8cc7ba', 'Natural Language Processing (NLP)'),
|
||||
('0199d2ba-8be8-7187-9574-f8bdf5b19dee', 'Computer Vision'),
|
||||
('0199d2ba-8be8-718a-a97c-c9b08723e49d', 'Speech Recognition'),
|
||||
('0199d2ba-8be8-718b-aab8-54035f6d4f34', 'Generative AI'),
|
||||
('0199d2ba-8be8-718c-aa7f-00a847f047c8', 'Prompt Engineering'),
|
||||
('0199d2ba-8be8-718e-9ab5-d45b6ef3e8bd', 'Model Fine-Tuning'),
|
||||
('0199d2ba-8be8-718f-885e-6c1f3a0475a0', 'Data Cleaning'),
|
||||
('0199d2ba-8be8-7192-ac44-fefd5a412f38', 'Feature Engineering'),
|
||||
('0199d2ba-8be8-7196-9dc8-981b95bb6cf4', 'Model Training & Evaluation'),
|
||||
('0199d2ba-8be8-7197-9da9-5fc46e582585', 'Model Deployment'),
|
||||
('0199d2ba-8be8-719a-8aa7-acec647d1e6e', 'Model Monitoring'),
|
||||
('0199d2ba-8be8-719b-a98b-2d64c7b03e7e', 'API Development'),
|
||||
('0199d2ba-8be8-719e-870d-ada6e96b4364', 'Backend Development'),
|
||||
('0199d2ba-8be8-719f-ac90-52aeb64617dc', 'Frontend Development'),
|
||||
('0199d2ba-8be8-71a0-8f02-2a9008707dc1', 'Version Control (Git/GitHub)'),
|
||||
('0199d2ba-8be8-71a1-96ac-b0070ec183d0', 'CI/CD Pipelines'),
|
||||
('0199d2ba-8be8-71a2-bde3-3145230d461b', 'Software Engineering'),
|
||||
('0199d2ba-8be8-71a5-a946-9065c969cb08', 'Microservices Architecture'),
|
||||
('0199d2ba-8be8-71a6-af92-63b8c37e0fcd', 'Data Governance'),
|
||||
('0199d2ba-8be8-71a7-a0ad-dada184b2d54', 'Data Privacy'),
|
||||
('0199d2ba-8be8-71a9-aa08-0a4032d081a6', 'AI Ethics'),
|
||||
('0199d2ba-8be8-71aa-be8e-6e516d17c667', 'AI Safety'),
|
||||
('0199d2ba-8be8-71ad-9d78-765705dbc9eb', 'AI Governance'),
|
||||
('0199d2ba-8be8-71ae-9a18-5b0d428ce609', 'Product Management'),
|
||||
('0199d2ba-8be8-71af-ac84-0da3ed93129a', 'Project Management'),
|
||||
('0199d2ba-8be8-71b1-993e-fca092ad393b', 'Agile / Scrum'),
|
||||
('0199d2ba-8be8-71b2-b22d-7d704f78ddf6', 'Business Analysis'),
|
||||
('0199d2ba-8be8-71b3-88cf-38b5af680eb9', 'Data Storytelling'),
|
||||
('0199d2ba-8be8-71b5-81ce-e6b0b77e8602', 'Communication Skills'),
|
||||
('0199d2ba-8be8-71b6-84f0-82b804071d56', 'Leadership'),
|
||||
('0199d2ba-8be8-71b9-82a4-128ab42e19dd', 'Technical Writing'),
|
||||
('0199d2ba-8be8-71ba-9830-ec53b4e67047', 'Content Creation'),
|
||||
('0199d2ba-8be8-71bd-aed9-20178cc764d5', 'Public Speaking'),
|
||||
('0199d2ba-8be8-71be-86c3-9fc39a68bd9b', 'Teaching & Mentorship'),
|
||||
('0199d2ba-8be8-71bf-a724-4c6165e016a7', 'Open Source Contribution'),
|
||||
('0199d2ba-8be8-71c1-b3f5-a21ac46b64ba', 'Research & Experimentation'),
|
||||
('0199d2ba-8be8-71c2-a253-e96d7605252b', 'Academic Writing'),
|
||||
('0199d2ba-8be8-71c3-ac3a-1b4985532fa1', 'Kaggle Competitions'),
|
||||
('0199d2ba-8be8-71c5-be1b-9f55e24089fe', 'Entrepreneurship'),
|
||||
('0199d2ba-8be8-71c6-beb0-e6c6c89d12fd', 'Startup Development'),
|
||||
('0199d2ba-8be8-71c9-841a-65db45d08cff', 'Freelancing'),
|
||||
('0199d2ba-8be8-71ca-8dbc-2a24e339e5ff', 'Community Building'),
|
||||
('0199d2ba-8be8-71cb-a573-7919f5c2152e', 'AI Advocacy'),
|
||||
('0199d2ba-8be8-71cc-b47b-b5c4cb301cc7', 'Data Security'),
|
||||
('0199d2ba-8be8-71cd-a5a2-9f147e9f2271', 'API Integration'),
|
||||
('0199d2ba-8be8-71d0-a8e4-fdcd1e9f6c02', 'Time Series Analysis'),
|
||||
('0199d2ba-8be8-71d1-bea0-1f30a9467498', 'Experiment Tracking'),
|
||||
('0199d2ba-8be8-71d2-b7aa-d1478882499c', 'Hyperparameter Tuning'),
|
||||
('0199d2ba-8be8-71d4-8b05-b2e8266d3534', 'Model Optimization'),
|
||||
('0199d2ba-8be8-71d5-9a9a-57c11bf499d4', 'Feature Selection'),
|
||||
('0199d2ba-8be8-71d8-a72b-d633a1a6bada', 'Big Data Technologies'),
|
||||
('0199d2ba-8be8-71d9-9b3c-f802602f2dd1', 'Distributed Computing'),
|
||||
('0199d2ba-8be8-71da-be24-3aae458443d4', 'Cloud Computing'),
|
||||
('0199d2ba-8be8-71dc-969b-db1e83e26873', 'NoSQL Databases'),
|
||||
('0199d2ba-8be8-71dd-a6d4-282f52d79022', 'Graph Databases'),
|
||||
('0199d2ba-8be8-71e0-a0e7-910b07fa8aaf', 'Data Lakes'),
|
||||
('0199d2ba-8be8-71e1-8fb2-5db55b828b52', 'Automation'),
|
||||
('0199d2ba-8be8-71e2-8628-5205aedb39c1', 'MLOps'),
|
||||
('0199d2ba-8be8-71e4-a2b3-742d5fcec94a', 'DataOps'),
|
||||
('0199d2ba-8be8-71e5-939a-055758f99616', 'AIOps'),
|
||||
('0199d2ba-8be8-71e8-85eb-b7f0a8ff6bd3', 'Edge AI'),
|
||||
('0199d2ba-8be8-71e9-9fee-2442574ac577', 'TinyML'),
|
||||
('0199d2ba-8be8-71ec-a4cb-5e836d311be1', 'AutoML'),
|
||||
('0199d2ba-8be8-71ed-adc3-53f58cc3b684', 'Explainable AI (XAI)'),
|
||||
('0199d2ba-8be8-71ee-b476-fea7f9820fd4', 'Responsible AI'),
|
||||
('0199d2ba-8be8-71f0-927b-cca53d78c140', 'AI Policy'),
|
||||
('0199d2ba-8be8-71f1-b730-38d4effd4e5a', 'AI Regulation Compliance'),
|
||||
('0199d2ba-8be8-71f4-8030-3174413315e6', 'AI Product Design'),
|
||||
('0199d2ba-8be8-71f5-a473-3febc586b8d7', 'Human-Centered AI'),
|
||||
('0199d2ba-8be8-71f6-ab07-34e9403f0d23', 'User Experience (UX)'),
|
||||
('0199d2ba-8be8-71f7-ae8c-652f27106c74', 'Prototyping'),
|
||||
('0199d2ba-8be8-71f8-b10a-14d1db35e819', 'Streamlit'),
|
||||
('0199d2ba-8be8-71f9-aefe-fdba6ed47f11', 'Gradio'),
|
||||
('0199d2ba-8be8-71fb-94a6-f01c963c55ef', 'FastAPI'),
|
||||
('0199d2ba-8be8-71fc-b404-f201d4df49cd', 'Flask'),
|
||||
('0199d2ba-8be8-71ff-bf1e-19a7a8151908', 'Dash'),
|
||||
('0199d2ba-8be8-7200-b99d-232845111a56', 'Plotly Dashboards'),
|
||||
('0199d2ba-8be8-7201-ba41-d85fd5a8bd57', 'Business Intelligence'),
|
||||
('0199d2ba-8be8-7203-a3c1-69587b57a4c3', 'Kubernetes Helm'),
|
||||
('0199d2ba-8be8-7204-be49-bf2ecbbf13ae', 'Terraform'),
|
||||
('0199d2ba-8be8-7207-ac8f-3782663f1929', 'Serverless Functions'),
|
||||
('0199d2ba-8be8-7208-935c-74fcba2ce66c', 'REST APIs'),
|
||||
('0199d2ba-8be8-7209-b5a4-040fcb157631', 'GraphQL'),
|
||||
('0199d2ba-8be8-720b-b2b8-a62282a620c8', 'Testing & QA'),
|
||||
('0199d2ba-8be8-720c-98b1-be0802ddcfce', 'System Design'),
|
||||
('0199d2ba-8be8-720d-be28-17dfb7d1f22d', 'Scalability Engineering')
|
||||
ON CONFLICT (id) DO NOTHING;
|
||||
44
docker-compose.yml
Normal file
44
docker-compose.yml
Normal file
@@ -0,0 +1,44 @@
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
# Mock OAuth2 server for local development (run with: docker compose up mock-oauth)
|
||||
mock-oauth:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile.mock-oauth
|
||||
container_name: alinme-mock-oauth
|
||||
ports:
|
||||
- "9999:9999"
|
||||
environment:
|
||||
PORT: "9999"
|
||||
networks:
|
||||
- base-network
|
||||
|
||||
# PostgreSQL Database
|
||||
pg:
|
||||
image: postgres:18-alpine
|
||||
container_name: alinmedb-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: alinmeuser
|
||||
POSTGRES_PASSWORD: password
|
||||
POSTGRES_DB: alinmedb
|
||||
ports:
|
||||
- "5430:5432"
|
||||
volumes:
|
||||
- pg_data:/var/lib/postgresql/data
|
||||
- ./.docker/postgres/docker-entrypoint-initdb:/docker-entrypoint-initdb.d
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "alinmeuser", "-d", "alinmedb"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
networks:
|
||||
- base-network
|
||||
|
||||
volumes:
|
||||
pg_data:
|
||||
|
||||
networks:
|
||||
base-network:
|
||||
driver: bridge
|
||||
3150
docs/docs.go
Normal file
3150
docs/docs.go
Normal file
File diff suppressed because it is too large
Load Diff
3130
docs/swagger.json
Normal file
3130
docs/swagger.json
Normal file
File diff suppressed because it is too large
Load Diff
2045
docs/swagger.yaml
Normal file
2045
docs/swagger.yaml
Normal file
File diff suppressed because it is too large
Load Diff
134
go.mod
Normal file
134
go.mod
Normal file
@@ -0,0 +1,134 @@
|
||||
module base
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.12
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
github.com/redis/go-redis/v9 v9.17.2
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/samber/lo v1.52.0
|
||||
github.com/speps/go-hashids v2.0.0+incompatible
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/swaggo/files v1.0.1
|
||||
github.com/swaggo/gin-swagger v1.6.1
|
||||
github.com/swaggo/swag v1.16.6
|
||||
go.uber.org/fx v1.24.0
|
||||
golang.org/x/crypto v0.43.0
|
||||
golang.org/x/oauth2 v0.32.0
|
||||
golang.org/x/sync v0.17.0
|
||||
google.golang.org/grpc v1.77.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gorm.io/datatypes v1.2.7
|
||||
gorm.io/driver/postgres v1.6.0
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
cloud.google.com/go/compute/metadata v0.9.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect
|
||||
github.com/Azure/go-amqp v1.4.0 // indirect
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect
|
||||
github.com/KyleBanks/depth v1.2.1 // indirect
|
||||
github.com/PuerkitoBio/purell v1.1.1 // indirect
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.5 // indirect
|
||||
github.com/go-openapi/jsonreference v0.19.6 // indirect
|
||||
github.com/go-openapi/spec v0.20.4 // indirect
|
||||
github.com/go-openapi/swag v0.19.15 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.6.0 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kylelemons/godebug v1.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 // indirect
|
||||
github.com/lestrrat-go/dsig v1.0.0 // indirect
|
||||
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
|
||||
github.com/lestrrat-go/httpcc v1.0.1 // indirect
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
|
||||
github.com/lestrrat-go/option v1.0.1 // indirect
|
||||
github.com/lestrrat-go/option/v2 v2.0.0 // indirect
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 // indirect
|
||||
github.com/mailru/easyjson v0.7.6 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.22 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/oklog/ulid v1.3.1 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_model v0.6.2 // indirect
|
||||
github.com/prometheus/common v0.66.1 // indirect
|
||||
github.com/prometheus/procfs v0.17.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.11.0 // indirect
|
||||
github.com/segmentio/asm v1.2.1 // indirect
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
|
||||
github.com/spf13/afero v1.15.0 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
go.uber.org/dig v1.19.0 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
go.uber.org/zap v1.26.0 // indirect
|
||||
go.yaml.in/yaml/v2 v2.4.2 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
golang.org/x/text v0.30.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gorm.io/driver/mysql v1.5.6 // indirect
|
||||
)
|
||||
390
go.sum
Normal file
390
go.sum
Normal file
@@ -0,0 +1,390 @@
|
||||
cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
|
||||
cloud.google.com/go/compute/metadata v0.9.0/go.mod h1:E0bWwX5wTnLPedCKqk3pJmVgCBSM6qQI1yTBdEb3C10=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0 h1:kE5kpeiSqu4jcCQ/sWuyggMXJ/pT6oQ99+8hwPmyeJ0=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/messaging/azservicebus v1.10.0/go.mod h1:IAN3Z0DMtehoxoQQnfqg1891z1P7GNoDryKtFcAyMBI=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1 h1:/Zt+cDPnpC3OVDm/JKLOs7M2DKmLRIIp3XIx9pHHiig=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage v1.8.1/go.mod h1:Ng3urmn6dYe8gnbCMoHHVl5APYz2txho3koEkV2o2HA=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3 h1:ZJJNFaQ86GVKQ9ehwqyAFE6pIfyicpuJ8IkVaPBc6/4=
|
||||
github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.6.3/go.mod h1:URuDvhmATVKqHBH9/0nOiNKk0+YcwfQ3WkK5PqHKxc8=
|
||||
github.com/Azure/go-amqp v1.4.0 h1:Xj3caqi4comOF/L1Uc5iuBxR/pB6KumejC01YQOqOR4=
|
||||
github.com/Azure/go-amqp v1.4.0/go.mod h1:vZAogwdrkbyK3Mla8m/CxSc/aKdnTZ4IbPxl51Y5WZE=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM=
|
||||
github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs=
|
||||
github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk=
|
||||
github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc=
|
||||
github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE=
|
||||
github.com/PuerkitoBio/purell v1.1.1 h1:WEQqlqaGbrPkxLJWfBwQmfEAE1Z7ONdDLqrN38tNFfI=
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 h1:d+Bc7a5rLufV/sSk/8dngufqelfh6jnri85riMAaF/M=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1 h1:t5xMivyf9tpmU3iozPqyrCZXHvoV1XQDfihas4sV0fY=
|
||||
github.com/ThreeDotsLabs/watermill v1.5.1/go.mod h1:Uop10dA3VeJWsSvis9qO3vbVY892LARrKAdki6WtXS4=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0 h1:Ny8MWAHyOepLGlLKYmXG4IEkioBysk6GpaRTLC8zwWs=
|
||||
github.com/bsm/ginkgo/v2 v2.12.0/go.mod h1:SwYbGRRDovPVboqFv0tPTcG1sN61LM1Z4ARdbAV9g4c=
|
||||
github.com/bsm/gomega v1.27.10 h1:yeMWxP2pV2fG3FgAODIY8EiRE3dy0aeFYt4l7wh6yKA=
|
||||
github.com/bsm/gomega v1.27.10/go.mod h1:JyEr/xRbxbtgWNi8tIEVPUYZ5Dzef52k01W3YH0H+O0=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
|
||||
github.com/coder/websocket v1.8.13/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc=
|
||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw=
|
||||
github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||
github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
|
||||
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY=
|
||||
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
|
||||
github.com/go-openapi/jsonreference v0.19.6 h1:UBIxjkht+AWIgYzCDSv2GN+E/togfwXUJFRTWhl2Jjs=
|
||||
github.com/go-openapi/jsonreference v0.19.6/go.mod h1:diGHMEHg2IqXZGKxqyvWdfWU/aim5Dprw5bqpKkTvns=
|
||||
github.com/go-openapi/spec v0.20.4 h1:O8hJrt0UMnhHcluhIdUgCLRWyM2x7QkBXRvOs7m+O1M=
|
||||
github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7FOEWeq8I=
|
||||
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
|
||||
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
|
||||
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA=
|
||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||
github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A=
|
||||
github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.6.0 h1:SWJzexBzPL5jb0GEsrPMLIsi/3jOo7RHlzTjcAeDrPY=
|
||||
github.com/jackc/pgx/v5 v5.6.0/go.mod h1:DNZ/vlrUnhWCoFGxHAG8U2ljioxukquj7utPDgtQdTw=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
|
||||
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
|
||||
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
|
||||
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
|
||||
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
|
||||
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
|
||||
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
|
||||
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
|
||||
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.1 h1:3n7Es68YYGZb2Jf+k//llA4FTZMl3yCwIjFIk4ubevI=
|
||||
github.com/lestrrat-go/httprc/v3 v3.0.1/go.mod h1:2uAvmbXE4Xq8kAUjVrZOq1tZVYYYs5iP62Cmtru00xk=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.12 h1:p25r68Y4KrbBdYjIsQweYxq794CtGCzcrc5dGzJIRjg=
|
||||
github.com/lestrrat-go/jwx/v3 v3.0.12/go.mod h1:HiUSaNmMLXgZ08OmGBaPVvoZQgJVOQphSrGr5zMamS8=
|
||||
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
|
||||
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
|
||||
github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
|
||||
github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7 h1:trX0KTHy4Pbwo/6ia8fscyHoGA+mf1jWbPJVuvyJQQ8=
|
||||
github.com/lithammer/shortuuid/v3 v3.0.7/go.mod h1:vMk8ke37EmiewwolSO1NLW8vP4ZaKlRuDIi8tWWmAts=
|
||||
github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA=
|
||||
github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
|
||||
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microsoft/go-mssqldb v1.7.2 h1:CHkFJiObW7ItKTJfHo1QX7QBBD1iV+mn1eOyRP3b/PA=
|
||||
github.com/microsoft/go-mssqldb v1.7.2/go.mod h1:kOvZKUdrhhFQmxLZqbwUV0rHkNkZpthMITIb2Ko1IoA=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4=
|
||||
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
|
||||
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
|
||||
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
|
||||
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
|
||||
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
|
||||
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
|
||||
github.com/prometheus/procfs v0.17.0 h1:FuLQ+05u4ZI+SS/w9+BWEM2TXiHKsUQ9TADiRH7DuK0=
|
||||
github.com/prometheus/procfs v0.17.0/go.mod h1:oPQLaDAMRbA+u8H5Pbfq+dl3VDAvHxMUOVhe0wYB2zw=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzukfVhBw=
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI=
|
||||
github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370=
|
||||
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
|
||||
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
|
||||
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
|
||||
github.com/samber/lo v1.52.0 h1:Rvi+3BFHES3A8meP33VPAxiBZX/Aws5RxrschYGjomw=
|
||||
github.com/samber/lo v1.52.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0=
|
||||
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
|
||||
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
|
||||
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
|
||||
github.com/speps/go-hashids v2.0.0+incompatible h1:kSfxGfESueJKTx0mpER9Y/1XHl+FVQjtCqRyYcviFbw=
|
||||
github.com/speps/go-hashids v2.0.0+incompatible/go.mod h1:P7hqPzMdnZOfyIk+xrlG1QaSMw+gCBdHKsBDnhpaZvc=
|
||||
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
|
||||
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
|
||||
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
|
||||
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
|
||||
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
|
||||
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
|
||||
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
|
||||
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
|
||||
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE=
|
||||
github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg=
|
||||
github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY=
|
||||
github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw=
|
||||
github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI=
|
||||
github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
go.uber.org/dig v1.19.0 h1:BACLhebsYdpQ7IROQ1AGPjrXcP5dF80U3gKoFzbaq/4=
|
||||
go.uber.org/dig v1.19.0/go.mod h1:Us0rSJiThwCv2GteUN0Q7OKvU7n5J4dxZ9JKUXozFdE=
|
||||
go.uber.org/fx v1.24.0 h1:wE8mruvpg2kiiL1Vqd0CC+tr0/24XIB10Iwp2lLWzkg=
|
||||
go.uber.org/fx v1.24.0/go.mod h1:AmDeGyS+ZARGKM4tlH4FY2Jr63VjbEDJHtqXTGP5hbo=
|
||||
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
|
||||
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
|
||||
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82 h1:6/3JGEh1C88g7m+qzzTbl3A0FtsLguXieqofVLU/JAo=
|
||||
golang.org/x/net v0.46.1-0.20251013234738-63d1a5100f82/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
|
||||
golang.org/x/oauth2 v0.32.0 h1:jsCblLleRMDrxMN29H3z/k1KliIvpLgCkE6R8FXXNgY=
|
||||
golang.org/x/oauth2 v0.32.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
|
||||
golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8 h1:M1rk8KBnUsBDg1oPGHNCxG4vc1f49epmTO7xscSajMk=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251022142026-3a174f9686a8/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/datatypes v1.2.7 h1:ww9GAhF1aGXZY3EB3cJPJ7//JiuQo7DlQA7NNlVaTdk=
|
||||
gorm.io/datatypes v1.2.7/go.mod h1:M2iO+6S3hhi4nAyYe444Pcb0dcIiOMJ7QHaUXxyiNZY=
|
||||
gorm.io/driver/mysql v1.5.6 h1:Ld4mkIickM+EliaQZQx3uOJDJHtrd70MxAUqWqlx3Y8=
|
||||
gorm.io/driver/mysql v1.5.6/go.mod h1:sEtPWMiqiN1N1cMXoXmBbd8C6/l+TESwriotuRRpkDM=
|
||||
gorm.io/driver/postgres v1.6.0 h1:2dxzU8xJ+ivvqTRph34QX+WrRaJlmfyPqXmoGVjMBa4=
|
||||
gorm.io/driver/postgres v1.6.0/go.mod h1:vUw0mrGgrTK+uPHEhAdV4sfFELrByKVGnaVRkXDhtWo=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/driver/sqlserver v1.6.0 h1:VZOBQVsVhkHU/NzNhRJKoANt5pZGQAS1Bwc6m6dgfnc=
|
||||
gorm.io/driver/sqlserver v1.6.0/go.mod h1:WQzt4IJo/WHKnckU9jXBLMJIVNMVeTu25dnOzehntWw=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
349
internal/application/asset/service.go
Normal file
349
internal/application/asset/service.go
Normal file
@@ -0,0 +1,349 @@
|
||||
package asset
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.uber.org/fx"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
domainAsset "base/internal/domain/asset"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrAssetNotFound = errors.New("asset not found")
|
||||
ErrCategoryNotFound = errors.New("asset category not found")
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Create(ctx context.Context, req dto.CreateAssetRequest) (*dto.AssetResponse, error)
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*dto.AssetResponse, error)
|
||||
Update(ctx context.Context, req dto.UpdateAssetRequest) (*dto.AssetResponse, error)
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
FindByProfileID(ctx context.Context, profileID uuid.UUID) (*dto.ListAssetsResponse, error)
|
||||
ListCategories(ctx context.Context) (*dto.ListCategoriesResponse, error)
|
||||
ListByCategoryID(ctx context.Context, categoryID uuid.UUID, limit, page int) (*dto.ListAssetsByCategoryIDResponse, error)
|
||||
GetCategoriesWithPreview(ctx context.Context, req dto.CategoriesPreviewRequest) (*dto.CategoriesPreviewResponse, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
logger zerolog.Logger
|
||||
assetRepo domainAsset.AssetRepository
|
||||
categoryRepo domainAsset.CategoryRepository
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
Logger zerolog.Logger
|
||||
AssetRepo domainAsset.AssetRepository
|
||||
CategoryRepo domainAsset.CategoryRepository
|
||||
|
||||
fx.In
|
||||
}
|
||||
|
||||
func New(param Param) Service {
|
||||
return &service{
|
||||
logger: param.Logger,
|
||||
assetRepo: param.AssetRepo,
|
||||
categoryRepo: param.CategoryRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) Create(ctx context.Context, req dto.CreateAssetRequest) (*dto.AssetResponse, error) {
|
||||
profileID, err := uuid.Parse(req.ProfileID)
|
||||
if err != nil {
|
||||
return nil, ErrAssetNotFound
|
||||
}
|
||||
categoryID, err := uuid.Parse(req.AssetCategoryID)
|
||||
if err != nil {
|
||||
return nil, ErrCategoryNotFound
|
||||
}
|
||||
|
||||
// Verify category exists
|
||||
category, err := s.categoryRepo.FindByID(ctx, categoryID)
|
||||
if err != nil || category == nil {
|
||||
return nil, ErrCategoryNotFound
|
||||
}
|
||||
|
||||
asset := &domainAsset.Asset{
|
||||
ID: uuid.New(),
|
||||
ProfileID: profileID,
|
||||
AssetCategoryID: categoryID,
|
||||
AssetCategory: *category,
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
Link: req.Link,
|
||||
Status: domainAsset.StatusPublished,
|
||||
}
|
||||
|
||||
if err := s.assetRepo.Create(ctx, asset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toAssetResponse(asset), nil
|
||||
}
|
||||
|
||||
func (s *service) GetByID(ctx context.Context, id uuid.UUID) (*dto.AssetResponse, error) {
|
||||
asset, err := s.assetRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, ErrAssetNotFound
|
||||
}
|
||||
return s.toAssetResponse(asset), nil
|
||||
}
|
||||
|
||||
func (s *service) Update(ctx context.Context, req dto.UpdateAssetRequest) (*dto.AssetResponse, error) {
|
||||
id, err := uuid.Parse(req.ID)
|
||||
if err != nil {
|
||||
return nil, ErrAssetNotFound
|
||||
}
|
||||
|
||||
asset, err := s.assetRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, ErrAssetNotFound
|
||||
}
|
||||
|
||||
asset.Title = req.Title
|
||||
asset.Description = req.Description
|
||||
asset.Link = req.Link
|
||||
|
||||
if req.AssetCategoryID != "" {
|
||||
categoryID, err := uuid.Parse(req.AssetCategoryID)
|
||||
if err == nil {
|
||||
category, err := s.categoryRepo.FindByID(ctx, categoryID)
|
||||
if err == nil && category != nil {
|
||||
asset.AssetCategoryID = categoryID
|
||||
asset.AssetCategory = *category
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if req.Status != nil && *req.Status >= 0 && *req.Status <= 3 {
|
||||
asset.Status = domainAsset.Status(*req.Status)
|
||||
}
|
||||
|
||||
if err := s.assetRepo.Update(ctx, asset); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toAssetResponse(asset), nil
|
||||
}
|
||||
|
||||
func (s *service) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
asset, err := s.assetRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return ErrAssetNotFound
|
||||
}
|
||||
return s.assetRepo.Delete(ctx, asset)
|
||||
}
|
||||
|
||||
func (s *service) FindByProfileID(ctx context.Context, profileID uuid.UUID) (*dto.ListAssetsResponse, error) {
|
||||
assets, err := s.assetRepo.FindByProfileID(ctx, profileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &dto.ListAssetsResponse{
|
||||
Assets: make([]dto.AssetResponse, len(assets)),
|
||||
}
|
||||
for i, a := range assets {
|
||||
resp.Assets[i] = *s.toAssetResponse(a)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *service) ListCategories(ctx context.Context) (*dto.ListCategoriesResponse, error) {
|
||||
categories, err := s.categoryRepo.FindAll(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := &dto.ListCategoriesResponse{
|
||||
Categories: make([]dto.CategoryDTO, len(categories)),
|
||||
}
|
||||
for i, c := range categories {
|
||||
resp.Categories[i] = dto.CategoryDTO{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
Icon: c.Icon,
|
||||
Color: c.Color,
|
||||
CardType: c.CardType,
|
||||
Featured: c.Featured,
|
||||
Description: c.Description,
|
||||
}
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *service) ListByCategoryID(ctx context.Context, categoryID uuid.UUID, limit, page int) (*dto.ListAssetsByCategoryIDResponse, error) {
|
||||
if limit < 1 {
|
||||
limit = 10
|
||||
}
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
|
||||
category, err := s.categoryRepo.FindByID(ctx, categoryID)
|
||||
if err != nil || category == nil {
|
||||
return nil, ErrCategoryNotFound
|
||||
}
|
||||
|
||||
total, err := s.assetRepo.CountByCategory(ctx, categoryID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
offset := (page - 1) * limit
|
||||
assets, err := s.assetRepo.FindLatestByCategoryPaginated(ctx, categoryID, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
totalPages := (total + limit - 1) / limit
|
||||
if totalPages < 1 {
|
||||
totalPages = 1
|
||||
}
|
||||
|
||||
resp := &dto.ListAssetsByCategoryIDResponse{
|
||||
Category: dto.CategoryDTO{
|
||||
ID: category.ID,
|
||||
Name: category.Name,
|
||||
Icon: category.Icon,
|
||||
Color: category.Color,
|
||||
CardType: category.CardType,
|
||||
Featured: category.Featured,
|
||||
Description: category.Description,
|
||||
},
|
||||
Assets: make([]dto.AssetResponse, len(assets)),
|
||||
Total: total,
|
||||
Page: page,
|
||||
PageSize: limit,
|
||||
TotalPages: totalPages,
|
||||
}
|
||||
for i, a := range assets {
|
||||
resp.Assets[i] = *s.toAssetResponse(a)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *service) GetCategoriesWithPreview(ctx context.Context, req dto.CategoriesPreviewRequest) (*dto.CategoriesPreviewResponse, error) {
|
||||
perCategory := req.AssetsPerCategory
|
||||
if perCategory < 1 {
|
||||
perCategory = 8
|
||||
}
|
||||
if perCategory > 20 {
|
||||
perCategory = 20
|
||||
}
|
||||
|
||||
var categoryIDs []uuid.UUID
|
||||
for _, s := range req.CategoryIDs {
|
||||
if id, err := uuid.Parse(s); err == nil {
|
||||
categoryIDs = append(categoryIDs, id)
|
||||
}
|
||||
}
|
||||
|
||||
var categories []*domainAsset.Category
|
||||
var err error
|
||||
if len(categoryIDs) > 0 {
|
||||
categories, err = s.categoryRepo.FindByIDs(ctx, categoryIDs)
|
||||
} else {
|
||||
categories, err = s.categoryRepo.FindAll(ctx)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if req.FeaturedOnly {
|
||||
filtered := make([]*domainAsset.Category, 0, len(categories))
|
||||
for _, c := range categories {
|
||||
if c.Featured {
|
||||
filtered = append(filtered, c)
|
||||
}
|
||||
}
|
||||
categories = filtered
|
||||
}
|
||||
|
||||
results := make([]dto.CategoryWithPreviewAssetsDTO, len(categories))
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
for index, category := range categories {
|
||||
i, cat := index, category
|
||||
g.Go(func() error {
|
||||
assets, assetErr := s.assetRepo.FindLatestByCategory(gCtx, cat.ID, perCategory)
|
||||
if assetErr != nil {
|
||||
return assetErr
|
||||
}
|
||||
|
||||
total, _ := s.assetRepo.CountByCategory(gCtx, cat.ID)
|
||||
|
||||
assetResps := make([]dto.AssetResponse, len(assets))
|
||||
for j, a := range assets {
|
||||
assetResps[j] = *s.toAssetResponse(a)
|
||||
}
|
||||
|
||||
results[i] = dto.CategoryWithPreviewAssetsDTO{
|
||||
Category: dto.CategoryDTO{
|
||||
ID: cat.ID,
|
||||
Name: cat.Name,
|
||||
Icon: cat.Icon,
|
||||
Color: cat.Color,
|
||||
CardType: cat.CardType,
|
||||
Featured: cat.Featured,
|
||||
Description: cat.Description,
|
||||
},
|
||||
Assets: assetResps,
|
||||
TotalAssets: total,
|
||||
HasMore: total > perCategory,
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &dto.CategoriesPreviewResponse{Categories: results}, nil
|
||||
}
|
||||
|
||||
func (s *service) toAssetResponse(a *domainAsset.Asset) *dto.AssetResponse {
|
||||
coverImage := ""
|
||||
for _, art := range a.AssetArtifacts {
|
||||
if strings.Contains(strings.ToLower(art.Type), "image") {
|
||||
coverImage = art.DownloadURL
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
resp := &dto.AssetResponse{
|
||||
ID: a.ID,
|
||||
ProfileID: a.ProfileID,
|
||||
AssetCategoryID: a.AssetCategoryID,
|
||||
Title: a.Title,
|
||||
Description: a.Description,
|
||||
Link: a.Link,
|
||||
CoverImage: coverImage,
|
||||
Status: int(a.Status),
|
||||
CreatedAt: formatTime(a.CreatedAt),
|
||||
UpdatedAt: formatTime(a.UpdatedAt),
|
||||
}
|
||||
|
||||
resp.Category = dto.CategoryDTO{
|
||||
ID: a.AssetCategory.ID,
|
||||
Name: a.AssetCategory.Name,
|
||||
Icon: a.AssetCategory.Icon,
|
||||
Color: a.AssetCategory.Color,
|
||||
CardType: a.AssetCategory.CardType,
|
||||
Featured: a.AssetCategory.Featured,
|
||||
Description: a.AssetCategory.Description,
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func formatTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
49
internal/application/auth/account_info.go
Normal file
49
internal/application/auth/account_info.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"base/internal/domain/auth"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
func (s *service) GetUserInfo(ctx context.Context, userID uuid.UUID) (*dto.UserInfoResponse, error) {
|
||||
user, err := s.userRepo.FindByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
var profileID *uuid.UUID
|
||||
prof, err := s.profileRepo.FindByUserID(ctx, userID)
|
||||
if err == nil && prof != nil {
|
||||
profileID = &prof.ID
|
||||
}
|
||||
|
||||
return &dto.UserInfoResponse{
|
||||
ID: user.ID,
|
||||
Email: user.Email,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
PhoneNumber: user.PhoneNumber,
|
||||
EmailVerified: user.EmailVerified,
|
||||
Status: userStatusToString(user.Status),
|
||||
ProfileID: profileID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func userStatusToString(s auth.UserStatus) string {
|
||||
switch s {
|
||||
case auth.UserStatusActive:
|
||||
return "active"
|
||||
case auth.UserStatusInactive:
|
||||
return "inactive"
|
||||
case auth.UserStatusPending:
|
||||
return "pending"
|
||||
case auth.UserStatusDeleted:
|
||||
return "deleted"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
86
internal/application/auth/oauth.go
Normal file
86
internal/application/auth/oauth.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"base/pkg/jwt"
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"base/internal/domain/auth"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
func (s *service) GetOAuthRedirectURL(ctx context.Context, request dto.OAuthRedirectURLRequest) (string, error) {
|
||||
provider := s.oauthService.Client(request.Provider)
|
||||
|
||||
state := uuid.New().String()
|
||||
redirectURL := provider.GetConsentAuthUrl(ctx, state)
|
||||
|
||||
return redirectURL, nil
|
||||
}
|
||||
|
||||
func (s *service) OAuthCallback(ctx context.Context, request dto.OAuthCallbackRequest) (*dto.OAuthCallbackResponse, error) {
|
||||
oauthProvider := s.oauthService.Client(request.Provider)
|
||||
|
||||
token, err := oauthProvider.ExchangeCodeWithToken(ctx, request.Code)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to exchange code for token: %w", err)
|
||||
}
|
||||
|
||||
userInfo, err := oauthProvider.GetUserInfo(ctx, token.AccessToken, token.RefreshToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user info: %w", err)
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
accessExpiry := now.Add(time.Duration(token.ExpiresIn) * time.Second)
|
||||
refreshExpiry := now.Add(7 * 24 * time.Hour)
|
||||
|
||||
user := &auth.User{
|
||||
ID: uuid.New(), // Will be set by repository if user exists
|
||||
Email: userInfo.Email(),
|
||||
FirstName: userInfo.FirstName(),
|
||||
LastName: userInfo.LastName(),
|
||||
Status: auth.UserStatusActive,
|
||||
EmailVerified: true, // OAuth providers verify email
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
account := &auth.Account{
|
||||
ID: uuid.New(),
|
||||
Provider: request.Provider,
|
||||
AccessToken: &token.AccessToken,
|
||||
RefreshToken: &token.RefreshToken,
|
||||
AccessTokenExpiry: &accessExpiry,
|
||||
RefreshTokenExpiry: &refreshExpiry,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
isNewUser, err := s.userRepo.UpsertWithAccount(ctx, userInfo.Email(), user, account)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Msg("failed to upsert user and account")
|
||||
return nil, fmt.Errorf("failed to upsert user and account: %w", err)
|
||||
}
|
||||
|
||||
tokens, genErr := s.jwtService.GenerateAccessRefreshTokenPair(ctx, &jwt.TokenData{Sub: user.ID.String()})
|
||||
if genErr != nil {
|
||||
return nil, fmt.Errorf("failed to generate tokens: %w", genErr)
|
||||
}
|
||||
|
||||
s.logger.Info().
|
||||
Str("user_id", user.ID.String()).
|
||||
Str("email", user.Email).
|
||||
Bool("is_new_user", isNewUser).
|
||||
Str("provider", request.Provider.String()).
|
||||
Msg("OAuth callback completed successfully")
|
||||
|
||||
return &dto.OAuthCallbackResponse{
|
||||
AccessToken: tokens.AccessToken,
|
||||
RefreshToken: tokens.RefreshToken,
|
||||
IsNewUser: isNewUser,
|
||||
}, nil
|
||||
}
|
||||
210
internal/application/auth/register.go
Normal file
210
internal/application/auth/register.go
Normal file
@@ -0,0 +1,210 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"base/internal/domain/auth"
|
||||
"base/internal/dto"
|
||||
"base/internal/pkg/oauth"
|
||||
"base/pkg/jwt"
|
||||
)
|
||||
|
||||
func (s *service) RegisterWithCredentials(ctx context.Context, request dto.RegisterRequest) (*dto.TokenResponse, error) {
|
||||
// Check if user already exists
|
||||
existingUser, err := s.userRepo.FindByEmail(ctx, request.Email)
|
||||
if err == nil && existingUser != nil {
|
||||
return nil, ErrUserAlreadyExists
|
||||
}
|
||||
|
||||
// Hash password
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
s.logger.Error().Err(err).Msg("failed to hash password")
|
||||
return nil, fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
hashedPasswordStr := string(hashedPassword)
|
||||
|
||||
id, genErr := uuid.NewV7()
|
||||
if genErr != nil {
|
||||
return nil, genErr
|
||||
}
|
||||
|
||||
// Create user and account within a transaction
|
||||
// If any operation fails, all changes are rolled back
|
||||
user := &auth.User{
|
||||
ID: id,
|
||||
Email: request.Email,
|
||||
FirstName: request.FirstName,
|
||||
LastName: request.LastName,
|
||||
PhoneNumber: request.PhoneNumber,
|
||||
Status: auth.UserStatusPending,
|
||||
EmailVerified: false,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
account := &auth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Credentials,
|
||||
Password: &hashedPasswordStr,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err = s.userRepo.CreateWithAccount(ctx, user, account); err != nil {
|
||||
s.logger.Error().Err(err).Msg("failed to create user and account")
|
||||
return nil, fmt.Errorf("failed to create user and account: %w", err)
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
tokens, genTokenErr := s.jwtService.GenerateAccessRefreshTokenPair(ctx, &jwt.TokenData{Sub: user.ID.String()})
|
||||
if genTokenErr != nil {
|
||||
return nil, fmt.Errorf("failed to generate tokens: %w", genTokenErr)
|
||||
}
|
||||
|
||||
// Update account with tokens
|
||||
account.AccessToken = &tokens.AccessToken
|
||||
account.RefreshToken = &tokens.RefreshToken
|
||||
now := time.Now()
|
||||
accessExpiry := now.Add(24 * time.Hour)
|
||||
refreshExpiry := now.Add(7 * 24 * time.Hour)
|
||||
account.AccessTokenExpiry = &accessExpiry
|
||||
account.RefreshTokenExpiry = &refreshExpiry
|
||||
|
||||
if err = s.accountRepo.Update(ctx, account); err != nil {
|
||||
s.logger.Error().Err(err).Msg("failed to update account with tokens")
|
||||
// Don't fail the registration, tokens are already generated
|
||||
}
|
||||
|
||||
// Profile is created when user calls setup-profile
|
||||
|
||||
return &dto.TokenResponse{
|
||||
AccessToken: tokens.AccessToken,
|
||||
RefreshToken: tokens.RefreshToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *service) LoginWithCredentials(ctx context.Context, email, password string) (*dto.TokenResponse, error) {
|
||||
// Find user by email with accounts
|
||||
user, err := s.userRepo.FindByEmail(ctx, email, auth.WithAccounts())
|
||||
if err != nil {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// Check user status
|
||||
if user.Status == auth.UserStatusDeleted {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// Find credentials account
|
||||
var credentialsAccount *auth.Account
|
||||
for _, acc := range user.Accounts {
|
||||
if acc.Provider == oauth.Credentials {
|
||||
credentialsAccount = &acc
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if credentialsAccount == nil || credentialsAccount.Password == nil {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// Verify password
|
||||
if err = bcrypt.CompareHashAndPassword([]byte(*credentialsAccount.Password), []byte(password)); err != nil {
|
||||
return nil, ErrInvalidCredentials
|
||||
}
|
||||
|
||||
// Generate tokens
|
||||
tokens, err := s.jwtService.GenerateAccessRefreshTokenPair(ctx, &jwt.TokenData{Sub: user.ID.String()})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate tokens: %w", err)
|
||||
}
|
||||
|
||||
// Update account with tokens
|
||||
credentialsAccount.AccessToken = &tokens.AccessToken
|
||||
credentialsAccount.RefreshToken = &tokens.RefreshToken
|
||||
now := time.Now()
|
||||
accessExpiry := now.Add(24 * time.Hour)
|
||||
refreshExpiry := now.Add(7 * 24 * time.Hour)
|
||||
credentialsAccount.AccessTokenExpiry = &accessExpiry
|
||||
credentialsAccount.RefreshTokenExpiry = &refreshExpiry
|
||||
|
||||
if err := s.accountRepo.Update(ctx, credentialsAccount); err != nil {
|
||||
s.logger.Error().Err(err).Msg("failed to update account with tokens")
|
||||
// Don't fail the login, tokens are already generated
|
||||
}
|
||||
|
||||
return &dto.TokenResponse{
|
||||
AccessToken: tokens.AccessToken,
|
||||
RefreshToken: tokens.RefreshToken,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *service) RefreshToken(ctx context.Context, refreshToken string) (*dto.TokenResponse, error) {
|
||||
claims, err := s.jwtService.VerifyToken(ctx, refreshToken)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRefreshToken
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(claims.Sub)
|
||||
if err != nil {
|
||||
return nil, ErrInvalidRefreshToken
|
||||
}
|
||||
|
||||
// Find user
|
||||
user, err := s.userRepo.FindByID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
accounts, err := s.accountRepo.FindByUserID(ctx, userID)
|
||||
if err != nil {
|
||||
return nil, ErrAccountNotFound
|
||||
}
|
||||
|
||||
var matchingAccount *auth.Account
|
||||
for _, acc := range accounts {
|
||||
if acc.RefreshToken != nil && *acc.RefreshToken == refreshToken {
|
||||
// Check if refresh token is expired
|
||||
if acc.RefreshTokenExpiry != nil && acc.RefreshTokenExpiry.Before(time.Now()) {
|
||||
return nil, ErrInvalidRefreshToken
|
||||
}
|
||||
matchingAccount = acc
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matchingAccount == nil {
|
||||
return nil, ErrInvalidRefreshToken
|
||||
}
|
||||
|
||||
tokens, err := s.jwtService.GenerateAccessRefreshTokenPair(ctx, &jwt.TokenData{Sub: user.ID.String()})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate tokens: %w", err)
|
||||
}
|
||||
|
||||
matchingAccount.AccessToken = &tokens.AccessToken
|
||||
matchingAccount.RefreshToken = &tokens.RefreshToken
|
||||
now := time.Now()
|
||||
accessExpiry := now.Add(24 * time.Hour)
|
||||
refreshExpiry := now.Add(7 * 24 * time.Hour)
|
||||
matchingAccount.AccessTokenExpiry = &accessExpiry
|
||||
matchingAccount.RefreshTokenExpiry = &refreshExpiry
|
||||
|
||||
if err = s.accountRepo.Update(ctx, matchingAccount); err != nil {
|
||||
s.logger.Error().Err(err).Msg("failed to update account with tokens")
|
||||
// Don't fail the refresh, tokens are already generated
|
||||
}
|
||||
|
||||
return &dto.TokenResponse{
|
||||
AccessToken: tokens.AccessToken,
|
||||
RefreshToken: tokens.RefreshToken,
|
||||
}, nil
|
||||
}
|
||||
131
internal/application/auth/reset_password.go
Normal file
131
internal/application/auth/reset_password.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"base/internal/domain/auth"
|
||||
"base/internal/dto"
|
||||
"base/internal/pkg/oauth"
|
||||
"base/pkg/email"
|
||||
"base/pkg/hashids"
|
||||
"base/pkg/jwt"
|
||||
)
|
||||
|
||||
// SendResetPasswordEmail sends a password reset email
|
||||
func (s *service) SendResetPasswordEmail(ctx context.Context, request dto.SendResetPasswordEmailRequest) error {
|
||||
user, err := s.userRepo.FindByEmail(ctx, request.Email)
|
||||
if err != nil {
|
||||
// Don't reveal if user exists or not for security
|
||||
return err
|
||||
}
|
||||
|
||||
// Generate reset code
|
||||
code := hashids.GenerateCode(int64(user.ID.Time()))
|
||||
key := fmt.Sprintf("reset_password:%s", user.ID.String())
|
||||
|
||||
// Store code in cache (15 minutes TTL)
|
||||
if storeErr := s.resetPasswordStore.Set(ctx, key, code, 15*time.Minute); storeErr != nil {
|
||||
return fmt.Errorf("failed to store reset password code: %w", storeErr)
|
||||
}
|
||||
|
||||
// Send email
|
||||
emailData := map[string]interface{}{
|
||||
"Code": code,
|
||||
"Name": user.FirstName,
|
||||
}
|
||||
|
||||
emailMsg := email.Request{
|
||||
To: user.Email,
|
||||
Subject: "Reset Your Password",
|
||||
Template: email.TemplateData{
|
||||
EmailTemplateName: email.TemplatePasswordReset,
|
||||
Data: emailData,
|
||||
},
|
||||
}
|
||||
|
||||
if _, sendEmailErr := s.emailService.Send(ctx, emailMsg); sendEmailErr != nil {
|
||||
return fmt.Errorf("failed to send reset password email: %w", sendEmailErr)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResetPassword resets a user's password with the provided code
|
||||
func (s *service) ResetPassword(ctx context.Context, request dto.ResetPasswordRequest) (*dto.TokenResponse, error) {
|
||||
user, err := s.userRepo.FindByEmail(ctx, request.Email, auth.WithAccounts())
|
||||
if err != nil {
|
||||
return nil, ErrUserNotFound
|
||||
}
|
||||
|
||||
// Get code from cache
|
||||
key := fmt.Sprintf("reset_password:%s", user.ID.String())
|
||||
|
||||
storedCode, found, getErr := s.resetPasswordStore.Get(ctx, key)
|
||||
if getErr != nil || !found {
|
||||
return nil, ErrInvalidVerificationCode
|
||||
}
|
||||
|
||||
if storedCode != request.Code {
|
||||
return nil, ErrInvalidVerificationCode
|
||||
}
|
||||
|
||||
// Find credentials account
|
||||
var credentialsAccount *auth.Account
|
||||
for _, acc := range user.Accounts {
|
||||
if acc.Provider == oauth.Credentials {
|
||||
credentialsAccount = &acc
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Hash new password
|
||||
hashedPassword, genHashPassErr := bcrypt.GenerateFromPassword([]byte(request.Password), bcrypt.DefaultCost)
|
||||
if genHashPassErr != nil {
|
||||
return nil, fmt.Errorf("failed to hash password: %w", genHashPassErr)
|
||||
}
|
||||
|
||||
hashedPasswordStr := string(hashedPassword)
|
||||
|
||||
if credentialsAccount != nil {
|
||||
// Update existing account
|
||||
credentialsAccount.Password = &hashedPasswordStr
|
||||
credentialsAccount.UpdatedAt = time.Now()
|
||||
|
||||
if err := s.accountRepo.Update(ctx, credentialsAccount); err != nil {
|
||||
return nil, fmt.Errorf("failed to update account: %w", err)
|
||||
}
|
||||
} else {
|
||||
// Create new credentials account
|
||||
account := &auth.Account{
|
||||
ID: uuid.New(),
|
||||
UserID: user.ID,
|
||||
Provider: oauth.Credentials,
|
||||
Password: &hashedPasswordStr,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := s.accountRepo.Create(ctx, account); err != nil {
|
||||
return nil, fmt.Errorf("failed to create account: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Delete reset code from cache
|
||||
_ = s.resetPasswordStore.Delete(ctx, key)
|
||||
|
||||
// Generate tokens
|
||||
tokens, err := s.jwtService.GenerateAccessRefreshTokenPair(ctx, &jwt.TokenData{Sub: user.ID.String()})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate tokens: %w", err)
|
||||
}
|
||||
|
||||
return &dto.TokenResponse{
|
||||
AccessToken: tokens.AccessToken,
|
||||
RefreshToken: tokens.RefreshToken,
|
||||
}, nil
|
||||
}
|
||||
87
internal/application/auth/service.go
Normal file
87
internal/application/auth/service.go
Normal file
@@ -0,0 +1,87 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"base/config"
|
||||
"base/internal/domain/auth"
|
||||
"base/internal/domain/profile"
|
||||
"base/internal/dto"
|
||||
"base/internal/pkg/oauth"
|
||||
"base/pkg/email"
|
||||
"base/pkg/jwt"
|
||||
"base/pkg/store"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
ErrUserAlreadyExists = errors.New("user already exists")
|
||||
ErrInvalidVerificationCode = errors.New("invalid verification code")
|
||||
ErrEmailAlreadyVerified = errors.New("email already verified")
|
||||
ErrInvalidRefreshToken = errors.New("invalid refresh token")
|
||||
ErrAccountNotFound = errors.New("account not found")
|
||||
ErrProfileNotFound = errors.New("profile not found")
|
||||
ErrProfileAlreadyExists = errors.New("profile already exists")
|
||||
ErrHandleAlreadyTaken = errors.New("handle already taken")
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
RegisterWithCredentials(ctx context.Context, request dto.RegisterRequest) (*dto.TokenResponse, error)
|
||||
LoginWithCredentials(ctx context.Context, email, password string) (*dto.TokenResponse, error)
|
||||
RefreshToken(ctx context.Context, refreshToken string) (*dto.TokenResponse, error)
|
||||
GetOAuthRedirectURL(ctx context.Context, request dto.OAuthRedirectURLRequest) (string, error)
|
||||
OAuthCallback(ctx context.Context, request dto.OAuthCallbackRequest) (*dto.OAuthCallbackResponse, error)
|
||||
SendVerificationEmail(ctx context.Context, request dto.SendVerificationEmailRequest) error
|
||||
VerifyAccount(ctx context.Context, request dto.VerifyAccountRequest) error
|
||||
SendResetPasswordEmail(ctx context.Context, request dto.SendResetPasswordEmailRequest) error
|
||||
ResetPassword(ctx context.Context, request dto.ResetPasswordRequest) (*dto.TokenResponse, error)
|
||||
SetupProfile(ctx context.Context, userID uuid.UUID, request dto.SetupProfileRequest) error
|
||||
GetUserInfo(ctx context.Context, userID uuid.UUID) (*dto.UserInfoResponse, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
logger zerolog.Logger
|
||||
config *config.AppConfig
|
||||
userRepo auth.UserRepository
|
||||
accountRepo auth.AccountRepository
|
||||
profileRepo profile.Repository
|
||||
emailService email.Email
|
||||
oauthService oauth.OAuth
|
||||
verificationStore store.Store[string]
|
||||
resetPasswordStore store.Store[string]
|
||||
jwtService jwt.TokenService
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
Logger zerolog.Logger
|
||||
Config *config.AppConfig
|
||||
UserRepo auth.UserRepository
|
||||
AccountRepo auth.AccountRepository
|
||||
ProfileRepo profile.Repository
|
||||
EmailService email.Email
|
||||
OAuthService oauth.OAuth
|
||||
VerificationStore store.Store[string] `name:"verification_store"`
|
||||
ResetPasswordStore store.Store[string] `name:"reset_password_store"`
|
||||
|
||||
fx.In
|
||||
}
|
||||
|
||||
func New(param Param) Service {
|
||||
return &service{
|
||||
logger: param.Logger,
|
||||
config: param.Config,
|
||||
userRepo: param.UserRepo,
|
||||
accountRepo: param.AccountRepo,
|
||||
profileRepo: param.ProfileRepo,
|
||||
emailService: param.EmailService,
|
||||
oauthService: param.OAuthService,
|
||||
verificationStore: param.VerificationStore,
|
||||
resetPasswordStore: param.ResetPasswordStore,
|
||||
jwtService: jwt.New(param.Config.JWT.Secret, param.Config.JWT.AccessTokenExpiration, param.Config.JWT.RefreshTokenExpiration),
|
||||
}
|
||||
}
|
||||
76
internal/application/auth/setup_profile.go
Normal file
76
internal/application/auth/setup_profile.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
||||
"base/internal/domain/profile"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
var slugRe = regexp.MustCompile(`[^a-z0-9-]+`)
|
||||
|
||||
func (s *service) SetupProfile(ctx context.Context, userID uuid.UUID, req dto.SetupProfileRequest) error {
|
||||
existingProfile, _ := s.profileRepo.FindByUserID(ctx, userID)
|
||||
if existingProfile != nil {
|
||||
return ErrProfileAlreadyExists
|
||||
}
|
||||
|
||||
user, err := s.userRepo.FindByID(ctx, userID)
|
||||
if err != nil || user == nil {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
|
||||
handle := generateHandle(user.FirstName, user.LastName, userID)
|
||||
if req.Handle != "" {
|
||||
handle = req.Handle
|
||||
}
|
||||
other, _ := s.profileRepo.FindByHandle(ctx, handle)
|
||||
if other != nil {
|
||||
return ErrHandleAlreadyTaken
|
||||
}
|
||||
|
||||
newProfile := &profile.Profile{
|
||||
ID: uuid.New(),
|
||||
UserID: &userID,
|
||||
Handle: handle,
|
||||
Hero: profile.Hero{
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
ShortDescription: req.ShortDescription,
|
||||
},
|
||||
Contact: profile.Contact{
|
||||
Email: user.Email,
|
||||
Phone: user.PhoneNumber,
|
||||
},
|
||||
PageSetting: profile.PageSetting{
|
||||
VisibilityLevel: "public",
|
||||
},
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
newProfile.Hero.Role = &profile.Role{ID: req.RoleID}
|
||||
|
||||
if req.RoleLevel != "" && newProfile.Hero.Role != nil {
|
||||
newProfile.Hero.Role.Level = req.RoleLevel
|
||||
} else if req.RoleLevel != "" {
|
||||
newProfile.Hero.Role = &profile.Role{Level: req.RoleLevel}
|
||||
}
|
||||
|
||||
return s.profileRepo.Create(ctx, newProfile)
|
||||
}
|
||||
|
||||
func generateHandle(firstName, lastName string, userID uuid.UUID) string {
|
||||
slug := slugRe.ReplaceAllString(strings.ToLower(strings.TrimSpace(firstName+"-"+lastName)), "-")
|
||||
slug = strings.Trim(slug, "-")
|
||||
if slug == "" {
|
||||
slug = "user"
|
||||
}
|
||||
return fmt.Sprintf("%s-%s", slug, userID.String()[:8])
|
||||
}
|
||||
16
internal/application/auth/utils.go
Normal file
16
internal/application/auth/utils.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
)
|
||||
|
||||
func generateOTP() (string, error) {
|
||||
newInt := big.NewInt(10000) // 0 .. 999999
|
||||
n, err := rand.Int(rand.Reader, newInt)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return fmt.Sprintf("%04d", n.Int64()), err
|
||||
}
|
||||
62
internal/application/auth/verify.go
Normal file
62
internal/application/auth/verify.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"base/internal/domain/auth"
|
||||
"base/internal/dto"
|
||||
"base/pkg/email"
|
||||
)
|
||||
|
||||
func (s *service) SendVerificationEmail(ctx context.Context, request dto.SendVerificationEmailRequest) error {
|
||||
emailMsg := email.Request{
|
||||
To: request.Email,
|
||||
Subject: "Verify Your Email",
|
||||
Template: email.TemplateData{EmailTemplateName: email.TemplateEmailVerification},
|
||||
}
|
||||
|
||||
if _, err := s.emailService.Send(ctx, emailMsg); err != nil {
|
||||
s.logger.Error().Err(err).Msg("failed to send verification email")
|
||||
return fmt.Errorf("failed to send verification email: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *service) VerifyAccount(ctx context.Context, request dto.VerifyAccountRequest) error {
|
||||
user, err := s.userRepo.FindByEmail(ctx, request.Email)
|
||||
if err != nil {
|
||||
return ErrUserNotFound
|
||||
}
|
||||
|
||||
if user.EmailVerified {
|
||||
return ErrEmailAlreadyVerified
|
||||
}
|
||||
|
||||
// Get code from cache
|
||||
key := fmt.Sprintf("verification:%s", request.Email)
|
||||
|
||||
storedCode, found, err := s.verificationStore.Get(ctx, key)
|
||||
if err != nil || !found {
|
||||
return ErrInvalidVerificationCode
|
||||
}
|
||||
|
||||
if storedCode != request.Code {
|
||||
return ErrInvalidVerificationCode
|
||||
}
|
||||
|
||||
user.EmailVerified = true
|
||||
user.Status = auth.UserStatusActive
|
||||
user.UpdatedAt = time.Now()
|
||||
|
||||
if err := s.userRepo.Update(ctx, user); err != nil {
|
||||
return fmt.Errorf("failed to update user: %w", err)
|
||||
}
|
||||
|
||||
// Delete verification code from cache
|
||||
_ = s.verificationStore.Delete(ctx, key)
|
||||
|
||||
return nil
|
||||
}
|
||||
272
internal/application/discovery/service.go
Normal file
272
internal/application/discovery/service.go
Normal file
@@ -0,0 +1,272 @@
|
||||
package discovery
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.uber.org/fx"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
domainAsset "base/internal/domain/asset"
|
||||
domainProfile "base/internal/domain/profile"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GetDiscoveryOverview(ctx context.Context) (*dto.OverviewFetchedResponse, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
logger zerolog.Logger
|
||||
profileRepo domainProfile.Repository
|
||||
assetRepo domainAsset.AssetRepository
|
||||
categoryRepo domainAsset.CategoryRepository
|
||||
}
|
||||
|
||||
// Param holds dependencies for the discovery overview service.
|
||||
type Param struct {
|
||||
Logger zerolog.Logger
|
||||
ProfileRepo domainProfile.Repository
|
||||
AssetRepo domainAsset.AssetRepository
|
||||
CategoryRepo domainAsset.CategoryRepository
|
||||
|
||||
fx.In
|
||||
}
|
||||
|
||||
func New(param Param) Service {
|
||||
return &service{
|
||||
logger: param.Logger,
|
||||
profileRepo: param.ProfileRepo,
|
||||
assetRepo: param.AssetRepo,
|
||||
categoryRepo: param.CategoryRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) GetDiscoveryOverview(ctx context.Context) (*dto.OverviewFetchedResponse, error) {
|
||||
resp := &struct {
|
||||
domainAssets []*domainAsset.Asset
|
||||
recentlyJoined []*domainProfile.Profile
|
||||
totalProfiles int
|
||||
totalAssets int
|
||||
}{}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
assets, err := s.assetRepo.FindLatest(gCtx, 6, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.domainAssets = assets
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
profiles, total, err := s.profileRepo.FindAll(gCtx, domainProfile.Filter{
|
||||
Page: 1,
|
||||
PageSize: 6,
|
||||
SortedBy: "created_at",
|
||||
Ascending: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.recentlyJoined = profiles
|
||||
resp.totalProfiles = total
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
count, err := s.assetRepo.Count(gCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.totalAssets = count
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assets := s.toOverviewAssets(ctx, resp.domainAssets)
|
||||
flatProfiles := ToFlatProfiles(resp.recentlyJoined)
|
||||
|
||||
return &dto.OverviewFetchedResponse{
|
||||
Message: "Overview fetched successfully",
|
||||
Data: dto.OverviewFetchedDataDTO{
|
||||
Assets: assets,
|
||||
RecentlyJoined: flatProfiles,
|
||||
Analytics: dto.AnalyticsDTO{
|
||||
TotalAssets: resp.totalAssets,
|
||||
TotalProfiles: resp.totalProfiles,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *service) toOverviewAssets(ctx context.Context, assets []*domainAsset.Asset) []dto.OverviewAssetDTO {
|
||||
out := make([]dto.OverviewAssetDTO, len(assets))
|
||||
for i, a := range assets {
|
||||
out[i] = s.toOverviewAsset(ctx, a)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *service) toOverviewAsset(ctx context.Context, a *domainAsset.Asset) dto.OverviewAssetDTO {
|
||||
price := 0
|
||||
coverImage := ""
|
||||
for _, art := range a.AssetArtifacts {
|
||||
if strings.Contains(strings.ToLower(art.Type), "image") {
|
||||
coverImage = art.DownloadURL
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(a.AssetArtifacts) > 0 {
|
||||
price = a.AssetArtifacts[0].Price
|
||||
}
|
||||
|
||||
cat := (*dto.CategoryDTO)(nil)
|
||||
if a.AssetCategory.ID != uuid.Nil {
|
||||
cat = &dto.CategoryDTO{
|
||||
ID: a.AssetCategory.ID,
|
||||
Name: a.AssetCategory.Name,
|
||||
Icon: a.AssetCategory.Icon,
|
||||
Color: a.AssetCategory.Color,
|
||||
CardType: a.AssetCategory.CardType,
|
||||
Featured: a.AssetCategory.Featured,
|
||||
Description: a.AssetCategory.Description,
|
||||
}
|
||||
}
|
||||
|
||||
ownerID := a.ProfileID.String()
|
||||
if p, err := s.profileRepo.FindByID(ctx, a.ProfileID); err == nil && p.UserID != nil {
|
||||
ownerID = p.UserID.String()
|
||||
}
|
||||
|
||||
return dto.OverviewAssetDTO{
|
||||
ID: a.ID.String(),
|
||||
Title: a.Title,
|
||||
Description: a.Description,
|
||||
Content: a.Description,
|
||||
AssetCategoryID: a.AssetCategoryID.String(),
|
||||
AssetCategory: cat,
|
||||
CoverImage: coverImage,
|
||||
Link: a.Link,
|
||||
OwnerID: ownerID,
|
||||
ProfileID: a.ProfileID.String(),
|
||||
Profile: nil,
|
||||
Price: price,
|
||||
Currency: "USD",
|
||||
Status: assetStatusToString(a.Status),
|
||||
Rating: 0,
|
||||
CreatedAt: a.CreatedAt,
|
||||
UpdatedAt: a.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func assetStatusToString(st domainAsset.Status) string {
|
||||
switch st {
|
||||
case domainAsset.StatusPublished:
|
||||
return "published"
|
||||
case domainAsset.StatusDisabled:
|
||||
return "disabled"
|
||||
case domainAsset.StatusPending:
|
||||
return "pending"
|
||||
case domainAsset.StatusDeleted:
|
||||
return "deleted"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func formatTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
|
||||
// ToFlatProfiles converts domain profiles to flat DTOs.
|
||||
func ToFlatProfiles(profiles []*domainProfile.Profile) []dto.FlatProfileDTO {
|
||||
out := make([]dto.FlatProfileDTO, len(profiles))
|
||||
for i, p := range profiles {
|
||||
out[i] = ToFlatProfile(p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ToFlatProfile converts a single profile to flat DTO.
|
||||
func ToFlatProfile(p *domainProfile.Profile) dto.FlatProfileDTO {
|
||||
roleID := ""
|
||||
roleName := ""
|
||||
if p.Hero.Role != nil {
|
||||
roleID = p.Hero.Role.ID.String()
|
||||
if p.Hero.Role.Title != "" {
|
||||
roleName = p.Hero.Role.Title
|
||||
}
|
||||
}
|
||||
|
||||
achievements := make(map[string]dto.AchievementItemDTO)
|
||||
for _, a := range p.About.Achievements {
|
||||
key := strings.ToLower(strings.ReplaceAll(a.Title, " ", ""))
|
||||
if key == "" {
|
||||
key = a.Title
|
||||
}
|
||||
achievements[key] = dto.AchievementItemDTO{Value: a.Value, Enabled: a.Enabled}
|
||||
}
|
||||
if len(achievements) == 0 {
|
||||
achievements = map[string]dto.AchievementItemDTO{
|
||||
"happyClient": {Value: "", Enabled: true},
|
||||
"yearExperience": {Value: "", Enabled: true},
|
||||
"projectCompeleted": {Value: "", Enabled: true},
|
||||
}
|
||||
}
|
||||
|
||||
var socialLinks []dto.SocialLinkDTO
|
||||
for _, sl := range p.Contact.SocialLinks {
|
||||
socialLinks = append(socialLinks, dto.SocialLinkDTO{LinkType: sl.LinkType, Link: sl.Link})
|
||||
}
|
||||
|
||||
displayName := strings.TrimSpace(p.Hero.FirstName + " " + p.Hero.LastName)
|
||||
if displayName == "" {
|
||||
displayName = p.Handle
|
||||
}
|
||||
|
||||
status := "published"
|
||||
if p.PageSetting.VisibilityLevel != "public" {
|
||||
status = "draft"
|
||||
}
|
||||
|
||||
return dto.FlatProfileDTO{
|
||||
ID: p.ID.String(),
|
||||
ProfileHandle: p.Handle,
|
||||
Status: status,
|
||||
BackgroundImage: "",
|
||||
ProfilePicture: p.About.ProfilePicture,
|
||||
FirstName: p.Hero.FirstName,
|
||||
LastName: p.Hero.LastName,
|
||||
DisplayName: displayName,
|
||||
RoleID: roleID,
|
||||
Role: dto.RoleDTO{ID: roleID, Name: roleName},
|
||||
CurrentCompany: p.Hero.Company,
|
||||
ShortDescription: p.Hero.ShortDescription,
|
||||
CTAEnabled: p.Hero.CTAEnabled,
|
||||
CTAAction: "",
|
||||
ResumeLink: p.Hero.ResumeLink,
|
||||
About: p.About.About,
|
||||
ContactEmail: p.Contact.Email,
|
||||
Achievements: achievements,
|
||||
ContactPhone: p.Contact.Phone,
|
||||
Country: "",
|
||||
CustomRoles: "",
|
||||
RoleLevel: p.Hero.Role.Level,
|
||||
SocialLinks: socialLinks,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
HandleUpdatedAt: time.Time{},
|
||||
}
|
||||
}
|
||||
238
internal/application/landing/service.go
Normal file
238
internal/application/landing/service.go
Normal file
@@ -0,0 +1,238 @@
|
||||
package landing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.uber.org/fx"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
domainAsset "base/internal/domain/asset"
|
||||
domainProfile "base/internal/domain/profile"
|
||||
"base/internal/dto"
|
||||
"base/pkg/cache"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultAssetsPerCategory = 6
|
||||
defaultSpecialistsLimit = 6
|
||||
defaultRolesLimit = 20
|
||||
landingCacheKey = "landing:page"
|
||||
landingCacheTTL = 5 * time.Minute
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
GetLanding(ctx context.Context) (*dto.Landing, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
logger zerolog.Logger
|
||||
cache cache.Cache[dto.Landing]
|
||||
categoryRepo domainAsset.CategoryRepository
|
||||
assetRepo domainAsset.AssetRepository
|
||||
profileRepo domainProfile.Repository
|
||||
roleRepo domainProfile.RoleRepository
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
Logger zerolog.Logger
|
||||
Cache cache.Cache[dto.Landing]
|
||||
CategoryRepo domainAsset.CategoryRepository
|
||||
AssetRepo domainAsset.AssetRepository
|
||||
ProfileRepo domainProfile.Repository
|
||||
RoleRepo domainProfile.RoleRepository
|
||||
|
||||
fx.In
|
||||
}
|
||||
|
||||
func New(param Param) Service {
|
||||
return &service{
|
||||
logger: param.Logger,
|
||||
cache: param.Cache,
|
||||
categoryRepo: param.CategoryRepo,
|
||||
assetRepo: param.AssetRepo,
|
||||
profileRepo: param.ProfileRepo,
|
||||
roleRepo: param.RoleRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) GetLanding(ctx context.Context) (*dto.Landing, error) {
|
||||
result, err := s.cache.WithCache(ctx, landingCacheKey, s.fetchLanding, landingCacheTTL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *service) fetchLanding(ctx context.Context) (dto.Landing, error) {
|
||||
data := &dto.LandingPageData{
|
||||
Categories: []dto.CategoryDTO{},
|
||||
SpecialistRoles: []dto.ProfileRole{},
|
||||
Assets: []dto.LandingAssetData{},
|
||||
Specialists: []dto.Specialist{},
|
||||
Blogs: []dto.Blog{},
|
||||
}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
g.Go(func() error {
|
||||
categories, err := s.categoryRepo.FindAll(gCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data.Categories = make([]dto.CategoryDTO, len(categories))
|
||||
for i, c := range categories {
|
||||
data.Categories[i] = dto.CategoryDTO{
|
||||
ID: c.ID,
|
||||
Name: c.Name,
|
||||
Icon: c.Icon,
|
||||
Color: c.Color,
|
||||
CardType: c.CardType,
|
||||
Featured: c.Featured,
|
||||
Description: c.Description,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
domainRoles, err := s.roleRepo.List(gCtx, defaultRolesLimit, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data.SpecialistRoles = make([]dto.ProfileRole, len(domainRoles))
|
||||
for i, r := range domainRoles {
|
||||
data.SpecialistRoles[i] = dto.ProfileRole{
|
||||
Id: r.ID.String(),
|
||||
Title: r.Title,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
profiles, _, err := s.profileRepo.FindAll(
|
||||
gCtx,
|
||||
domainProfile.Filter{
|
||||
Page: 1,
|
||||
PageSize: defaultSpecialistsLimit,
|
||||
SortedBy: "created_at",
|
||||
Ascending: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
data.Specialists = make([]dto.Specialist, len(profiles))
|
||||
for i, p := range profiles {
|
||||
data.Specialists[i] = dto.Specialist{
|
||||
Id: p.ID.String(),
|
||||
Handle: p.Handle,
|
||||
Avatar: p.About.ProfilePicture,
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
g.Go(func() error {
|
||||
categories, err := s.categoryRepo.FindAll(gCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
assetsByCat := make([]dto.LandingAssetData, len(categories))
|
||||
mu := &sync.Mutex{}
|
||||
eg, egCtx := errgroup.WithContext(gCtx)
|
||||
|
||||
for index, category := range categories {
|
||||
i, cat := index, category
|
||||
|
||||
eg.Go(func() error {
|
||||
assets, findLatestAssetErr := s.assetRepo.FindLatestByCategory(egCtx, cat.ID, defaultAssetsPerCategory)
|
||||
if findLatestAssetErr != nil {
|
||||
return findLatestAssetErr
|
||||
}
|
||||
|
||||
assetResp := make([]dto.AssetResponse, len(assets))
|
||||
for j, a := range assets {
|
||||
assetResp[j] = *s.toAssetResponse(a)
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
|
||||
assetsByCat[i] = dto.LandingAssetData{
|
||||
AssetCategory: dto.AssetCategory{
|
||||
Id: cat.ID.String(),
|
||||
Title: cat.Name,
|
||||
Icon: cat.Icon,
|
||||
},
|
||||
Assets: assetResp,
|
||||
}
|
||||
|
||||
mu.Unlock()
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
if err = eg.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
data.Assets = assetsByCat
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return dto.Landing{}, err
|
||||
}
|
||||
|
||||
return dto.Landing{
|
||||
Message: "Landing page fetched successfully",
|
||||
Data: *data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *service) toAssetResponse(a *domainAsset.Asset) *dto.AssetResponse {
|
||||
coverImage := ""
|
||||
for _, art := range a.AssetArtifacts {
|
||||
if strings.Contains(strings.ToLower(art.Type), "image") {
|
||||
coverImage = art.DownloadURL
|
||||
break
|
||||
}
|
||||
}
|
||||
resp := &dto.AssetResponse{
|
||||
ID: a.ID,
|
||||
ProfileID: a.ProfileID,
|
||||
AssetCategoryID: a.AssetCategoryID,
|
||||
Title: a.Title,
|
||||
Description: a.Description,
|
||||
Link: a.Link,
|
||||
CoverImage: coverImage,
|
||||
Status: int(a.Status),
|
||||
CreatedAt: formatTime(a.CreatedAt),
|
||||
UpdatedAt: formatTime(a.UpdatedAt),
|
||||
}
|
||||
if a.AssetCategory.ID != uuid.Nil {
|
||||
resp.Category = dto.CategoryDTO{
|
||||
ID: a.AssetCategory.ID,
|
||||
Name: a.AssetCategory.Name,
|
||||
Icon: a.AssetCategory.Icon,
|
||||
Color: a.AssetCategory.Color,
|
||||
CardType: a.AssetCategory.CardType,
|
||||
Featured: a.AssetCategory.Featured,
|
||||
Description: a.AssetCategory.Description,
|
||||
}
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func formatTime(t time.Time) string {
|
||||
if t.IsZero() {
|
||||
return ""
|
||||
}
|
||||
return t.Format(time.RFC3339)
|
||||
}
|
||||
28
internal/application/module.go
Normal file
28
internal/application/module.go
Normal file
@@ -0,0 +1,28 @@
|
||||
package application
|
||||
|
||||
import (
|
||||
"go.uber.org/fx"
|
||||
|
||||
"base/internal/application/asset"
|
||||
"base/internal/application/auth"
|
||||
"base/internal/application/discovery"
|
||||
"base/internal/application/landing"
|
||||
"base/internal/application/profile"
|
||||
"base/internal/application/profilerole"
|
||||
"base/internal/application/skill"
|
||||
"base/internal/application/specialist"
|
||||
)
|
||||
|
||||
var Module = fx.Module(
|
||||
"application",
|
||||
fx.Provide(
|
||||
auth.New,
|
||||
profile.New,
|
||||
asset.New,
|
||||
discovery.New,
|
||||
landing.New,
|
||||
specialist.New,
|
||||
profilerole.New,
|
||||
skill.New,
|
||||
),
|
||||
)
|
||||
72
internal/application/profile/converter.go
Normal file
72
internal/application/profile/converter.go
Normal file
@@ -0,0 +1,72 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"base/internal/domain/profile"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
// DomainProfileToProfileResponse converts a domain profile to ProfileResponse.
|
||||
// Used by specialist overview and other consumers that have a domain profile.
|
||||
func DomainProfileToProfileResponse(p *profile.Profile) *dto.ProfileResponse {
|
||||
if p == nil {
|
||||
return nil
|
||||
}
|
||||
roleLevel := ""
|
||||
if p.Hero.Role != nil {
|
||||
roleLevel = p.Hero.Role.Level
|
||||
}
|
||||
resp := &dto.ProfileResponse{
|
||||
ID: p.ID,
|
||||
Handle: p.Handle,
|
||||
PageSectionOrder: p.PageSectionOrder,
|
||||
Hero: dto.HeroDTO{
|
||||
RoleLevel: roleLevel,
|
||||
FirstName: p.Hero.FirstName,
|
||||
LastName: p.Hero.LastName,
|
||||
Company: p.Hero.Company,
|
||||
ShortDescription: p.Hero.ShortDescription,
|
||||
ResumeLink: p.Hero.ResumeLink,
|
||||
CTAEnabled: p.Hero.CTAEnabled,
|
||||
Avatar: p.Hero.Avatar,
|
||||
},
|
||||
About: dto.AboutDTO{
|
||||
ProfilePicture: p.About.ProfilePicture,
|
||||
About: p.About.About,
|
||||
},
|
||||
Contact: dto.ContactDTO{
|
||||
Email: p.Contact.Email,
|
||||
Phone: p.Contact.Phone,
|
||||
},
|
||||
PageSetting: dto.PageSettingDTO{
|
||||
VisibilityLevel: p.PageSetting.VisibilityLevel,
|
||||
},
|
||||
}
|
||||
|
||||
if p.Hero.Role != nil {
|
||||
resp.Hero.RoleID = &p.Hero.Role.ID
|
||||
}
|
||||
|
||||
for _, skill := range p.Skills {
|
||||
resp.Skills = append(resp.Skills, dto.SkillDTO{
|
||||
SkillName: skill.SkillName,
|
||||
Level: skill.Level,
|
||||
})
|
||||
}
|
||||
|
||||
for _, achievement := range p.About.Achievements {
|
||||
resp.About.Achievements = append(resp.About.Achievements, dto.AchievementDTO{
|
||||
Title: achievement.Title,
|
||||
Value: achievement.Value,
|
||||
Enabled: achievement.Enabled,
|
||||
})
|
||||
}
|
||||
|
||||
for _, sl := range p.Contact.SocialLinks {
|
||||
resp.Contact.SocialLinks = append(resp.Contact.SocialLinks, dto.SocialLinkDTO{
|
||||
LinkType: sl.LinkType,
|
||||
Link: sl.Link,
|
||||
})
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
315
internal/application/profile/service.go
Normal file
315
internal/application/profile/service.go
Normal file
@@ -0,0 +1,315 @@
|
||||
package profile
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/samber/lo"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"base/internal/domain/profile"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Create(ctx context.Context, req dto.CreateProfileRequest) (*dto.ProfileResponse, error)
|
||||
Update(ctx context.Context, req dto.UpdateProfileRequest) (*dto.ProfileResponse, error)
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*dto.ProfileResponse, error)
|
||||
GetByHandle(ctx context.Context, handle string) (*dto.ProfileResponse, error)
|
||||
List(ctx context.Context, req dto.ListProfilesRequest) (*dto.ListProfilesResponse, error)
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
logger zerolog.Logger
|
||||
profileRepo profile.Repository
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
Logger zerolog.Logger
|
||||
ProfileRepo profile.Repository
|
||||
|
||||
fx.In
|
||||
}
|
||||
|
||||
func New(param Param) Service {
|
||||
return &service{
|
||||
logger: param.Logger,
|
||||
profileRepo: param.ProfileRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) Create(ctx context.Context, req dto.CreateProfileRequest) (*dto.ProfileResponse, error) {
|
||||
domainProfile := &profile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: req.Handle,
|
||||
PageSectionOrder: req.PageSectionOrder,
|
||||
Hero: profile.Hero{
|
||||
Role: &profile.Role{
|
||||
ID: lo.FromPtr(req.Hero.RoleID),
|
||||
Level: req.Hero.RoleLevel,
|
||||
},
|
||||
FirstName: req.Hero.FirstName,
|
||||
LastName: req.Hero.LastName,
|
||||
Company: req.Hero.Company,
|
||||
ShortDescription: req.Hero.ShortDescription,
|
||||
ResumeLink: req.Hero.ResumeLink,
|
||||
CTAEnabled: req.Hero.CTAEnabled,
|
||||
Avatar: req.Hero.Avatar,
|
||||
},
|
||||
About: profile.About{
|
||||
ProfilePicture: req.About.ProfilePicture,
|
||||
About: req.About.About,
|
||||
},
|
||||
Contact: profile.Contact{
|
||||
Email: req.Contact.Email,
|
||||
Phone: req.Contact.Phone,
|
||||
},
|
||||
PageSetting: profile.PageSetting{
|
||||
VisibilityLevel: req.PageSetting.VisibilityLevel,
|
||||
},
|
||||
}
|
||||
|
||||
if req.Hero.RoleID != nil {
|
||||
domainProfile.Hero.Role = &profile.Role{
|
||||
ID: *req.Hero.RoleID,
|
||||
}
|
||||
}
|
||||
|
||||
for _, skill := range req.Skills {
|
||||
domainProfile.Skills = append(domainProfile.Skills, profile.Skill{
|
||||
SkillName: skill.SkillName,
|
||||
Level: skill.Level,
|
||||
})
|
||||
}
|
||||
|
||||
for _, achievement := range req.About.Achievements {
|
||||
domainProfile.About.Achievements = append(domainProfile.About.Achievements, profile.Achievement{
|
||||
Title: achievement.Title,
|
||||
Value: achievement.Value,
|
||||
Enabled: achievement.Enabled,
|
||||
})
|
||||
}
|
||||
|
||||
for _, socialLink := range req.Contact.SocialLinks {
|
||||
domainProfile.Contact.SocialLinks = append(domainProfile.Contact.SocialLinks, profile.SocialLink{
|
||||
LinkType: socialLink.LinkType,
|
||||
Link: socialLink.Link,
|
||||
})
|
||||
}
|
||||
|
||||
if err := s.profileRepo.Create(ctx, domainProfile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toProfileResponse(domainProfile), nil
|
||||
}
|
||||
|
||||
func (s *service) Update(ctx context.Context, req dto.UpdateProfileRequest) (*dto.ProfileResponse, error) {
|
||||
id, err := uuid.Parse(req.ID)
|
||||
if err != nil {
|
||||
return nil, profile.ErrProfileNotFound
|
||||
}
|
||||
|
||||
// First, get the existing profile to ensure it exists
|
||||
existingProfile, err := s.profileRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, profile.ErrProfileNotFound
|
||||
}
|
||||
|
||||
domainProfile := &profile.Profile{
|
||||
ID: id,
|
||||
Handle: req.Handle,
|
||||
PageSectionOrder: req.PageSectionOrder,
|
||||
Hero: profile.Hero{
|
||||
FirstName: req.Hero.FirstName,
|
||||
Role: &profile.Role{
|
||||
ID: lo.FromPtr(req.Hero.RoleID),
|
||||
Level: req.Hero.RoleLevel,
|
||||
},
|
||||
LastName: req.Hero.LastName,
|
||||
Company: req.Hero.Company,
|
||||
ShortDescription: req.Hero.ShortDescription,
|
||||
ResumeLink: req.Hero.ResumeLink,
|
||||
CTAEnabled: req.Hero.CTAEnabled,
|
||||
Avatar: req.Hero.Avatar,
|
||||
},
|
||||
About: profile.About{
|
||||
ProfilePicture: req.About.ProfilePicture,
|
||||
About: req.About.About,
|
||||
},
|
||||
Contact: profile.Contact{
|
||||
Email: req.Contact.Email,
|
||||
Phone: req.Contact.Phone,
|
||||
},
|
||||
PageSetting: profile.PageSetting{
|
||||
VisibilityLevel: req.PageSetting.VisibilityLevel,
|
||||
},
|
||||
}
|
||||
|
||||
if req.Hero.RoleID != nil {
|
||||
domainProfile.Hero.Role = &profile.Role{
|
||||
ID: *req.Hero.RoleID,
|
||||
}
|
||||
} else if existingProfile != nil && existingProfile.Hero.Role != nil {
|
||||
domainProfile.Hero.Role = existingProfile.Hero.Role
|
||||
}
|
||||
|
||||
if req.Hero.RoleLevel == "" && existingProfile != nil {
|
||||
domainProfile.Hero.Role.Level = existingProfile.Hero.Role.Level
|
||||
}
|
||||
|
||||
for _, skill := range req.Skills {
|
||||
domainProfile.Skills = append(domainProfile.Skills, profile.Skill{
|
||||
SkillName: skill.SkillName,
|
||||
Level: skill.Level,
|
||||
})
|
||||
}
|
||||
|
||||
for _, achievement := range req.About.Achievements {
|
||||
domainProfile.About.Achievements = append(domainProfile.About.Achievements, profile.Achievement{
|
||||
Title: achievement.Title,
|
||||
Value: achievement.Value,
|
||||
Enabled: achievement.Enabled,
|
||||
})
|
||||
}
|
||||
|
||||
for _, socialLink := range req.Contact.SocialLinks {
|
||||
domainProfile.Contact.SocialLinks = append(domainProfile.Contact.SocialLinks, profile.SocialLink{
|
||||
LinkType: socialLink.LinkType,
|
||||
Link: socialLink.Link,
|
||||
})
|
||||
}
|
||||
|
||||
if err := s.profileRepo.Update(ctx, domainProfile); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s.toProfileResponse(domainProfile), nil
|
||||
}
|
||||
|
||||
func (s *service) GetByID(ctx context.Context, id uuid.UUID) (*dto.ProfileResponse, error) {
|
||||
profileData, err := s.profileRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, profile.ErrProfileNotFound
|
||||
}
|
||||
|
||||
return s.toProfileResponse(profileData), nil
|
||||
}
|
||||
|
||||
func (s *service) GetByHandle(ctx context.Context, handle string) (*dto.ProfileResponse, error) {
|
||||
profileData, err := s.profileRepo.FindByHandle(ctx, handle)
|
||||
if err != nil {
|
||||
return nil, profile.ErrProfileNotFound
|
||||
}
|
||||
|
||||
return s.toProfileResponse(profileData), nil
|
||||
}
|
||||
|
||||
func (s *service) List(ctx context.Context, req dto.ListProfilesRequest) (*dto.ListProfilesResponse, error) {
|
||||
filter := profile.Filter{
|
||||
FirstName: req.FirstName,
|
||||
LastName: req.LastName,
|
||||
Company: req.Company,
|
||||
SkillName: req.SkillName,
|
||||
Page: req.Page,
|
||||
PageSize: req.PageSize,
|
||||
SortedBy: req.SortedBy,
|
||||
Ascending: req.Ascending,
|
||||
}
|
||||
|
||||
if req.Page == 0 {
|
||||
filter.Page = 1
|
||||
}
|
||||
if req.PageSize == 0 {
|
||||
filter.PageSize = 10
|
||||
}
|
||||
|
||||
if req.RoleID != nil {
|
||||
filter.RoleID = *req.RoleID
|
||||
}
|
||||
|
||||
profiles, total, err := s.profileRepo.FindAll(ctx, filter)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
response := &dto.ListProfilesResponse{
|
||||
Profiles: make([]dto.ProfileResponse, len(profiles)),
|
||||
Total: total,
|
||||
Page: filter.Page,
|
||||
PageSize: filter.PageSize,
|
||||
}
|
||||
|
||||
for i, p := range profiles {
|
||||
response.Profiles[i] = *s.toProfileResponse(p)
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (s *service) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
// Get profile first to ensure it exists
|
||||
profileData, err := s.profileRepo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return profile.ErrProfileNotFound
|
||||
}
|
||||
|
||||
return s.profileRepo.Delete(ctx, profileData)
|
||||
}
|
||||
|
||||
func (s *service) toProfileResponse(p *profile.Profile) *dto.ProfileResponse {
|
||||
resp := &dto.ProfileResponse{
|
||||
ID: p.ID,
|
||||
Handle: p.Handle,
|
||||
PageSectionOrder: p.PageSectionOrder,
|
||||
Hero: dto.HeroDTO{
|
||||
FirstName: p.Hero.FirstName,
|
||||
LastName: p.Hero.LastName,
|
||||
Company: p.Hero.Company,
|
||||
ShortDescription: p.Hero.ShortDescription,
|
||||
ResumeLink: p.Hero.ResumeLink,
|
||||
CTAEnabled: p.Hero.CTAEnabled,
|
||||
Avatar: p.Hero.Avatar,
|
||||
},
|
||||
About: dto.AboutDTO{
|
||||
ProfilePicture: p.About.ProfilePicture,
|
||||
About: p.About.About,
|
||||
},
|
||||
Contact: dto.ContactDTO{
|
||||
Email: p.Contact.Email,
|
||||
Phone: p.Contact.Phone,
|
||||
},
|
||||
PageSetting: dto.PageSettingDTO{
|
||||
VisibilityLevel: p.PageSetting.VisibilityLevel,
|
||||
},
|
||||
}
|
||||
|
||||
if p.Hero.Role != nil {
|
||||
resp.Hero.RoleID = &p.Hero.Role.ID
|
||||
}
|
||||
|
||||
for _, skill := range p.Skills {
|
||||
resp.Skills = append(resp.Skills, dto.SkillDTO{
|
||||
SkillName: skill.SkillName,
|
||||
Level: skill.Level,
|
||||
})
|
||||
}
|
||||
|
||||
for _, achievement := range p.About.Achievements {
|
||||
resp.About.Achievements = append(resp.About.Achievements, dto.AchievementDTO{
|
||||
Title: achievement.Title,
|
||||
Value: achievement.Value,
|
||||
Enabled: achievement.Enabled,
|
||||
})
|
||||
}
|
||||
|
||||
for _, socialLink := range p.Contact.SocialLinks {
|
||||
resp.Contact.SocialLinks = append(resp.Contact.SocialLinks, dto.SocialLinkDTO{
|
||||
LinkType: socialLink.LinkType,
|
||||
Link: socialLink.Link,
|
||||
})
|
||||
}
|
||||
|
||||
return resp
|
||||
}
|
||||
121
internal/application/profilerole/service.go
Normal file
121
internal/application/profilerole/service.go
Normal file
@@ -0,0 +1,121 @@
|
||||
package profilerole
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.uber.org/fx"
|
||||
|
||||
domainProfile "base/internal/domain/profile"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
var ErrNotFound = domainProfile.ErrRoleNotFound
|
||||
|
||||
type Service interface {
|
||||
List(ctx context.Context) ([]dto.ProfileRole, error)
|
||||
ListWithLimit(ctx context.Context, limit, offset int) ([]dto.ProfileRole, error)
|
||||
Create(ctx context.Context, req dto.CreateProfileRoleRequest) (*dto.ProfileRole, error)
|
||||
GetByID(ctx context.Context, id uuid.UUID) (*dto.ProfileRole, error)
|
||||
Update(ctx context.Context, req dto.UpdateProfileRoleRequest) (*dto.ProfileRole, error)
|
||||
Delete(ctx context.Context, id uuid.UUID) error
|
||||
}
|
||||
|
||||
type service struct {
|
||||
logger zerolog.Logger
|
||||
repo domainProfile.RoleRepository
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
Logger zerolog.Logger
|
||||
Repo domainProfile.RoleRepository
|
||||
|
||||
fx.In
|
||||
}
|
||||
|
||||
func New(param Param) Service {
|
||||
return &service{
|
||||
logger: param.Logger,
|
||||
repo: param.Repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) List(ctx context.Context) ([]dto.ProfileRole, error) {
|
||||
roles, err := s.repo.FindAll(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toDTOs(roles), nil
|
||||
}
|
||||
|
||||
func (s *service) ListWithLimit(ctx context.Context, limit, offset int) ([]dto.ProfileRole, error) {
|
||||
roles, err := s.repo.List(ctx, limit, offset)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toDTOs(roles), nil
|
||||
}
|
||||
|
||||
func (s *service) Create(ctx context.Context, req dto.CreateProfileRoleRequest) (*dto.ProfileRole, error) {
|
||||
role := &domainProfile.Role{
|
||||
ID: uuid.New(),
|
||||
Title: req.Title,
|
||||
}
|
||||
if err := s.repo.Create(ctx, role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toDTO(role), nil
|
||||
}
|
||||
|
||||
func (s *service) GetByID(ctx context.Context, id uuid.UUID) (*dto.ProfileRole, error) {
|
||||
role, err := s.repo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, ErrNotFound) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return toDTO(role), nil
|
||||
}
|
||||
|
||||
func (s *service) Update(ctx context.Context, req dto.UpdateProfileRoleRequest) (*dto.ProfileRole, error) {
|
||||
id, err := uuid.Parse(req.ID)
|
||||
if err != nil {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
role, err := s.repo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
role.Title = req.Title
|
||||
|
||||
if err := s.repo.Update(ctx, role); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return toDTO(role), nil
|
||||
}
|
||||
|
||||
func (s *service) Delete(ctx context.Context, id uuid.UUID) error {
|
||||
role, err := s.repo.FindByID(ctx, id)
|
||||
if err != nil {
|
||||
return ErrNotFound
|
||||
}
|
||||
return s.repo.Delete(ctx, role.ID)
|
||||
}
|
||||
|
||||
func toDTO(r *domainProfile.Role) *dto.ProfileRole {
|
||||
return &dto.ProfileRole{
|
||||
Id: r.ID.String(),
|
||||
Title: r.Title,
|
||||
}
|
||||
}
|
||||
|
||||
func toDTOs(roles []*domainProfile.Role) []dto.ProfileRole {
|
||||
out := make([]dto.ProfileRole, len(roles))
|
||||
for i, r := range roles {
|
||||
out[i] = *toDTO(r)
|
||||
}
|
||||
return out
|
||||
}
|
||||
46
internal/application/skill/service.go
Normal file
46
internal/application/skill/service.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package skill
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.uber.org/fx"
|
||||
|
||||
domainSkill "base/internal/domain/skill"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
List(ctx context.Context) ([]dto.Skill, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
logger zerolog.Logger
|
||||
repo domainSkill.Repository
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
Logger zerolog.Logger
|
||||
Repo domainSkill.Repository
|
||||
|
||||
fx.In
|
||||
}
|
||||
|
||||
func New(param Param) Service {
|
||||
return &service{
|
||||
logger: param.Logger,
|
||||
repo: param.Repo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) List(ctx context.Context) ([]dto.Skill, error) {
|
||||
skills, err := s.repo.FindAll(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := make([]dto.Skill, len(skills))
|
||||
for i, sk := range skills {
|
||||
out[i] = dto.Skill{ID: sk.ID.String(), Name: sk.Name}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
426
internal/application/specialist/service.go
Normal file
426
internal/application/specialist/service.go
Normal file
@@ -0,0 +1,426 @@
|
||||
package specialist
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"go.uber.org/fx"
|
||||
"golang.org/x/sync/errgroup"
|
||||
|
||||
appProfile "base/internal/application/profile"
|
||||
domainAsset "base/internal/domain/asset"
|
||||
domainProfile "base/internal/domain/profile"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
type Service interface {
|
||||
Overview(ctx context.Context, userID uuid.UUID) (*dto.SpecialistOverviewFetchedResponse, error)
|
||||
UpdateHero(ctx context.Context, userID uuid.UUID, req dto.HeroDTO) error
|
||||
UpdateContact(ctx context.Context, userID uuid.UUID, req dto.ContactDTO) error
|
||||
UpdateSkills(ctx context.Context, userID uuid.UUID, req dto.SkillsUpdateRequest) error
|
||||
GetPageSections(ctx context.Context, userID uuid.UUID) (*dto.PageSectionsResponse, error)
|
||||
GetProfile(ctx context.Context, userID uuid.UUID) (*dto.ProfileResponse, error)
|
||||
}
|
||||
|
||||
type service struct {
|
||||
logger zerolog.Logger
|
||||
profileRepo domainProfile.Repository
|
||||
assetRepo domainAsset.AssetRepository
|
||||
}
|
||||
|
||||
// Param holds dependencies for the specialist overview service.
|
||||
type Param struct {
|
||||
Logger zerolog.Logger
|
||||
ProfileRepo domainProfile.Repository
|
||||
AssetRepo domainAsset.AssetRepository
|
||||
|
||||
fx.In
|
||||
}
|
||||
|
||||
func New(param Param) Service {
|
||||
return &service{
|
||||
logger: param.Logger,
|
||||
profileRepo: param.ProfileRepo,
|
||||
assetRepo: param.AssetRepo,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *service) Overview(ctx context.Context, userID uuid.UUID) (*dto.SpecialistOverviewFetchedResponse, error) {
|
||||
resp := &struct {
|
||||
profile *domainProfile.Profile
|
||||
assets []*domainAsset.Asset
|
||||
recentlyJoined []*domainProfile.Profile
|
||||
totalProfiles int
|
||||
totalAssets int
|
||||
}{}
|
||||
|
||||
g, gCtx := errgroup.WithContext(ctx)
|
||||
|
||||
// 1. Profile by OwnerID (includes Skills) + Assets by ProfileID
|
||||
g.Go(func() error {
|
||||
profile, err := s.profileRepo.FindByUserID(gCtx, userID)
|
||||
if err != nil {
|
||||
return domainProfile.ErrProfileNotFound
|
||||
}
|
||||
resp.profile = profile
|
||||
|
||||
assets, err := s.assetRepo.FindByProfileID(gCtx, profile.ID)
|
||||
if err != nil {
|
||||
assets = []*domainAsset.Asset{}
|
||||
}
|
||||
resp.assets = assets
|
||||
return nil
|
||||
})
|
||||
|
||||
// 2. Latest 6 profiles + total profiles count (FindAll returns both)
|
||||
g.Go(func() error {
|
||||
profiles, total, err := s.profileRepo.FindAll(gCtx, domainProfile.Filter{
|
||||
Page: 1,
|
||||
PageSize: 6,
|
||||
SortedBy: "created_at",
|
||||
Ascending: false,
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.recentlyJoined = profiles
|
||||
resp.totalProfiles = total
|
||||
return nil
|
||||
})
|
||||
|
||||
// 3. Total assets count
|
||||
g.Go(func() error {
|
||||
count, err := s.assetRepo.Count(gCtx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp.totalAssets = count
|
||||
return nil
|
||||
})
|
||||
|
||||
if err := g.Wait(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
assets := s.toOverviewAssets(resp.assets, userID)
|
||||
flatProfiles := ToFlatProfiles(resp.recentlyJoined)
|
||||
|
||||
profileResp := appProfile.DomainProfileToProfileResponse(resp.profile)
|
||||
var skills []dto.SkillDTO
|
||||
if profileResp != nil {
|
||||
skills = profileResp.Skills
|
||||
}
|
||||
|
||||
tasks := s.computeTasks(resp.profile, resp.assets)
|
||||
completionPercent := s.computeCompletionPercent(tasks)
|
||||
|
||||
return &dto.SpecialistOverviewFetchedResponse{
|
||||
Message: "",
|
||||
Data: dto.SpecialistOverviewFetchedDataDTO{
|
||||
Assets: assets,
|
||||
RecentlyJoined: flatProfiles,
|
||||
Analytics: dto.AnalyticsDTO{TotalAssets: resp.totalAssets, TotalProfiles: resp.totalProfiles},
|
||||
Profile: profileResp,
|
||||
Skills: skills,
|
||||
CompletionPercent: completionPercent,
|
||||
Tasks: tasks,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// computeTasks derives task flags from profile and assets. true = needs action.
|
||||
func (s *service) computeTasks(p *domainProfile.Profile, assets []*domainAsset.Asset) dto.TasksDTO {
|
||||
tasks := dto.TasksDTO{}
|
||||
if p == nil {
|
||||
return tasks
|
||||
}
|
||||
// profile_action: Hero section (firstName, lastName, shortDescription) incomplete
|
||||
tasks.ProfileAction = strings.TrimSpace(p.Hero.FirstName) == "" ||
|
||||
strings.TrimSpace(p.Hero.LastName) == "" ||
|
||||
strings.TrimSpace(p.Hero.ShortDescription) == ""
|
||||
|
||||
// about_action: About section incomplete
|
||||
tasks.AboutAction = strings.TrimSpace(p.About.ProfilePicture) == "" ||
|
||||
strings.TrimSpace(p.About.About) == ""
|
||||
|
||||
// publish_action: not public
|
||||
tasks.PublishAction = p.PageSetting.VisibilityLevel != "public"
|
||||
|
||||
// works_action: no assets
|
||||
tasks.WorksAction = len(assets) == 0
|
||||
|
||||
// skills_action: no skills
|
||||
tasks.SkillsAction = len(p.Skills) == 0
|
||||
|
||||
// social_action: no social links
|
||||
tasks.SocialAction = len(p.Contact.SocialLinks) == 0
|
||||
|
||||
return tasks
|
||||
}
|
||||
|
||||
// computeCompletionPercent: 6 sections, each complete = !action. Percent = (6 - actionsNeeded) / 6 * 100
|
||||
func (s *service) computeCompletionPercent(tasks dto.TasksDTO) int {
|
||||
complete := 0
|
||||
if !tasks.ProfileAction {
|
||||
complete++
|
||||
}
|
||||
if !tasks.AboutAction {
|
||||
complete++
|
||||
}
|
||||
if !tasks.PublishAction {
|
||||
complete++
|
||||
}
|
||||
if !tasks.WorksAction {
|
||||
complete++
|
||||
}
|
||||
if !tasks.SkillsAction {
|
||||
complete++
|
||||
}
|
||||
if !tasks.SocialAction {
|
||||
complete++
|
||||
}
|
||||
if complete == 0 {
|
||||
return 0
|
||||
}
|
||||
return (complete * 100) / 6
|
||||
}
|
||||
|
||||
func (s *service) UpdateHero(ctx context.Context, userID uuid.UUID, req dto.HeroDTO) error {
|
||||
p, err := s.profileRepo.FindByUserID(ctx, userID)
|
||||
if err != nil || p == nil {
|
||||
return domainProfile.ErrProfileNotFound
|
||||
}
|
||||
|
||||
p.Hero.FirstName = req.FirstName
|
||||
p.Hero.LastName = req.LastName
|
||||
p.Hero.Company = req.Company
|
||||
p.Hero.ShortDescription = req.ShortDescription
|
||||
p.Hero.ResumeLink = req.ResumeLink
|
||||
p.Hero.CTAEnabled = req.CTAEnabled
|
||||
p.Hero.Avatar = req.Avatar
|
||||
|
||||
if req.RoleID != nil {
|
||||
if p.Hero.Role == nil {
|
||||
p.Hero.Role = &domainProfile.Role{ID: *req.RoleID, Level: req.RoleLevel}
|
||||
} else {
|
||||
p.Hero.Role.ID = *req.RoleID
|
||||
p.Hero.Role.Level = req.RoleLevel
|
||||
}
|
||||
} else if req.RoleLevel != "" {
|
||||
if p.Hero.Role == nil {
|
||||
p.Hero.Role = &domainProfile.Role{Level: req.RoleLevel}
|
||||
} else {
|
||||
p.Hero.Role.Level = req.RoleLevel
|
||||
}
|
||||
}
|
||||
|
||||
p.UpdatedAt = time.Now()
|
||||
return s.profileRepo.Update(ctx, p)
|
||||
}
|
||||
|
||||
func (s *service) UpdateContact(ctx context.Context, userID uuid.UUID, req dto.ContactDTO) error {
|
||||
p, err := s.profileRepo.FindByUserID(ctx, userID)
|
||||
if err != nil || p == nil {
|
||||
return domainProfile.ErrProfileNotFound
|
||||
}
|
||||
p.Contact.Email = req.Email
|
||||
p.Contact.Phone = req.Phone
|
||||
p.Contact.SocialLinks = make([]domainProfile.SocialLink, len(req.SocialLinks))
|
||||
for i, sl := range req.SocialLinks {
|
||||
p.Contact.SocialLinks[i] = domainProfile.SocialLink{LinkType: sl.LinkType, Link: sl.Link}
|
||||
}
|
||||
p.UpdatedAt = time.Now()
|
||||
return s.profileRepo.Update(ctx, p)
|
||||
}
|
||||
|
||||
func (s *service) UpdateSkills(ctx context.Context, userID uuid.UUID, req dto.SkillsUpdateRequest) error {
|
||||
p, err := s.profileRepo.FindByUserID(ctx, userID)
|
||||
if err != nil || p == nil {
|
||||
return domainProfile.ErrProfileNotFound
|
||||
}
|
||||
p.Skills = make([]domainProfile.Skill, len(req.Skills))
|
||||
for i, s := range req.Skills {
|
||||
p.Skills[i] = domainProfile.Skill{SkillName: s.SkillName, Level: s.Level}
|
||||
}
|
||||
p.UpdatedAt = time.Now()
|
||||
return s.profileRepo.Update(ctx, p)
|
||||
}
|
||||
|
||||
func (s *service) GetPageSections(ctx context.Context, userID uuid.UUID) (*dto.PageSectionsResponse, error) {
|
||||
p, err := s.profileRepo.FindByUserID(ctx, userID)
|
||||
if err != nil || p == nil {
|
||||
return nil, domainProfile.ErrProfileNotFound
|
||||
}
|
||||
resp := appProfile.DomainProfileToProfileResponse(p)
|
||||
if resp == nil {
|
||||
return nil, domainProfile.ErrProfileNotFound
|
||||
}
|
||||
return &dto.PageSectionsResponse{
|
||||
Hero: resp.Hero,
|
||||
Contact: resp.Contact,
|
||||
Skills: resp.Skills,
|
||||
PageSectionOrder: resp.PageSectionOrder,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *service) GetProfile(ctx context.Context, userID uuid.UUID) (*dto.ProfileResponse, error) {
|
||||
p, err := s.profileRepo.FindByUserID(ctx, userID)
|
||||
if err != nil || p == nil {
|
||||
return nil, domainProfile.ErrProfileNotFound
|
||||
}
|
||||
return appProfile.DomainProfileToProfileResponse(p), nil
|
||||
}
|
||||
|
||||
func (s *service) toOverviewAssets(assets []*domainAsset.Asset, ownerID uuid.UUID) []dto.OverviewAssetDTO {
|
||||
out := make([]dto.OverviewAssetDTO, len(assets))
|
||||
for i, a := range assets {
|
||||
out[i] = s.toOverviewAsset(a, ownerID)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *service) toOverviewAsset(a *domainAsset.Asset, ownerID uuid.UUID) dto.OverviewAssetDTO {
|
||||
price := 0
|
||||
coverImage := ""
|
||||
for _, art := range a.AssetArtifacts {
|
||||
if strings.Contains(strings.ToLower(art.Type), "image") {
|
||||
coverImage = art.DownloadURL
|
||||
break
|
||||
}
|
||||
}
|
||||
if len(a.AssetArtifacts) > 0 {
|
||||
price = a.AssetArtifacts[0].Price
|
||||
}
|
||||
|
||||
cat := (*dto.CategoryDTO)(nil)
|
||||
if a.AssetCategory.ID != uuid.Nil {
|
||||
cat = &dto.CategoryDTO{
|
||||
ID: a.AssetCategory.ID,
|
||||
Name: a.AssetCategory.Name,
|
||||
Icon: a.AssetCategory.Icon,
|
||||
Color: a.AssetCategory.Color,
|
||||
CardType: a.AssetCategory.CardType,
|
||||
Featured: a.AssetCategory.Featured,
|
||||
Description: a.AssetCategory.Description,
|
||||
}
|
||||
}
|
||||
|
||||
return dto.OverviewAssetDTO{
|
||||
ID: a.ID.String(),
|
||||
Title: a.Title,
|
||||
Description: a.Description,
|
||||
Content: a.Description,
|
||||
AssetCategoryID: a.AssetCategoryID.String(),
|
||||
AssetCategory: cat,
|
||||
CoverImage: coverImage,
|
||||
Link: a.Link,
|
||||
OwnerID: ownerID.String(),
|
||||
ProfileID: a.ProfileID.String(),
|
||||
Profile: nil,
|
||||
Price: price,
|
||||
Currency: "USD",
|
||||
Status: assetStatusToString(a.Status),
|
||||
Rating: 0,
|
||||
CreatedAt: a.CreatedAt,
|
||||
UpdatedAt: a.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func assetStatusToString(st domainAsset.Status) string {
|
||||
switch st {
|
||||
case domainAsset.StatusPublished:
|
||||
return "published"
|
||||
case domainAsset.StatusDisabled:
|
||||
return "disabled"
|
||||
case domainAsset.StatusPending:
|
||||
return "pending"
|
||||
case domainAsset.StatusDeleted:
|
||||
return "deleted"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// ToFlatProfiles converts domain profiles to flat DTOs.
|
||||
func ToFlatProfiles(profiles []*domainProfile.Profile) []dto.FlatProfileDTO {
|
||||
out := make([]dto.FlatProfileDTO, len(profiles))
|
||||
for i, p := range profiles {
|
||||
out[i] = ToFlatProfile(p)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// ToFlatProfile converts a single profile to flat DTO.
|
||||
func ToFlatProfile(p *domainProfile.Profile) dto.FlatProfileDTO {
|
||||
roleID := ""
|
||||
roleName := ""
|
||||
if p.Hero.Role != nil {
|
||||
roleID = p.Hero.Role.ID.String()
|
||||
if p.Hero.Role.Title != "" {
|
||||
roleName = p.Hero.Role.Title
|
||||
}
|
||||
}
|
||||
|
||||
achievements := make(map[string]dto.AchievementItemDTO)
|
||||
for _, a := range p.About.Achievements {
|
||||
key := strings.ToLower(strings.ReplaceAll(a.Title, " ", ""))
|
||||
if key == "" {
|
||||
key = a.Title
|
||||
}
|
||||
achievements[key] = dto.AchievementItemDTO{Value: a.Value, Enabled: a.Enabled}
|
||||
}
|
||||
if len(achievements) == 0 {
|
||||
achievements = map[string]dto.AchievementItemDTO{
|
||||
"happyClient": {Value: "", Enabled: true},
|
||||
"yearExperience": {Value: "", Enabled: true},
|
||||
"projectCompeleted": {Value: "", Enabled: true},
|
||||
}
|
||||
}
|
||||
|
||||
var socialLinks []dto.SocialLinkDTO
|
||||
for _, sl := range p.Contact.SocialLinks {
|
||||
socialLinks = append(socialLinks, dto.SocialLinkDTO{LinkType: sl.LinkType, Link: sl.Link})
|
||||
}
|
||||
|
||||
displayName := strings.TrimSpace(p.Hero.FirstName + " " + p.Hero.LastName)
|
||||
if displayName == "" {
|
||||
displayName = p.Handle
|
||||
}
|
||||
|
||||
status := "published"
|
||||
if p.PageSetting.VisibilityLevel != "public" {
|
||||
status = "draft"
|
||||
}
|
||||
|
||||
return dto.FlatProfileDTO{
|
||||
ID: p.ID.String(),
|
||||
ProfileHandle: p.Handle,
|
||||
Status: status,
|
||||
BackgroundImage: "",
|
||||
ProfilePicture: p.About.ProfilePicture,
|
||||
FirstName: p.Hero.FirstName,
|
||||
LastName: p.Hero.LastName,
|
||||
DisplayName: displayName,
|
||||
RoleID: roleID,
|
||||
Role: dto.RoleDTO{ID: roleID, Name: roleName},
|
||||
CurrentCompany: p.Hero.Company,
|
||||
ShortDescription: p.Hero.ShortDescription,
|
||||
CTAEnabled: p.Hero.CTAEnabled,
|
||||
CTAAction: "",
|
||||
ResumeLink: p.Hero.ResumeLink,
|
||||
About: p.About.About,
|
||||
ContactEmail: p.Contact.Email,
|
||||
Achievements: achievements,
|
||||
ContactPhone: p.Contact.Phone,
|
||||
Country: "",
|
||||
CustomRoles: "",
|
||||
RoleLevel: p.Hero.Role.Level,
|
||||
SocialLinks: socialLinks,
|
||||
CreatedAt: p.CreatedAt,
|
||||
UpdatedAt: p.UpdatedAt,
|
||||
HandleUpdatedAt: time.Time{},
|
||||
}
|
||||
}
|
||||
176
internal/application/specialist/service_test.go
Normal file
176
internal/application/specialist/service_test.go
Normal file
@@ -0,0 +1,176 @@
|
||||
package specialist_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
appMock "base/internal/application/mock"
|
||||
"base/internal/application/specialist"
|
||||
domainAsset "base/internal/domain/asset"
|
||||
domainProfile "base/internal/domain/profile"
|
||||
)
|
||||
|
||||
func TestSpecialistService_Overview(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
logger := zerolog.Nop()
|
||||
userID := uuid.New()
|
||||
profileID := uuid.New()
|
||||
|
||||
t.Run("success - returns overview with profile, assets, tasks", func(t *testing.T) {
|
||||
assetRepo := new(appMock.MockAssetRepository)
|
||||
profileRepo := new(appMock.MockProfileRepository)
|
||||
|
||||
profile := &domainProfile.Profile{
|
||||
ID: profileID,
|
||||
Handle: "specialist-user",
|
||||
UserID: &userID,
|
||||
Hero: domainProfile.Hero{
|
||||
FirstName: "Jane",
|
||||
LastName: "Doe",
|
||||
ShortDescription: "ML Engineer",
|
||||
},
|
||||
About: domainProfile.About{
|
||||
ProfilePicture: "avatar.jpg",
|
||||
About: "About me",
|
||||
},
|
||||
Skills: []domainProfile.Skill{
|
||||
{SkillName: "Go", Level: "expert"},
|
||||
},
|
||||
Contact: domainProfile.Contact{
|
||||
Email: "jane@example.com",
|
||||
SocialLinks: []domainProfile.SocialLink{{LinkType: "github", Link: "https://github.com/jane"}},
|
||||
},
|
||||
PageSetting: domainProfile.PageSetting{VisibilityLevel: "public"},
|
||||
}
|
||||
|
||||
asset := &domainAsset.Asset{
|
||||
ID: uuid.New(),
|
||||
ProfileID: profileID,
|
||||
Status: domainAsset.StatusPublished,
|
||||
AssetCategoryID: uuid.New(),
|
||||
Title: "My Project",
|
||||
Description: "A cool project",
|
||||
AssetArtifacts: []domainAsset.Artifact{{Type: "image", DownloadURL: "cover.png", Price: 0}},
|
||||
}
|
||||
|
||||
otherProfile := &domainProfile.Profile{
|
||||
ID: uuid.New(),
|
||||
Handle: "other-user",
|
||||
Hero: domainProfile.Hero{FirstName: "Other", LastName: "User"},
|
||||
PageSetting: domainProfile.PageSetting{VisibilityLevel: "public"},
|
||||
}
|
||||
|
||||
profileRepo.On("FindByUserID", ctx, userID).Return(profile, nil)
|
||||
assetRepo.On("FindByProfileID", ctx, profileID).Return([]*domainAsset.Asset{asset}, nil)
|
||||
profileRepo.On("FindAll", ctx, mock.MatchedBy(func(arg interface{}) bool {
|
||||
f, ok := arg.(domainProfile.Filter)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return f.Page == 1 && f.PageSize == 6 && f.SortedBy == "created_at" && !f.Ascending
|
||||
})).Return([]*domainProfile.Profile{otherProfile}, 26, nil)
|
||||
assetRepo.On("Count", ctx).Return(42, nil)
|
||||
|
||||
svc := specialist.New(specialist.Param{
|
||||
Logger: logger,
|
||||
ProfileRepo: profileRepo,
|
||||
AssetRepo: assetRepo,
|
||||
})
|
||||
|
||||
resp, err := svc.Overview(ctx, userID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
assert.Equal(t, "", resp.Message)
|
||||
assert.Len(t, resp.Data.Assets, 1)
|
||||
assert.Equal(t, "My Project", resp.Data.Assets[0].Title)
|
||||
|
||||
assert.Len(t, resp.Data.RecentlyJoined, 1)
|
||||
assert.Equal(t, "other-user", resp.Data.RecentlyJoined[0].ProfileHandle)
|
||||
|
||||
assert.Equal(t, 42, resp.Data.Analytics.TotalAssets)
|
||||
assert.Equal(t, 26, resp.Data.Analytics.TotalProfiles)
|
||||
|
||||
require.NotNil(t, resp.Data.Profile)
|
||||
assert.Equal(t, "specialist-user", resp.Data.Profile.Handle)
|
||||
assert.Len(t, resp.Data.Skills, 1)
|
||||
assert.Equal(t, "Go", resp.Data.Skills[0].SkillName)
|
||||
|
||||
// All sections complete -> 100% or high completion
|
||||
assert.False(t, resp.Data.Tasks.ProfileAction)
|
||||
assert.False(t, resp.Data.Tasks.AboutAction)
|
||||
assert.False(t, resp.Data.Tasks.PublishAction)
|
||||
assert.False(t, resp.Data.Tasks.WorksAction)
|
||||
assert.False(t, resp.Data.Tasks.SkillsAction)
|
||||
assert.False(t, resp.Data.Tasks.SocialAction)
|
||||
assert.Equal(t, 100, resp.Data.CompletionPercent)
|
||||
|
||||
assetRepo.AssertExpectations(t)
|
||||
profileRepo.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("profile not found returns ErrProfileNotFound", func(t *testing.T) {
|
||||
assetRepo := new(appMock.MockAssetRepository)
|
||||
profileRepo := new(appMock.MockProfileRepository)
|
||||
|
||||
profileRepo.On("FindByUserID", ctx, userID).Return(nil, domainProfile.ErrProfileNotFound)
|
||||
|
||||
svc := specialist.New(specialist.Param{
|
||||
Logger: logger,
|
||||
ProfileRepo: profileRepo,
|
||||
AssetRepo: assetRepo,
|
||||
})
|
||||
|
||||
resp, err := svc.Overview(ctx, userID)
|
||||
assert.Error(t, err)
|
||||
assert.True(t, errors.Is(err, domainProfile.ErrProfileNotFound))
|
||||
assert.Nil(t, resp)
|
||||
|
||||
profileRepo.AssertExpectations(t)
|
||||
})
|
||||
|
||||
t.Run("incomplete profile computes tasks and completion percent", func(t *testing.T) {
|
||||
assetRepo := new(appMock.MockAssetRepository)
|
||||
profileRepo := new(appMock.MockProfileRepository)
|
||||
|
||||
profile := &domainProfile.Profile{
|
||||
ID: profileID,
|
||||
Handle: "incomplete",
|
||||
UserID: &userID,
|
||||
Hero: domainProfile.Hero{FirstName: "A"}, // missing LastName, ShortDescription
|
||||
About: domainProfile.About{}, // missing picture, about
|
||||
Skills: []domainProfile.Skill{},
|
||||
PageSetting: domainProfile.PageSetting{VisibilityLevel: "private"},
|
||||
}
|
||||
|
||||
profileRepo.On("FindByUserID", ctx, userID).Return(profile, nil)
|
||||
assetRepo.On("FindByProfileID", ctx, profileID).Return([]*domainAsset.Asset{}, nil)
|
||||
profileRepo.On("FindAll", ctx, mock.Anything).Return([]*domainProfile.Profile{}, 0, nil)
|
||||
assetRepo.On("Count", ctx).Return(0, nil)
|
||||
|
||||
svc := specialist.New(specialist.Param{
|
||||
Logger: logger,
|
||||
ProfileRepo: profileRepo,
|
||||
AssetRepo: assetRepo,
|
||||
})
|
||||
|
||||
resp, err := svc.Overview(ctx, userID)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, resp)
|
||||
|
||||
assert.True(t, resp.Data.Tasks.ProfileAction)
|
||||
assert.True(t, resp.Data.Tasks.AboutAction)
|
||||
assert.True(t, resp.Data.Tasks.PublishAction)
|
||||
assert.True(t, resp.Data.Tasks.WorksAction)
|
||||
assert.True(t, resp.Data.Tasks.SkillsAction)
|
||||
assert.True(t, resp.Data.Tasks.SocialAction)
|
||||
assert.Equal(t, 0, resp.Data.CompletionPercent)
|
||||
})
|
||||
}
|
||||
76
internal/delivery/http/backoffice/back_office.go
Normal file
76
internal/delivery/http/backoffice/back_office.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package backoffice
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"base/config"
|
||||
appProfileRole "base/internal/application/profilerole"
|
||||
"base/internal/server/middleware"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
logger zerolog.Logger
|
||||
middleware middleware.Middleware
|
||||
config *config.AppConfig
|
||||
e *gin.Engine
|
||||
profileRoleService appProfileRole.Service
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
Logger zerolog.Logger
|
||||
Engine *gin.Engine
|
||||
Middleware middleware.Middleware
|
||||
Config *config.AppConfig
|
||||
ProfileRoleService appProfileRole.Service
|
||||
|
||||
fx.In
|
||||
}
|
||||
|
||||
func New(lc fx.Lifecycle, param Param) *Controller {
|
||||
c := &Controller{
|
||||
logger: param.Logger,
|
||||
middleware: param.Middleware,
|
||||
config: param.Config,
|
||||
e: param.Engine,
|
||||
profileRoleService: param.ProfileRoleService,
|
||||
}
|
||||
|
||||
lc.Append(
|
||||
fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
c.SetupRouter()
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
// getMaxFileSize returns the maximum file size in bytes from configuration
|
||||
func (ctl *Controller) getMaxFileSize() int64 {
|
||||
return ctl.config.Server.GetMaxFileSizeBytes()
|
||||
}
|
||||
|
||||
func (ctl *Controller) SetupRouter() {
|
||||
router := ctl.e.Group("/api/v1")
|
||||
ctl.registerRoutes(router)
|
||||
}
|
||||
|
||||
func (ctl *Controller) registerRoutes(router *gin.RouterGroup) {
|
||||
backofficeRouter := router.Group("/backoffice")
|
||||
profileRoleRouter := backofficeRouter.Group("/profile-roles")
|
||||
profileRoleRouter.GET("", ctl.ListProfileRoles)
|
||||
profileRoleRouter.GET("/:id", ctl.GetProfileRole)
|
||||
protected := profileRoleRouter.Use(ctl.middleware.AuthShield())
|
||||
protected.POST("", ctl.CreateProfileRole)
|
||||
protected.PUT("/:id", ctl.UpdateProfileRole)
|
||||
protected.DELETE("/:id", ctl.DeleteProfileRole)
|
||||
}
|
||||
7
internal/delivery/http/backoffice/permissions.go
Normal file
7
internal/delivery/http/backoffice/permissions.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package backoffice
|
||||
|
||||
var HttpRoutePermissionMap = map[string]string{}
|
||||
|
||||
var GrpcRoutePermissionMap = map[string]string{}
|
||||
|
||||
var ExcludedGrpcRoutePermissionMap = map[string]string{}
|
||||
216
internal/delivery/http/backoffice/profilerole.go
Normal file
216
internal/delivery/http/backoffice/profilerole.go
Normal file
@@ -0,0 +1,216 @@
|
||||
package backoffice
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
appProfileRole "base/internal/application/profilerole"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
// ListProfileRoles returns the list of profile roles.
|
||||
// @Summary list profile roles
|
||||
// @Description returns all profile roles (id, title, status)
|
||||
// @Tags BackOffice
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} dto.ProfileRole "list of profile roles"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/backoffice/profile-roles [get]
|
||||
func (ctl *Controller) ListProfileRoles(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "backoffice").
|
||||
Str("router", "profile-roles").
|
||||
Str("handler", "ListProfileRoles").
|
||||
Logger()
|
||||
|
||||
roles, err := ctl.profileRoleService.List(c.Request.Context())
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to list profile roles")
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, roles)
|
||||
}
|
||||
|
||||
// CreateProfileRole creates a new profile role.
|
||||
// @Summary create profile role
|
||||
// @Description create a new profile role
|
||||
// @Tags BackOffice
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body dto.CreateProfileRoleRequest true "create request"
|
||||
// @Success 201 {object} dto.ProfileRole "created profile role"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 401 {object} dto.ErrorResponse "unauthorized"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/backoffice/profile-roles [post]
|
||||
func (ctl *Controller) CreateProfileRole(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "backoffice").
|
||||
Str("router", "profile-roles").
|
||||
Str("handler", "CreateProfileRole").
|
||||
Logger()
|
||||
|
||||
var req dto.CreateProfileRoleRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
role, err := ctl.profileRoleService.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to create profile role")
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.Created(role)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// GetProfileRole returns a profile role by ID.
|
||||
// @Summary get profile role by ID
|
||||
// @Description get profile role by ID
|
||||
// @Tags BackOffice
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "profile role ID"
|
||||
// @Success 200 {object} dto.ProfileRole "profile role"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 404 {object} dto.ErrorResponse "not found"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/backoffice/profile-roles/{id} [get]
|
||||
func (ctl *Controller) GetProfileRole(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "backoffice").
|
||||
Str("router", "profile-roles").
|
||||
Str("handler", "GetProfileRole").
|
||||
Logger()
|
||||
|
||||
var req dto.GetProfileRoleRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(req.ID)
|
||||
if err != nil {
|
||||
r := dto.BadRequest().WithMessage("invalid profile role ID")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
role, err := ctl.profileRoleService.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
if errors.Is(err, appProfileRole.ErrNotFound) {
|
||||
r := dto.NotFound().WithMessage("profile role not found")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
lg.Error().Err(err).Msg("failed to get profile role")
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(role)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// UpdateProfileRole updates a profile role.
|
||||
// @Summary update profile role
|
||||
// @Description update an existing profile role
|
||||
// @Tags BackOffice
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "profile role ID"
|
||||
// @Param request body dto.UpdateProfileRoleRequest true "update request"
|
||||
// @Success 200 {object} dto.ProfileRole "updated profile role"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 401 {object} dto.ErrorResponse "unauthorized"
|
||||
// @Failure 404 {object} dto.ErrorResponse "not found"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/backoffice/profile-roles/{id} [put]
|
||||
func (ctl *Controller) UpdateProfileRole(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "backoffice").
|
||||
Str("router", "profile-roles").
|
||||
Str("handler", "UpdateProfileRole").
|
||||
Logger()
|
||||
|
||||
var req dto.UpdateProfileRoleRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
role, err := ctl.profileRoleService.Update(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
if errors.Is(err, appProfileRole.ErrNotFound) {
|
||||
r := dto.NotFound().WithMessage("profile role not found")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
lg.Error().Err(err).Msg("failed to update profile role")
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(role)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// DeleteProfileRole deletes a profile role.
|
||||
// @Summary delete profile role
|
||||
// @Description delete a profile role
|
||||
// @Tags BackOffice
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param id path string true "profile role ID"
|
||||
// @Success 200 {object} dto.Response "success"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 401 {object} dto.ErrorResponse "unauthorized"
|
||||
// @Failure 404 {object} dto.ErrorResponse "not found"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/backoffice/profile-roles/{id} [delete]
|
||||
func (ctl *Controller) DeleteProfileRole(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "backoffice").
|
||||
Str("router", "profile-roles").
|
||||
Str("handler", "DeleteProfileRole").
|
||||
Logger()
|
||||
|
||||
var req dto.DeleteProfileRoleRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(req.ID)
|
||||
if err != nil {
|
||||
r := dto.BadRequest().WithMessage("invalid profile role ID")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ctl.profileRoleService.Delete(c.Request.Context(), id); err != nil {
|
||||
if errors.Is(err, appProfileRole.ErrNotFound) {
|
||||
r := dto.NotFound().WithMessage("profile role not found")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
lg.Error().Err(err).Msg("failed to delete profile role")
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithMessage("profile role deleted successfully")
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
50
internal/delivery/http/backoffice/utils.go
Normal file
50
internal/delivery/http/backoffice/utils.go
Normal file
@@ -0,0 +1,50 @@
|
||||
package backoffice
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"base/internal/dto"
|
||||
"base/pkg/helper"
|
||||
"base/pkg/validation"
|
||||
)
|
||||
|
||||
func shouldBindJSON(c *gin.Context) bool {
|
||||
switch c.Request.Method {
|
||||
case http.MethodPost, http.MethodPut, http.MethodPatch:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
contentType := c.ContentType()
|
||||
return contentType == "application/json" || strings.HasSuffix(contentType, "+json")
|
||||
}
|
||||
|
||||
func (ctl *Controller) validateRequest(c *gin.Context, request dto.DTO) bool {
|
||||
if err := c.ShouldBindUri(request); err != nil {
|
||||
ctl.logger.Error().Err(err).Msg("RequestBindErr")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request path parameters"})
|
||||
return false
|
||||
}
|
||||
if err := c.ShouldBindQuery(request); err != nil {
|
||||
ctl.logger.Error().Err(err).Msg("RequestBindErr")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request query parameters"})
|
||||
return false
|
||||
}
|
||||
if shouldBindJSON(c) {
|
||||
if err := c.ShouldBindJSON(request); err != nil {
|
||||
ctl.logger.Error().Err(err).Msg("RequestBindErr")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return false
|
||||
}
|
||||
}
|
||||
validator := validation.NewGenericValidator()
|
||||
validator.Validate(helper.StructToMap(request), request.Schema())
|
||||
if validator.HasErrors() {
|
||||
ctl.logger.Error().Any("request", request).Any("error", validator.GetErrors()).Msg("validatorHasErrors")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"errors": validator.GetErrors()})
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
13
internal/delivery/http/module.go
Normal file
13
internal/delivery/http/module.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"go.uber.org/fx"
|
||||
|
||||
"base/internal/delivery/http/backoffice"
|
||||
"base/internal/delivery/http/platform"
|
||||
)
|
||||
|
||||
var Module = fx.Module(
|
||||
"http",
|
||||
fx.Provide(platform.New, backoffice.New),
|
||||
)
|
||||
363
internal/delivery/http/platform/asset.go
Normal file
363
internal/delivery/http/platform/asset.go
Normal file
@@ -0,0 +1,363 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
appAsset "base/internal/application/asset"
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
// ListAssetCategories godoc
|
||||
// @Summary list asset categories
|
||||
// @Description returns all asset categories
|
||||
// @Tags Asset
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.ListCategoriesResponse "list of categories"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/assets/categories [get]
|
||||
func (ctl *Controller) ListAssetCategories(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "asset").
|
||||
Str("handler", "ListAssetCategories").
|
||||
Logger()
|
||||
|
||||
resp, err := ctl.assetService.ListCategories(c.Request.Context())
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to list asset categories")
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(resp)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// ListCategoriesWithPreview returns categories with up to 8 assets per category.
|
||||
// @Summary list categories with preview assets
|
||||
// @Description returns asset categories, each with up to N sample assets (default 8). Use for carousels and landing previews.
|
||||
// @Tags Asset
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.CategoriesPreviewRequest true "filter options"
|
||||
// @Success 200 {object} dto.CategoriesPreviewResponse "categories with preview assets"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/assets/categories/preview [post]
|
||||
func (ctl *Controller) ListCategoriesWithPreview(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "asset").
|
||||
Str("handler", "ListCategoriesWithPreview").
|
||||
Logger()
|
||||
|
||||
var req dto.CategoriesPreviewRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
if req.AssetsPerCategory == 0 {
|
||||
req.AssetsPerCategory = 8
|
||||
}
|
||||
|
||||
resp, err := ctl.assetService.GetCategoriesWithPreview(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to list categories with preview")
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(resp).WithMessage("Asset categories with sample assets")
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// ListAssetsByCategoryID returns paginated assets for a single category (Phase 2 of two-phase loading).
|
||||
// @Summary list assets by category ID
|
||||
// @Description returns paginated assets for the given category. Use after fetching categories from GET /assets/categories.
|
||||
// @Tags Asset
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "category UUID"
|
||||
// @Param limit query int false "max items per page (default 10)"
|
||||
// @Param page query int false "page number (default 1)"
|
||||
// @Success 200 {object} dto.ListAssetsByCategoryIDResponse "paginated assets for category"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid category ID"
|
||||
// @Failure 404 {object} dto.ErrorResponse "category not found"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/assets/categories/{id}/assets [get]
|
||||
func (ctl *Controller) ListAssetsByCategoryID(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "asset").
|
||||
Str("handler", "ListAssetsByCategoryID").
|
||||
Logger()
|
||||
|
||||
categoryID, err := uuid.Parse(c.Param("id"))
|
||||
if err != nil {
|
||||
r := dto.BadRequest().WithMessage("invalid category ID")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
limit, page := 10, 1
|
||||
if v := c.Query("limit"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
if v := c.Query("page"); v != "" {
|
||||
if n, err := strconv.Atoi(v); err == nil && n > 0 {
|
||||
page = n
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := ctl.assetService.ListByCategoryID(c.Request.Context(), categoryID, limit, page)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to list assets by category")
|
||||
switch {
|
||||
case errors.Is(err, appAsset.ErrCategoryNotFound):
|
||||
r := dto.NotFound().WithMessage("category not found")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(resp)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// CreateAsset godoc
|
||||
// @Summary create asset
|
||||
// @Description create a new asset
|
||||
// @Tags Asset
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.CreateAssetRequest true "create asset request"
|
||||
// @Success 201 {object} dto.AssetResponse "asset response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 404 {object} dto.ErrorResponse "category not found"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/assets [post]
|
||||
func (ctl *Controller) CreateAsset(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "asset").
|
||||
Str("handler", "CreateAsset").
|
||||
Logger()
|
||||
|
||||
var req dto.CreateAssetRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
asset, err := ctl.assetService.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to create asset")
|
||||
switch {
|
||||
case errors.Is(err, appAsset.ErrCategoryNotFound):
|
||||
r := dto.NotFound().WithMessage("asset category not found")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError().WithMessage("failed to create asset")
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.Created(asset)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// GetAsset godoc
|
||||
// @Summary get asset by ID
|
||||
// @Description get asset by ID
|
||||
// @Tags Asset
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "asset ID"
|
||||
// @Success 200 {object} dto.AssetResponse "asset response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 404 {object} dto.ErrorResponse "asset not found"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/assets/{id} [get]
|
||||
func (ctl *Controller) GetAsset(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "asset").
|
||||
Str("handler", "GetAsset").
|
||||
Logger()
|
||||
|
||||
var req dto.GetAssetRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(req.ID)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("invalid asset ID")
|
||||
r := dto.BadRequest().WithMessage("invalid asset ID")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
asset, err := ctl.assetService.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to get asset")
|
||||
switch {
|
||||
case errors.Is(err, appAsset.ErrAssetNotFound):
|
||||
r := dto.NotFound().WithMessage("asset not found")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(asset)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// UpdateAsset godoc
|
||||
// @Summary update asset
|
||||
// @Description update an existing asset
|
||||
// @Tags Asset
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "asset ID"
|
||||
// @Param request body dto.UpdateAssetRequest true "update asset request"
|
||||
// @Success 200 {object} dto.AssetResponse "asset response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 404 {object} dto.ErrorResponse "asset not found"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/assets/{id} [put]
|
||||
func (ctl *Controller) UpdateAsset(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "asset").
|
||||
Str("handler", "UpdateAsset").
|
||||
Logger()
|
||||
|
||||
var req dto.UpdateAssetRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
asset, err := ctl.assetService.Update(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to update asset")
|
||||
switch {
|
||||
case errors.Is(err, appAsset.ErrAssetNotFound):
|
||||
r := dto.NotFound().WithMessage("asset not found")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(asset)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// ListAssetsByProfile godoc
|
||||
// @Summary list assets by profile ID
|
||||
// @Description list all assets for a profile
|
||||
// @Tags Asset
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "profile ID"
|
||||
// @Success 200 {object} dto.ListAssetsResponse "list assets response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/profiles/{id}/assets [get]
|
||||
func (ctl *Controller) ListAssetsByProfile(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "asset").
|
||||
Str("handler", "ListAssetsByProfile").
|
||||
Logger()
|
||||
|
||||
var req dto.ListAssetsByProfileRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
profileID, err := uuid.Parse(req.ProfileID)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("invalid profile ID")
|
||||
r := dto.BadRequest().WithMessage("invalid profile ID")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
assets, err := ctl.assetService.FindByProfileID(c.Request.Context(), profileID)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to list assets")
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(assets)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// DeleteAsset godoc
|
||||
// @Summary delete asset
|
||||
// @Description delete an asset
|
||||
// @Tags Asset
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "asset ID"
|
||||
// @Success 200 {object} dto.SuccessResponse "success response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 404 {object} dto.ErrorResponse "asset not found"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/assets/{id} [delete]
|
||||
func (ctl *Controller) DeleteAsset(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "asset").
|
||||
Str("handler", "DeleteAsset").
|
||||
Logger()
|
||||
|
||||
var req dto.DeleteAssetRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(req.ID)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("invalid asset ID")
|
||||
r := dto.BadRequest().WithMessage("invalid asset ID")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := ctl.assetService.Delete(c.Request.Context(), id); err != nil {
|
||||
lg.Error().Err(err).Msg("failed to delete asset")
|
||||
switch {
|
||||
case errors.Is(err, appAsset.ErrAssetNotFound):
|
||||
r := dto.NotFound().WithMessage("asset not found")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithMessage("asset deleted successfully")
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
468
internal/delivery/http/platform/auth.go
Normal file
468
internal/delivery/http/platform/auth.go
Normal file
@@ -0,0 +1,468 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"base/internal/application/auth"
|
||||
"base/internal/dto"
|
||||
"base/internal/pkg/oauth"
|
||||
)
|
||||
|
||||
// RegisterWithCredentials godoc
|
||||
// @Summary register with credentials
|
||||
// @Description register a new user with email and password
|
||||
// @Tags Public
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.RegisterRequest true "register request"
|
||||
// @Success 200 {object} dto.TokenResponse "token response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/auth/register [post]
|
||||
func (ctl *Controller) RegisterWithCredentials(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "auth").
|
||||
Str("handler", "RegisterWithCredentials").
|
||||
Logger()
|
||||
|
||||
var req dto.RegisterRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
tokens, err := ctl.authService.RegisterWithCredentials(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to register user")
|
||||
switch {
|
||||
case errors.Is(err, auth.ErrUserAlreadyExists):
|
||||
r := dto.Conflict().WithMessage("user already exists")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(dto.TokenResponse{
|
||||
AccessToken: tokens.AccessToken,
|
||||
RefreshToken: tokens.RefreshToken,
|
||||
})
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// LoginWithCredentials godoc
|
||||
// @Summary login with credentials
|
||||
// @Description login with email and password
|
||||
// @Tags Public
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.LoginRequest true "login request"
|
||||
// @Success 200 {object} dto.TokenResponse "token response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 401 {object} dto.ErrorResponse "invalid credentials"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/auth/login [post]
|
||||
func (ctl *Controller) LoginWithCredentials(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "auth").
|
||||
Str("handler", "LoginWithCredentials").
|
||||
Logger()
|
||||
|
||||
var req dto.LoginRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
tokens, err := ctl.authService.LoginWithCredentials(
|
||||
c.Request.Context(),
|
||||
req.Email,
|
||||
req.Password,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to login")
|
||||
switch {
|
||||
case errors.Is(err, auth.ErrInvalidCredentials):
|
||||
r := dto.Unauthorized().WithMessage("invalid credentials")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(dto.TokenResponse{
|
||||
AccessToken: tokens.AccessToken,
|
||||
RefreshToken: tokens.RefreshToken,
|
||||
})
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// RefreshToken godoc
|
||||
// @Summary refresh token
|
||||
// @Description refresh access token using refresh token
|
||||
// @Tags Public
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.RefreshTokenRequest true "refresh token request"
|
||||
// @Success 200 {object} dto.TokenResponse "token response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 401 {object} dto.ErrorResponse "invalid refresh token"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/auth/refresh-token [post]
|
||||
func (ctl *Controller) RefreshToken(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "auth").
|
||||
Str("handler", "RefreshToken").
|
||||
Logger()
|
||||
|
||||
var req dto.RefreshTokenRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
tokens, err := ctl.authService.RefreshToken(
|
||||
c.Request.Context(),
|
||||
req.RefreshToken,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to refresh token")
|
||||
switch {
|
||||
case errors.Is(err, auth.ErrInvalidRefreshToken):
|
||||
r := dto.Unauthorized().WithMessage("invalid refresh token")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(dto.TokenResponse{
|
||||
AccessToken: tokens.AccessToken,
|
||||
RefreshToken: tokens.RefreshToken,
|
||||
})
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// GetOauthRedirectURL godoc
|
||||
// @Summary get oauth redirect url
|
||||
// @Description get OAuth redirect URL for the specified provider
|
||||
// @Tags Public
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.OAuthRedirectURLRequest true "oauth redirect url request"
|
||||
// @Success 200 {object} dto.OAuthRedirectURLResponse "oauth redirect url response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/auth/oauth/redirect-url [post]
|
||||
func (ctl *Controller) GetOauthRedirectURL(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "auth").
|
||||
Str("handler", "GetOauthRedirectURL").
|
||||
Logger()
|
||||
|
||||
var req dto.OAuthRedirectURLRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
redirectURL, err := ctl.authService.GetOAuthRedirectURL(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to get OAuth redirect URL")
|
||||
r := dto.BadRequest().WithMessage(err.Error())
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(dto.OAuthRedirectURLResponse{
|
||||
RedirectURL: redirectURL,
|
||||
})
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// OauthCallbackGET handles OAuth redirect from provider (GET with code, state in query).
|
||||
// Compatible with OAuth 2.0 flow where provider redirects to redirect_uri?code=...&state=...
|
||||
// Route: GET /api/v1/auth/oauth/callback/:provider
|
||||
func (ctl *Controller) OauthCallbackGET(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "auth").
|
||||
Str("handler", "OauthCallbackGET").
|
||||
Logger()
|
||||
|
||||
providerStr := c.Param("provider")
|
||||
provider, err := oauth.ParseProvider(providerStr)
|
||||
if err != nil {
|
||||
r := dto.BadRequest().WithMessage("invalid provider")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
code := c.Query("code")
|
||||
if code == "" {
|
||||
r := dto.BadRequest().WithMessage("code is required")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
req := dto.OAuthCallbackRequest{Provider: provider, Code: code}
|
||||
response, err := ctl.authService.OAuthCallback(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to handle OAuth callback")
|
||||
msg := err.Error()
|
||||
if errors.Is(err, oauth.ErrMockNotEnabled) {
|
||||
msg = "OAuth mock is not enabled - set oauth.mock.enabled=true and oauth.mock.base_url for local development"
|
||||
}
|
||||
r := dto.BadRequest().WithMessage(msg)
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
// If success_redirect in query, redirect with tokens in fragment (OAuth-compatible)
|
||||
if redirectTo := c.Query("success_redirect"); redirectTo != "" {
|
||||
u, err := url.Parse(redirectTo)
|
||||
if err == nil {
|
||||
u.Fragment = fmt.Sprintf("access_token=%s&refresh_token=%s&is_new_user=%t",
|
||||
response.AccessToken, response.RefreshToken, response.IsNewUser)
|
||||
c.Redirect(http.StatusFound, u.String())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(dto.OAuthCallbackResponse{
|
||||
AccessToken: response.AccessToken,
|
||||
RefreshToken: response.RefreshToken,
|
||||
IsNewUser: response.IsNewUser,
|
||||
})
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// OauthCallback handles OAuth callback via POST (e.g. frontend posting code).
|
||||
// @Summary oauth callback
|
||||
// @Description handle OAuth callback and authenticate user
|
||||
// @Tags Public
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.OAuthCallbackRequest true "oauth callback request"
|
||||
// @Success 200 {object} dto.OAuthCallbackResponse "oauth callback response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/auth/oauth/callback [post]
|
||||
func (ctl *Controller) OauthCallback(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "auth").
|
||||
Str("handler", "OauthCallback").
|
||||
Logger()
|
||||
|
||||
var req dto.OAuthCallbackRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
response, err := ctl.authService.OAuthCallback(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to handle OAuth callback")
|
||||
msg := err.Error()
|
||||
if errors.Is(err, oauth.ErrMockNotEnabled) {
|
||||
msg = "OAuth mock is not enabled - set oauth.mock.enabled=true and oauth.mock.base_url for local development"
|
||||
}
|
||||
r := dto.BadRequest().WithMessage(msg)
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(response)
|
||||
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// SendVerificationEmail godoc
|
||||
// @Summary send verification email
|
||||
// @Description send verification email to the authenticated user
|
||||
// @Tags Public
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body dto.SendVerificationEmailRequest true "send verification email request"
|
||||
// @Success 200 {object} dto.SuccessResponse "success response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/auth/send-verification-email [post]
|
||||
func (ctl *Controller) SendVerificationEmail(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "auth").
|
||||
Str("handler", "SendVerificationEmail").
|
||||
Logger()
|
||||
|
||||
var req dto.SendVerificationEmailRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
err := ctl.authService.SendVerificationEmail(c.Request.Context(), dto.SendVerificationEmailRequest{})
|
||||
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to send verification email")
|
||||
switch {
|
||||
case errors.Is(err, auth.ErrUserNotFound):
|
||||
r := dto.NotFound().WithMessage("user not found")
|
||||
c.JSON(r.Status, r)
|
||||
case errors.Is(err, auth.ErrEmailAlreadyVerified):
|
||||
r := dto.BadRequest().WithMessage("email already verified")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithMessage("verification email sent")
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// VerifyAccount godoc
|
||||
// @Summary verify account
|
||||
// @Description verify account with verification code
|
||||
// @Tags Public
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body dto.VerifyAccountRequest true "verify account request"
|
||||
// @Success 200 {object} dto.SuccessResponse "success response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/auth/verify-account [post]
|
||||
func (ctl *Controller) VerifyAccount(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "auth").
|
||||
Str("handler", "VerifyAccount").
|
||||
Logger()
|
||||
|
||||
var req dto.VerifyAccountRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
err := ctl.authService.VerifyAccount(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to verify account")
|
||||
switch {
|
||||
case errors.Is(err, auth.ErrUserNotFound):
|
||||
r := dto.NotFound().WithMessage("user not found")
|
||||
c.JSON(r.Status, r)
|
||||
case errors.Is(err, auth.ErrInvalidVerificationCode):
|
||||
r := dto.BadRequest().WithMessage("invalid verification code")
|
||||
c.JSON(r.Status, r)
|
||||
case errors.Is(err, auth.ErrEmailAlreadyVerified):
|
||||
r := dto.BadRequest().WithMessage("email already verified")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithMessage("account verified successfully")
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// SendResetPasswordEmail godoc
|
||||
// @Summary send reset password email
|
||||
// @Description send password reset email
|
||||
// @Tags Public
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.SendResetPasswordEmailRequest true "send reset password email request"
|
||||
// @Success 200 {object} dto.SuccessResponse "success response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/auth/send-reset-password-email [post]
|
||||
func (ctl *Controller) SendResetPasswordEmail(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "auth").
|
||||
Str("handler", "SendResetPasswordEmail").
|
||||
Logger()
|
||||
|
||||
var req dto.SendResetPasswordEmailRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
err := ctl.authService.SendResetPasswordEmail(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
// TODO: we should handle for when user not exist, email service goes wrong and ...
|
||||
lg.Error().Err(err).Msg("failed to send reset password email")
|
||||
// Don't reveal if user exists or not for security
|
||||
r := dto.OK().WithMessage("if the email exists, a reset password email has been sent")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithMessage("if the email exists, a reset password email has been sent")
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// ResetPassword godoc
|
||||
// @Summary reset password
|
||||
// @Description reset password with reset code
|
||||
// @Tags Public
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.ResetPasswordRequest true "reset password request"
|
||||
// @Success 200 {object} dto.TokenResponse "token response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/auth/reset-password [post]
|
||||
func (ctl *Controller) ResetPassword(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "auth").
|
||||
Str("handler", "ResetPassword").
|
||||
Logger()
|
||||
|
||||
var req dto.ResetPasswordRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
tokens, err := ctl.authService.ResetPassword(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to reset password")
|
||||
switch {
|
||||
case errors.Is(err, auth.ErrUserNotFound):
|
||||
r := dto.NotFound().WithMessage("user not found")
|
||||
c.JSON(r.Status, r)
|
||||
case errors.Is(err, auth.ErrInvalidVerificationCode):
|
||||
r := dto.BadRequest().WithMessage("invalid reset code")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(dto.TokenResponse{
|
||||
AccessToken: tokens.AccessToken,
|
||||
RefreshToken: tokens.RefreshToken,
|
||||
})
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
36
internal/delivery/http/platform/landing.go
Normal file
36
internal/delivery/http/platform/landing.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
|
||||
// GetLanding returns the landing page data.
|
||||
// @Summary get landing page
|
||||
// @Description returns landing page with categories, specialist roles, assets by category, specialists, and blogs
|
||||
// @Tags Landing
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} dto.Landing "landing page data"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/landing [get]
|
||||
func (ctl *Controller) GetLanding(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "landing").
|
||||
Str("handler", "GetLanding").
|
||||
Logger()
|
||||
|
||||
resp, err := ctl.landingService.GetLanding(c.Request.Context())
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to get landing page")
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(resp.Data).WithMessage(resp.Message)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
106
internal/delivery/http/platform/overview.go
Normal file
106
internal/delivery/http/platform/overview.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"base/internal/domain/profile"
|
||||
"base/internal/dto"
|
||||
"base/internal/server/middleware"
|
||||
)
|
||||
|
||||
// GetSpecialistOverview returns overview for specialist users with full asset details, profile, and skills.
|
||||
// @Summary get specialist overview
|
||||
// @Description get overview for specialist view with assets, profile, skills, recently joined, analytics
|
||||
// @Tags Platform
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} dto.SpecialistOverviewFetchedResponse
|
||||
// @Failure 401 {object} dto.ErrorResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse "profile not found"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/platform/overview/specialist [get]
|
||||
func (ctl *Controller) GetSpecialistOverview(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "overview").
|
||||
Str("handler", "Overview").
|
||||
Logger()
|
||||
|
||||
userIDVal, exists := c.Get(middleware.UserIDKey)
|
||||
if !exists {
|
||||
r := dto.Unauthorized()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
userIDStr, ok := userIDVal.(string)
|
||||
if !ok {
|
||||
r := dto.Unauthorized()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
r := dto.BadRequest().WithMessage("invalid user ID")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := ctl.specialistService.Overview(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to fetch overview")
|
||||
switch {
|
||||
case errors.Is(err, profile.ErrProfileNotFound):
|
||||
r := dto.NotFound().WithMessage("profile not found")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(resp)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// GetDiscoveryOverview returns overview for non-specialist users discovering assets and specialists.
|
||||
// No profile required - callers browse latest assets and profiles.
|
||||
// @Summary get discovery overview
|
||||
// @Description overview for browsing users (latest assets, recently joined profiles, analytics). No profile required.
|
||||
// @Tags Platform
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Success 200 {object} dto.OverviewFetchedResponse "overview response"
|
||||
// @Failure 401 {object} dto.ErrorResponse "unauthorized"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/platform/overview/discovery [get]
|
||||
func (ctl *Controller) GetDiscoveryOverview(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "overview").
|
||||
Str("handler", "GetDiscoveryOverview").
|
||||
Logger()
|
||||
|
||||
if _, exists := c.Get(middleware.UserIDKey); !exists {
|
||||
r := dto.Unauthorized()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
overview, err := ctl.discoveryService.GetDiscoveryOverview(c.Request.Context())
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to get discovery overview")
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(overview)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
274
internal/delivery/http/platform/profile.go
Normal file
274
internal/delivery/http/platform/profile.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
profileDomian "base/internal/domain/profile"
|
||||
"errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
// CreateProfile godoc
|
||||
// @Summary create profile
|
||||
// @Description create a new profile
|
||||
// @Tags Profile
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body dto.CreateProfileRequest true "create profile request"
|
||||
// @Success 201 {object} dto.ProfileResponse "profile response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/profiles [post]
|
||||
func (ctl *Controller) CreateProfile(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "profile").
|
||||
Str("handler", "CreateProfile").
|
||||
Logger()
|
||||
|
||||
var req dto.CreateProfileRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
profile, err := ctl.profileService.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to create profile")
|
||||
r := dto.InternalServerError().WithMessage("failed to create profile")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.Created(profile)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// GetProfile godoc
|
||||
// @Summary get profile by ID
|
||||
// @Description get profile by ID
|
||||
// @Tags Profile
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "profile ID"
|
||||
// @Success 200 {object} dto.ProfileResponse "profile response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 404 {object} dto.ErrorResponse "profile not found"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/profiles/{id} [get]
|
||||
func (ctl *Controller) GetProfile(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "profile").
|
||||
Str("handler", "GetProfile").
|
||||
Logger()
|
||||
|
||||
var req dto.GetProfileRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(req.ID)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("invalid profile ID")
|
||||
r := dto.BadRequest().WithMessage("invalid profile ID")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
profile, err := ctl.profileService.GetByID(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to get profile")
|
||||
switch {
|
||||
case errors.Is(err, profileDomian.ErrProfileNotFound):
|
||||
r := dto.NotFound().WithMessage("profile not found")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(profile)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// GetProfileByHandle godoc
|
||||
// @Summary get profile by handle
|
||||
// @Description get profile by handle
|
||||
// @Tags Profile
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param handle path string true "profile handle"
|
||||
// @Success 200 {object} dto.ProfileResponse "profile response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 404 {object} dto.ErrorResponse "profile not found"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/profiles/handle/{handle} [get]
|
||||
func (ctl *Controller) GetProfileByHandle(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "profile").
|
||||
Str("handler", "GetProfileByHandle").
|
||||
Logger()
|
||||
|
||||
var req dto.GetProfileByHandleRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
profile, err := ctl.profileService.GetByHandle(c.Request.Context(), req.Handle)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to get profile by handle")
|
||||
switch {
|
||||
case errors.Is(err, profileDomian.ErrProfileNotFound):
|
||||
r := dto.NotFound().WithMessage("profile not found")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(profile)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// UpdateProfile godoc
|
||||
// @Summary update profile
|
||||
// @Description update an existing profile
|
||||
// @Tags Profile
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "profile ID"
|
||||
// @Param request body dto.UpdateProfileRequest true "update profile request"
|
||||
// @Success 200 {object} dto.ProfileResponse "profile response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 404 {object} dto.ErrorResponse "profile not found"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/profiles/{id} [put]
|
||||
func (ctl *Controller) UpdateProfile(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "profile").
|
||||
Str("handler", "UpdateProfile").
|
||||
Logger()
|
||||
|
||||
var req dto.UpdateProfileRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
profile, err := ctl.profileService.Update(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to update profile")
|
||||
switch {
|
||||
case errors.Is(err, profileDomian.ErrProfileNotFound):
|
||||
r := dto.NotFound().WithMessage("profile not found")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(profile)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// ListProfiles godoc
|
||||
// @Summary list profiles
|
||||
// @Description list profiles with filtering and pagination
|
||||
// @Tags Profile
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param role_id query string false "role ID"
|
||||
// @Param first_name query string false "first name"
|
||||
// @Param last_name query string false "last name"
|
||||
// @Param company query string false "company"
|
||||
// @Param skill_name query string false "skill name"
|
||||
// @Param page query int false "page number" default(1)
|
||||
// @Param page_size query int false "page size" default(10)
|
||||
// @Param sorted_by query string false "sort field"
|
||||
// @Param ascending query bool false "ascending order" default(false)
|
||||
// @Success 200 {object} dto.ListProfilesResponse "list profiles response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/profiles [get]
|
||||
func (ctl *Controller) ListProfiles(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "profile").
|
||||
Str("handler", "ListProfiles").
|
||||
Logger()
|
||||
|
||||
var req dto.ListProfilesRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
profiles, err := ctl.profileService.List(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to list profiles")
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(profiles)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// DeleteProfile godoc
|
||||
// @Summary delete profile
|
||||
// @Description delete a profile
|
||||
// @Tags Profile
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path string true "profile ID"
|
||||
// @Success 200 {object} dto.SuccessResponse "success response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 404 {object} dto.ErrorResponse "profile not found"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/profiles/{id} [delete]
|
||||
func (ctl *Controller) DeleteProfile(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "profile").
|
||||
Str("handler", "DeleteProfile").
|
||||
Logger()
|
||||
|
||||
var req dto.DeleteProfileRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(req.ID)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("invalid profile ID")
|
||||
r := dto.BadRequest().WithMessage("invalid profile ID")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
err = ctl.profileService.Delete(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to delete profile")
|
||||
switch {
|
||||
case errors.Is(err, profileDomian.ErrProfileNotFound):
|
||||
r := dto.NotFound().WithMessage("profile not found")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithMessage("profile deleted successfully")
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
34
internal/delivery/http/platform/profilerole.go
Normal file
34
internal/delivery/http/platform/profilerole.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
// ListProfileRoles returns the list of profile roles for setup-profile.
|
||||
// @Summary list profile roles
|
||||
// @Description returns all profile roles (id, title) for platform - use role_id when calling setup-profile
|
||||
// @Tags Platform
|
||||
// @Produce json
|
||||
// @Success 200 {array} dto.ProfileRole "list of profile roles"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/platform/profile-roles [get]
|
||||
func (ctl *Controller) ListProfileRoles(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "platform").
|
||||
Str("handler", "ListProfileRoles").
|
||||
Logger()
|
||||
|
||||
roles, err := ctl.profileRoleService.List(c.Request.Context())
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to list profile roles")
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(roles)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
163
internal/delivery/http/platform/public.go
Normal file
163
internal/delivery/http/platform/public.go
Normal file
@@ -0,0 +1,163 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog"
|
||||
"go.uber.org/fx"
|
||||
|
||||
"base/config"
|
||||
appAsset "base/internal/application/asset"
|
||||
appAuth "base/internal/application/auth"
|
||||
appDiscovery "base/internal/application/discovery"
|
||||
appLanding "base/internal/application/landing"
|
||||
appProfile "base/internal/application/profile"
|
||||
appProfileRole "base/internal/application/profilerole"
|
||||
appSkill "base/internal/application/skill"
|
||||
appSpecialist "base/internal/application/specialist"
|
||||
"base/internal/server/middleware"
|
||||
)
|
||||
|
||||
type Controller struct {
|
||||
logger zerolog.Logger
|
||||
middleware middleware.Middleware
|
||||
config *config.AppConfig
|
||||
e *gin.Engine
|
||||
authService appAuth.Service
|
||||
profileService appProfile.Service
|
||||
profileRoleService appProfileRole.Service
|
||||
skillService appSkill.Service
|
||||
assetService appAsset.Service
|
||||
discoveryService appDiscovery.Service
|
||||
landingService appLanding.Service
|
||||
specialistService appSpecialist.Service
|
||||
}
|
||||
|
||||
type Param struct {
|
||||
Logger zerolog.Logger
|
||||
Engine *gin.Engine
|
||||
Middleware middleware.Middleware
|
||||
Config *config.AppConfig
|
||||
AuthService appAuth.Service
|
||||
ProfileService appProfile.Service
|
||||
ProfileRoleService appProfileRole.Service
|
||||
SkillService appSkill.Service
|
||||
AssetService appAsset.Service
|
||||
DiscoveryService appDiscovery.Service
|
||||
LandingService appLanding.Service
|
||||
SpecialistService appSpecialist.Service
|
||||
|
||||
fx.In
|
||||
}
|
||||
|
||||
func New(lc fx.Lifecycle, param Param) *Controller {
|
||||
c := &Controller{
|
||||
logger: param.Logger,
|
||||
e: param.Engine,
|
||||
middleware: param.Middleware,
|
||||
config: param.Config,
|
||||
authService: param.AuthService,
|
||||
profileService: param.ProfileService,
|
||||
profileRoleService: param.ProfileRoleService,
|
||||
skillService: param.SkillService,
|
||||
assetService: param.AssetService,
|
||||
discoveryService: param.DiscoveryService,
|
||||
landingService: param.LandingService,
|
||||
specialistService: param.SpecialistService,
|
||||
}
|
||||
|
||||
lc.Append(
|
||||
fx.Hook{
|
||||
OnStart: func(ctx context.Context) error {
|
||||
c.SetupRouter()
|
||||
|
||||
return nil
|
||||
},
|
||||
OnStop: func(ctx context.Context) error {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
return c
|
||||
}
|
||||
|
||||
func (ctl *Controller) SetupRouter() {
|
||||
apiRouter := ctl.e.Group("/api")
|
||||
ctl.registerRoutes(apiRouter.Group("/v1"))
|
||||
ctl.registerSpecialistRoutes(apiRouter.Group("/specialists/v1"))
|
||||
}
|
||||
|
||||
func (ctl *Controller) registerRoutes(router *gin.RouterGroup) {
|
||||
authRouter := router.Group("/auth")
|
||||
ctl.registerAuthRoutes(authRouter)
|
||||
|
||||
accountRouter := router.Group("/account")
|
||||
ctl.registerAccountRoutes(accountRouter)
|
||||
|
||||
profileRouter := router.Group("/profiles")
|
||||
ctl.registerProfileRoutes(profileRouter)
|
||||
ctl.registerAssetRoutes(router)
|
||||
|
||||
platformRouter := router.Group("/platform")
|
||||
ctl.registerPlatformRoutes(platformRouter)
|
||||
|
||||
landingRouter := router.Group("/landing")
|
||||
ctl.registerLandingRoutes(landingRouter)
|
||||
}
|
||||
|
||||
func (ctl *Controller) registerPlatformRoutes(platformRouter *gin.RouterGroup) {
|
||||
protected := platformRouter.Use(ctl.middleware.AuthShield())
|
||||
protected.GET("/profile-roles", ctl.ListProfileRoles)
|
||||
protected.GET("/skills", ctl.ListSkills)
|
||||
protected.GET("/overview/discovery", ctl.GetDiscoveryOverview)
|
||||
protected.GET("/overview/specialist", ctl.GetSpecialistOverview)
|
||||
protected.POST("/verify-account", ctl.VerifyAccount)
|
||||
protected.POST("/setup-profile", ctl.SetupProfile)
|
||||
}
|
||||
|
||||
func (ctl *Controller) registerLandingRoutes(landingRouter *gin.RouterGroup) {
|
||||
landingRouter.GET("", ctl.GetLanding)
|
||||
}
|
||||
|
||||
func (ctl *Controller) registerAuthRoutes(authRouter *gin.RouterGroup) {
|
||||
authRouter.POST("/login", ctl.LoginWithCredentials)
|
||||
authRouter.POST("/register", ctl.RegisterWithCredentials)
|
||||
authRouter.POST("/refresh-token", ctl.RefreshToken)
|
||||
authRouter.POST("/oauth/redirect-url", ctl.GetOauthRedirectURL)
|
||||
authRouter.GET("/oauth/callback/:provider", ctl.OauthCallbackGET)
|
||||
authRouter.POST("/oauth/callback", ctl.OauthCallback)
|
||||
authRouter.POST("/send-reset-password-email", ctl.SendResetPasswordEmail)
|
||||
authRouter.POST("/reset-password", ctl.ResetPassword)
|
||||
|
||||
// Protected routes
|
||||
protectedRoutes := authRouter.Use(ctl.middleware.AuthShield())
|
||||
protectedRoutes.POST("/send-verification-email", ctl.SendVerificationEmail)
|
||||
}
|
||||
|
||||
func (ctl *Controller) registerAccountRoutes(accountRouter *gin.RouterGroup) {
|
||||
protected := accountRouter.Use(ctl.middleware.AuthShield())
|
||||
protected.GET("/info", ctl.GetUserInfo)
|
||||
}
|
||||
|
||||
func (ctl *Controller) registerProfileRoutes(profileRouter *gin.RouterGroup) {
|
||||
profileRouter.POST("", ctl.CreateProfile)
|
||||
profileRouter.GET("", ctl.ListProfiles)
|
||||
profileRouter.GET("/handle/:handle", ctl.GetProfileByHandle)
|
||||
profileRouter.GET("/:id/assets", ctl.ListAssetsByProfile)
|
||||
profileRouter.GET("/:id", ctl.GetProfile)
|
||||
profileRouter.PUT("/:id", ctl.UpdateProfile)
|
||||
profileRouter.DELETE("/:id", ctl.DeleteProfile)
|
||||
}
|
||||
|
||||
func (ctl *Controller) registerAssetRoutes(router *gin.RouterGroup) {
|
||||
assetRouter := router.Group("/assets")
|
||||
assetRouter.GET("/categories", ctl.ListAssetCategories)
|
||||
assetRouter.POST("/categories/preview", ctl.ListCategoriesWithPreview)
|
||||
assetRouter.GET("/categories/:id/assets", ctl.ListAssetsByCategoryID)
|
||||
assetRouter.POST("", ctl.CreateAsset)
|
||||
assetRouter.GET("/:id", ctl.GetAsset)
|
||||
assetRouter.PUT("/:id", ctl.UpdateAsset)
|
||||
assetRouter.DELETE("/:id", ctl.DeleteAsset)
|
||||
}
|
||||
34
internal/delivery/http/platform/skill.go
Normal file
34
internal/delivery/http/platform/skill.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"base/internal/dto"
|
||||
)
|
||||
|
||||
// ListSkills returns the list of skills for profile skill selection.
|
||||
// @Summary list skills
|
||||
// @Description returns all skills from the catalog for profile update skill selection
|
||||
// @Tags Platform
|
||||
// @Produce json
|
||||
// @Success 200 {array} dto.Skill "list of skills"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/platform/skills [get]
|
||||
func (ctl *Controller) ListSkills(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "platform").
|
||||
Str("handler", "ListSkills").
|
||||
Logger()
|
||||
|
||||
skills, err := ctl.skillService.List(c.Request.Context())
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to list skills")
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(skills)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
185
internal/delivery/http/platform/specialist.go
Normal file
185
internal/delivery/http/platform/specialist.go
Normal file
@@ -0,0 +1,185 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"base/internal/domain/profile"
|
||||
"base/internal/dto"
|
||||
"base/internal/server/middleware"
|
||||
)
|
||||
|
||||
func (ctl *Controller) registerSpecialistRoutes(router *gin.RouterGroup) {
|
||||
protected := router.Use(ctl.middleware.AuthShield())
|
||||
protected.PUT("/page-sections/hero", ctl.SpecialistUpdateHero)
|
||||
protected.PUT("/page-sections/contact", ctl.SpecialistUpdateContact)
|
||||
protected.PUT("/page-sections/skills", ctl.SpecialistUpdateSkills)
|
||||
protected.GET("/page-sections", ctl.SpecialistGetPageSections)
|
||||
protected.GET("/profile", ctl.SpecialistGetProfile)
|
||||
}
|
||||
|
||||
// SpecialistUpdateHero updates the hero section of the specialist's profile.
|
||||
// @Summary update hero section
|
||||
// @Tags Specialist
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body dto.HeroDTO true "hero section"
|
||||
// @Success 200 {object} dto.SuccessResponse
|
||||
// @Failure 401 {object} dto.ErrorResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /api/specialists/v1/page-sections/hero [put]
|
||||
func (ctl *Controller) SpecialistUpdateHero(c *gin.Context) {
|
||||
userID, err := getUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var req dto.HeroDTO
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
if err := ctl.specialistService.UpdateHero(c.Request.Context(), userID, req); err != nil {
|
||||
ctl.handleSpecialistError(c, err)
|
||||
return
|
||||
}
|
||||
r := dto.OK().WithMessage("hero updated")
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// SpecialistUpdateContact updates the contact section.
|
||||
// @Summary update contact section
|
||||
// @Tags Specialist
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body dto.ContactDTO true "contact section"
|
||||
// @Success 200 {object} dto.SuccessResponse
|
||||
// @Failure 401 {object} dto.ErrorResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /api/specialists/v1/page-sections/contact [put]
|
||||
func (ctl *Controller) SpecialistUpdateContact(c *gin.Context) {
|
||||
userID, err := getUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var req dto.ContactDTO
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
if err := ctl.specialistService.UpdateContact(c.Request.Context(), userID, req); err != nil {
|
||||
ctl.handleSpecialistError(c, err)
|
||||
return
|
||||
}
|
||||
r := dto.OK().WithMessage("contact updated")
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// SpecialistUpdateSkills updates the skills section.
|
||||
// @Summary update skills section
|
||||
// @Tags Specialist
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body dto.SkillsUpdateRequest true "skills section"
|
||||
// @Success 200 {object} dto.SuccessResponse
|
||||
// @Failure 401 {object} dto.ErrorResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /api/specialists/v1/page-sections/skills [put]
|
||||
func (ctl *Controller) SpecialistUpdateSkills(c *gin.Context) {
|
||||
userID, err := getUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var req dto.SkillsUpdateRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
if err := ctl.specialistService.UpdateSkills(c.Request.Context(), userID, req); err != nil {
|
||||
ctl.handleSpecialistError(c, err)
|
||||
return
|
||||
}
|
||||
r := dto.OK().WithMessage("skills updated")
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// SpecialistGetPageSections returns hero, contact, skills for the specialist.
|
||||
// @Summary get page sections
|
||||
// @Tags Specialist
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Success 200 {object} dto.PageSectionsResponse
|
||||
// @Failure 401 {object} dto.ErrorResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /api/specialists/v1/page-sections [get]
|
||||
func (ctl *Controller) SpecialistGetPageSections(c *gin.Context) {
|
||||
userID, err := getUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := ctl.specialistService.GetPageSections(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
ctl.handleSpecialistError(c, err)
|
||||
return
|
||||
}
|
||||
r := dto.OK().WithData(resp)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// SpecialistGetProfile returns the specialist's full profile.
|
||||
// @Summary get specialist profile
|
||||
// @Tags Specialist
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Success 200 {object} dto.ProfileResponse
|
||||
// @Failure 401 {object} dto.ErrorResponse
|
||||
// @Failure 404 {object} dto.ErrorResponse
|
||||
// @Router /api/specialists/v1/profile [get]
|
||||
func (ctl *Controller) SpecialistGetProfile(c *gin.Context) {
|
||||
userID, err := getUserIDFromContext(c)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
resp, err := ctl.specialistService.GetProfile(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
ctl.handleSpecialistError(c, err)
|
||||
return
|
||||
}
|
||||
r := dto.OK().WithData(resp)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
func getUserIDFromContext(c *gin.Context) (uuid.UUID, error) {
|
||||
val, exists := c.Get(middleware.UserIDKey)
|
||||
if !exists {
|
||||
c.JSON(dto.Unauthorized().Status, dto.Unauthorized())
|
||||
return uuid.Nil, errors.New("unauthorized")
|
||||
}
|
||||
|
||||
str, ok := val.(string)
|
||||
if !ok {
|
||||
c.JSON(dto.Unauthorized().Status, dto.Unauthorized())
|
||||
return uuid.Nil, errors.New("invalid user id type")
|
||||
}
|
||||
|
||||
id, err := uuid.Parse(str)
|
||||
if err != nil {
|
||||
c.JSON(dto.BadRequest().Status, dto.BadRequest().WithMessage("invalid user ID"))
|
||||
return uuid.Nil, err
|
||||
}
|
||||
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (ctl *Controller) handleSpecialistError(c *gin.Context, err error) {
|
||||
switch {
|
||||
case errors.Is(err, profile.ErrProfileNotFound):
|
||||
r := dto.NotFound().WithMessage("profile not found")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
ctl.logger.Error().Err(err).Msg("specialist error")
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
}
|
||||
141
internal/delivery/http/platform/user.go
Normal file
141
internal/delivery/http/platform/user.go
Normal file
@@ -0,0 +1,141 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
|
||||
"base/internal/application/auth"
|
||||
"base/internal/dto"
|
||||
"base/internal/server/middleware"
|
||||
)
|
||||
|
||||
// SetupProfile godoc
|
||||
// @Summary setup profile after registration
|
||||
// @Description complete profile with handle, role, level, and short bio. Requires authentication.
|
||||
// @Tags Platform
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Param request body dto.SetupProfileRequest true "setup profile request"
|
||||
// @Success 200 {object} dto.SuccessResponse "success response"
|
||||
// @Failure 400 {object} dto.ErrorResponse "invalid request"
|
||||
// @Failure 401 {object} dto.ErrorResponse "unauthorized"
|
||||
// @Failure 404 {object} dto.ErrorResponse "user not found"
|
||||
// @Failure 409 {object} dto.ErrorResponse "profile already exists or handle already taken"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/user/platform/setup-profile [post]
|
||||
func (ctl *Controller) SetupProfile(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "auth").
|
||||
Str("handler", "SetupProfile").
|
||||
Logger()
|
||||
|
||||
userIDVal, exists := c.Get(middleware.UserIDKey)
|
||||
if !exists {
|
||||
r := dto.Unauthorized()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
userIDStr, ok := userIDVal.(string)
|
||||
if !ok {
|
||||
r := dto.Unauthorized()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
r := dto.BadRequest().WithMessage("invalid user ID")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.SetupProfileRequest
|
||||
if !ctl.validateRequest(c, &req) {
|
||||
return
|
||||
}
|
||||
|
||||
err = ctl.authService.SetupProfile(c.Request.Context(), userID, req)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to setup profile")
|
||||
switch {
|
||||
case errors.Is(err, auth.ErrProfileAlreadyExists):
|
||||
r := dto.Conflict().WithMessage("profile already exists")
|
||||
c.JSON(r.Status, r)
|
||||
case errors.Is(err, auth.ErrHandleAlreadyTaken):
|
||||
r := dto.Conflict().WithMessage("handle already taken")
|
||||
c.JSON(r.Status, r)
|
||||
case errors.Is(err, auth.ErrUserNotFound):
|
||||
r := dto.NotFound().WithMessage("user not found")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithMessage("profile created successfully")
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
|
||||
// GetUserInfo godoc
|
||||
// @Summary get account info
|
||||
// @Description returns user and profile_id for the authenticated user
|
||||
// @Tags Platform
|
||||
// @Produce json
|
||||
// @Security Bearer
|
||||
// @Success 200 {object} dto.UserInfoResponse "account info"
|
||||
// @Failure 401 {object} dto.ErrorResponse "unauthorized"
|
||||
// @Failure 404 {object} dto.ErrorResponse "user not found"
|
||||
// @Failure 500 {object} dto.ErrorResponse "internal server error"
|
||||
// @Router /api/v1/platform/user/info [get]
|
||||
func (ctl *Controller) GetUserInfo(c *gin.Context) {
|
||||
lg := ctl.logger.With().
|
||||
Str("module", "platform").
|
||||
Str("router", "account").
|
||||
Str("handler", "GetUserInfo").
|
||||
Logger()
|
||||
|
||||
userIDVal, exists := c.Get(middleware.UserIDKey)
|
||||
if !exists {
|
||||
r := dto.Unauthorized()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
userIDStr, ok := userIDVal.(string)
|
||||
if !ok {
|
||||
r := dto.Unauthorized()
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := uuid.Parse(userIDStr)
|
||||
if err != nil {
|
||||
r := dto.BadRequest().WithMessage("invalid user ID")
|
||||
c.JSON(r.Status, r)
|
||||
return
|
||||
}
|
||||
|
||||
info, err := ctl.authService.GetUserInfo(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
lg.Error().Err(err).Msg("failed to get account info")
|
||||
switch {
|
||||
case errors.Is(err, auth.ErrUserNotFound):
|
||||
r := dto.NotFound().WithMessage("user not found")
|
||||
c.JSON(r.Status, r)
|
||||
default:
|
||||
r := dto.InternalServerError()
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r := dto.OK().WithData(info)
|
||||
c.JSON(r.Status, r)
|
||||
}
|
||||
58
internal/delivery/http/platform/utils.go
Normal file
58
internal/delivery/http/platform/utils.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package platform
|
||||
|
||||
import (
|
||||
"base/internal/dto"
|
||||
"base/pkg/helper"
|
||||
"base/pkg/validation"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func shouldBindJSON(c *gin.Context) bool {
|
||||
// Only bind JSON for methods that normally carry bodies
|
||||
switch c.Request.Method {
|
||||
case http.MethodPost,
|
||||
http.MethodPut,
|
||||
http.MethodPatch:
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
||||
// Must actually be JSON
|
||||
contentType := c.ContentType()
|
||||
return contentType == "application/json" ||
|
||||
strings.HasSuffix(contentType, "+json")
|
||||
}
|
||||
|
||||
func (ctl *Controller) validateRequest(c *gin.Context, request dto.DTO) bool {
|
||||
if err := c.ShouldBindUri(&request); err != nil {
|
||||
ctl.logger.Error().Err(err).Msg("RequestBundErr")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request path parameters"})
|
||||
return false
|
||||
}
|
||||
if err := c.ShouldBindQuery(&request); err != nil {
|
||||
ctl.logger.Error().Err(err).Msg("RequestBundErr")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request query parameters"})
|
||||
return false
|
||||
}
|
||||
if shouldBindJSON(c) {
|
||||
if err := c.ShouldBindJSON(&request); err != nil {
|
||||
ctl.logger.Error().Err(err).Msg("RequestBundErr")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid request body"})
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
validator := validation.NewGenericValidator()
|
||||
validator.Validate(helper.StructToMap(request), request.Schema())
|
||||
|
||||
if validator.HasErrors() {
|
||||
ctl.logger.Error().Any("request", request).Any("error", validator.GetErrors()).Msg("validatorHasErrors")
|
||||
c.JSON(http.StatusBadRequest, gin.H{"errors": validator.GetErrors()})
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
12
internal/delivery/module.go
Normal file
12
internal/delivery/module.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package delivery
|
||||
|
||||
import (
|
||||
"go.uber.org/fx"
|
||||
|
||||
"base/internal/delivery/http"
|
||||
)
|
||||
|
||||
var Module = fx.Module(
|
||||
"delivery",
|
||||
http.Module,
|
||||
)
|
||||
0
internal/domain/.gitkeep
Normal file
0
internal/domain/.gitkeep
Normal file
17
internal/domain/asset/artifact.go
Normal file
17
internal/domain/asset/artifact.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package asset
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Artifact struct {
|
||||
ID uuid.UUID
|
||||
AssetID uuid.UUID
|
||||
Type string
|
||||
DownloadURL string
|
||||
Price int // in cents or smallest currency unit
|
||||
Title string
|
||||
Description string
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user