initial commit

This commit is contained in:
m.zare
2026-04-10 18:25:21 +03:30
commit 77ca6c34a3
263 changed files with 34470 additions and 0 deletions

View File

@@ -0,0 +1 @@
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

8
.gitignore vendored Normal file
View File

@@ -0,0 +1,8 @@
/.idea
/.qodo
config.yaml
vendor/
.vscode
.DS_Store
/tmp
/build

13
Dockerfile.mock-oauth Normal file
View 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"]

0
LICENSE Normal file
View File

72
Makefile Normal file
View 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
View 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>

View 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
}

View 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"];
}

View 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",
}

View 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
}

View 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;
}

View 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
View 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, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, "\"", "&quot;")
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
View 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
View 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
View 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"

View 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
}

View File

@@ -0,0 +1,8 @@
package config
type AzureCommunicationConfig struct {
Endpoint string
AccessKey string
ApiVersion string
SenderAddress string
}

View 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
View 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
View 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
View File

@@ -0,0 +1,7 @@
package config
const (
Local = "local"
Prod = "prod"
Stage = "stage"
)

10
config/jwt.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View File

@@ -0,0 +1,8 @@
package config
type SyslogConfig struct {
Host string
Port string
Protocol string
LogLevel string
}

View 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");

View 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");

View 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';

View 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");

View 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");

View 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;

View 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");

View File

@@ -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;

View 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;

View 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";

View 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");

View 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=

View 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]
}
}

View 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]
}
}

View 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]
}
}

View 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]
}
}

View File

@@ -0,0 +1,3 @@
schema "platform" {
comment = "Platform schema for cache tables"
}

View 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]
}
}

View 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]
}
}

View File

@@ -0,0 +1,3 @@
schema "public" {
comment = "Standard public schema"
}

View 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]
}
}

View File

@@ -0,0 +1,3 @@
schema "public" {
}

View 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]
}
}

View 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]
}
}

View 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]
}
}

View 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;

View 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;

View 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
View 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

File diff suppressed because it is too large Load Diff

3130
docs/swagger.json Normal file

File diff suppressed because it is too large Load Diff

2045
docs/swagger.yaml Normal file

File diff suppressed because it is too large Load Diff

134
go.mod Normal file
View 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
View 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=

View 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)
}

View 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"
}
}

View 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
}

View 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
}

View 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
}

View 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),
}
}

View 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])
}

View 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
}

View 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
}

View 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{},
}
}

View 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)
}

View 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,
),
)

View 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
}

View 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
}

View 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
}

View 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
}

View 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{},
}
}

View 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)
})
}

View 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)
}

View File

@@ -0,0 +1,7 @@
package backoffice
var HttpRoutePermissionMap = map[string]string{}
var GrpcRoutePermissionMap = map[string]string{}
var ExcludedGrpcRoutePermissionMap = map[string]string{}

View 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)
}

View 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
}

View 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),
)

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}

View 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)
}
}

View 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)
}

View 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
}

View 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
View File

View 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