forked from baron/baron-sso
Merge pull request 'dev/ci-cd2' (#49) from dev/ci-cd2 into main
Reviewed-on: ai-team/baron-sso#49
This commit is contained in:
@@ -87,7 +87,9 @@ jobs:
|
|||||||
sbom: false
|
sbom: false
|
||||||
|
|
||||||
- name: Temporarily update frontend nginx port
|
- name: Temporarily update frontend nginx port
|
||||||
run: sed -i 's/listen 5000;/listen 80;/g' frontend/nginx.conf
|
run: |
|
||||||
|
sed -i 's/listen 5000;/listen 80;/g' frontend/nginx.conf
|
||||||
|
sed -i 's/proxy_pass http:\/\/baron_backend:3000;/proxy_pass http:\/\/baron_backend:3010;/g' frontend/nginx.conf
|
||||||
|
|
||||||
- name: Build and push frontend RC image
|
- name: Build and push frontend RC image
|
||||||
uses: docker/build-push-action@v5
|
uses: docker/build-push-action@v5
|
||||||
|
|||||||
@@ -24,78 +24,62 @@ jobs:
|
|||||||
if: ${{ inputs.run_lint == true }}
|
if: ${{ inputs.run_lint == true }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# 리포지토리에서 소스 코드를 체크아웃합니다.
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Go 언어 환경을 설정합니다.
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: "1.25"
|
||||||
|
cache-dependency-path: backend/go.sum
|
||||||
|
|
||||||
|
- name: Setup Flutter
|
||||||
|
uses: subosito/flutter-action@v2
|
||||||
|
with:
|
||||||
|
channel: "stable"
|
||||||
|
cache: true
|
||||||
|
|
||||||
# Go 백엔드 코드의 정적 분석을 수행합니다.
|
|
||||||
- name: Lint Go backend
|
- name: Lint Go backend
|
||||||
uses: golangci/golangci-lint-action@v6
|
uses: golangci/golangci-lint-action@v6
|
||||||
with:
|
with:
|
||||||
version: v1.59
|
version: v1.59
|
||||||
working-directory: backend
|
working-directory: backend
|
||||||
|
args: --enable-only=gofmt,gofumpt
|
||||||
|
|
||||||
# Flutter SDK 환경을 설정합니다.
|
|
||||||
- name: Setup Flutter
|
|
||||||
uses: subosito/flutter-action@v2
|
|
||||||
with:
|
|
||||||
channel: 'stable'
|
|
||||||
|
|
||||||
# Flutter/Dart 프론트엔드 코드의 정적 분석을 수행합니다.
|
|
||||||
- name: Analyze Flutter frontend
|
- name: Analyze Flutter frontend
|
||||||
run: |
|
run: |
|
||||||
cd frontend
|
cd frontend
|
||||||
flutter pub get
|
flutter analyze --no-fatal-warnings --no-fatal-infos
|
||||||
flutter analyze
|
|
||||||
|
|
||||||
backend-tests:
|
backend-tests:
|
||||||
needs: lint
|
needs: lint
|
||||||
if: ${{ inputs.run_backend_tests == true }}
|
if: ${{ inputs.run_backend_tests == true }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
services:
|
services:
|
||||||
# 통합 테스트에 사용될 Redis 서비스 컨테이너입니다.
|
|
||||||
# 운영 환경과 일치하도록 포트를 6399로 설정합니다.
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
command: redis-server --port 6399
|
|
||||||
options: >
|
options: >
|
||||||
--health-cmd "redis-cli -p 6399 ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
--health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
ports:
|
|
||||||
- 6399:6399
|
|
||||||
# 통합 테스트에 사용될 ClickHouse 서비스 컨테이너입니다.
|
|
||||||
clickhouse:
|
clickhouse:
|
||||||
image: clickhouse/clickhouse-server:24.6
|
image: clickhouse/clickhouse-server:24.6
|
||||||
ports:
|
options: >
|
||||||
- 9000:9000
|
--health-cmd "wget -qO- 'http://localhost:8123/ping'" --health-interval 10s --health-timeout 5s --health-retries 5
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "-qO-", "http://localhost:8123/ping"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
REDIS_ADDR: localhost:6399
|
REDIS_ADDR: redis:6379
|
||||||
CLICKHOUSE_HOST: localhost
|
CLICKHOUSE_HOST: clickhouse
|
||||||
CLICKHOUSE_PORT_NATIVE: 9000
|
CLICKHOUSE_PORT_NATIVE: 9000
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
# 리포지토리에서 소스 코드를 체크아웃합니다.
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Go 언어 환경을 설정합니다.
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: "1.25"
|
||||||
|
cache-dependency-path: backend/go.sum
|
||||||
|
|
||||||
# 백엔드 디렉토리의 모든 Go 테스트를 실행합니다.
|
|
||||||
- name: Run backend tests
|
- name: Run backend tests
|
||||||
run: |
|
run: |
|
||||||
cd backend
|
cd backend
|
||||||
@@ -106,19 +90,20 @@ jobs:
|
|||||||
if: ${{ inputs.run_frontend_tests == true }}
|
if: ${{ inputs.run_frontend_tests == true }}
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
# 리포지토리에서 소스 코드를 체크아웃합니다.
|
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Flutter SDK 환경을 설정합니다.
|
|
||||||
- name: Setup Flutter
|
- name: Setup Flutter
|
||||||
uses: subosito/flutter-action@v2
|
uses: subosito/flutter-action@v2
|
||||||
with:
|
with:
|
||||||
channel: 'stable'
|
channel: "stable"
|
||||||
|
cache: true
|
||||||
|
|
||||||
# 프론트엔드 디렉토리의 모든 위젯 테스트를 실행합니다.
|
|
||||||
- name: Run frontend tests
|
- name: Run frontend tests
|
||||||
run: |
|
run: |
|
||||||
cd frontend
|
cd frontend
|
||||||
flutter pub get
|
if [ -d test ]; then
|
||||||
flutter test
|
flutter test
|
||||||
|
else
|
||||||
|
echo "No frontend tests: skipping (test/ directory not found)."
|
||||||
|
fi
|
||||||
|
|||||||
@@ -111,12 +111,12 @@ jobs:
|
|||||||
"REDIS_ADDR=${{ vars.PROD_REDIS_ADDR }}" \
|
"REDIS_ADDR=${{ vars.PROD_REDIS_ADDR }}" \
|
||||||
"DESCOPE_PROJECT_ID=${{ vars.DESCOPE_PROJECT_ID }}" \
|
"DESCOPE_PROJECT_ID=${{ vars.DESCOPE_PROJECT_ID }}" \
|
||||||
"DESCOPE_MANAGEMENT_KEY=${{ secrets.DESCOPE_MANAGEMENT_KEY }}" \
|
"DESCOPE_MANAGEMENT_KEY=${{ secrets.DESCOPE_MANAGEMENT_KEY }}" \
|
||||||
"NAVER_CLOUD_ACCESS_KEY=${{ secrets.NAVER_CLOUD_ACCESS_KEY }}" \
|
"NAVER_CLOUD_ACCESS_KEY=${{ vars.NAVER_CLOUD_ACCESS_KEY }}" \
|
||||||
"NAVER_CLOUD_SECRET_KEY=${{ secrets.NAVER_CLOUD_SECRET_KEY }}" \
|
"NAVER_CLOUD_SECRET_KEY=${{ secrets.NAVER_CLOUD_SECRET_KEY }}" \
|
||||||
"NAVER_CLOUD_SERVICE_ID=${{ vars.NAVER_CLOUD_SERVICE_ID }}" \
|
"NAVER_CLOUD_SERVICE_ID=${{ vars.NAVER_CLOUD_SERVICE_ID }}" \
|
||||||
"NAVER_SENDER_PHONE_NUMBER=${{ vars.NAVER_SENDER_PHONE_NUMBER }}" \
|
"NAVER_SENDER_PHONE_NUMBER=${{ vars.NAVER_SENDER_PHONE_NUMBER }}" \
|
||||||
"AWS_REGION=${{ vars.AWS_REGION }}" \
|
"AWS_REGION=${{ vars.AWS_REGION }}" \
|
||||||
"AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }}" \
|
"AWS_ACCESS_KEY_ID=${{ vars.AWS_ACCESS_KEY_ID }}" \
|
||||||
"AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" \
|
"AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }}" \
|
||||||
"AWS_SES_SENDER=${{ vars.AWS_SES_SENDER }}" \
|
"AWS_SES_SENDER=${{ vars.AWS_SES_SENDER }}" \
|
||||||
"FRONTEND_URL=${{ vars.PROD_FRONTEND_URL }}" \
|
"FRONTEND_URL=${{ vars.PROD_FRONTEND_URL }}" \
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ package domain
|
|||||||
|
|
||||||
type EnchantedLinkInitRequest struct {
|
type EnchantedLinkInitRequest struct {
|
||||||
LoginID string `json:"loginId"`
|
LoginID string `json:"loginId"`
|
||||||
URI string `json:"uri,omitempty"` // Redirect URI (optional for polling flow)
|
URI string `json:"uri,omitempty"` // Redirect URI (optional for polling flow)
|
||||||
Method string `json:"method,omitempty"` // "email" or "sms"
|
Method string `json:"method,omitempty"` // "email" or "sms"
|
||||||
}
|
}
|
||||||
|
|
||||||
type EnchantedLinkInitResponse struct {
|
type EnchantedLinkInitResponse struct {
|
||||||
LinkID string `json:"linkId"`
|
LinkID string `json:"linkId"`
|
||||||
PendingRef string `json:"pendingRef"`
|
PendingRef string `json:"pendingRef"`
|
||||||
MaskedEmail string `json:"maskedEmail"`
|
MaskedEmail string `json:"maskedEmail"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,4 +30,4 @@ type QRInitResponse struct {
|
|||||||
QRCode string `json:"qrCode"` // Base64 or URL
|
QRCode string `json:"qrCode"` // Base64 or URL
|
||||||
PendingRef string `json:"pendingRef"`
|
PendingRef string `json:"pendingRef"`
|
||||||
ExpiresIn int `json:"expiresIn"`
|
ExpiresIn int `json:"expiresIn"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,14 @@ import (
|
|||||||
|
|
||||||
// AuditLog represents a single audit event
|
// AuditLog represents a single audit event
|
||||||
type AuditLog struct {
|
type AuditLog struct {
|
||||||
Timestamp time.Time `json:"timestamp"`
|
Timestamp time.Time `json:"timestamp"`
|
||||||
UserID string `json:"user_id"`
|
UserID string `json:"user_id"`
|
||||||
EventType string `json:"event_type"` // e.g., "login_success", "login_failed", "otp_sent"
|
EventType string `json:"event_type"` // e.g., "login_success", "login_failed", "otp_sent"
|
||||||
Status string `json:"status"` // e.g., "success", "failure"
|
Status string `json:"status"` // e.g., "success", "failure"
|
||||||
IPAddress string `json:"ip_address"`
|
IPAddress string `json:"ip_address"`
|
||||||
UserAgent string `json:"user_agent"`
|
UserAgent string `json:"user_agent"`
|
||||||
DeviceID string `json:"device_id,omitempty"`
|
DeviceID string `json:"device_id,omitempty"`
|
||||||
Details string `json:"details,omitempty"` // JSON string or simple text
|
Details string `json:"details,omitempty"` // JSON string or simple text
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuditRepository defines interface for storing logs
|
// AuditRepository defines interface for storing logs
|
||||||
|
|||||||
@@ -7,12 +7,12 @@ type SmsService interface {
|
|||||||
|
|
||||||
// NaverSmsRequest represents the request body for the Naver Cloud SMS API.
|
// NaverSmsRequest represents the request body for the Naver Cloud SMS API.
|
||||||
type NaverSmsRequest struct {
|
type NaverSmsRequest struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
ContentType string `json:"contentType"`
|
ContentType string `json:"contentType"`
|
||||||
CountryCode string `json:"countryCode"`
|
CountryCode string `json:"countryCode"`
|
||||||
From string `json:"from"`
|
From string `json:"from"`
|
||||||
Content string `json:"content"`
|
Content string `json:"content"`
|
||||||
Messages []SmsMessage `json:"messages"`
|
Messages []SmsMessage `json:"messages"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SmsMessage represents a single message to be sent.
|
// SmsMessage represents a single message to be sent.
|
||||||
@@ -23,10 +23,10 @@ type SmsMessage struct {
|
|||||||
|
|
||||||
// NaverSmsResponse represents the response from the Naver Cloud SMS API.
|
// NaverSmsResponse represents the response from the Naver Cloud SMS API.
|
||||||
type NaverSmsResponse struct {
|
type NaverSmsResponse struct {
|
||||||
RequestID string `json:"requestId"`
|
RequestID string `json:"requestId"`
|
||||||
RequestTime string `json:"requestTime"`
|
RequestTime string `json:"requestTime"`
|
||||||
StatusCode string `json:"statusCode"`
|
StatusCode string `json:"statusCode"`
|
||||||
StatusName string `json:"statusName"`
|
StatusName string `json:"statusName"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// SmsRequest represents the request body for sending an SMS.
|
// SmsRequest represents the request body for sending an SMS.
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"net/url"
|
|
||||||
|
|
||||||
"github.com/descope/go-sdk/descope"
|
"github.com/descope/go-sdk/descope"
|
||||||
"github.com/descope/go-sdk/descope/client"
|
"github.com/descope/go-sdk/descope/client"
|
||||||
@@ -50,7 +50,7 @@ func (h *AdminHandler) checkAuth(c *fiber.Ctx) error {
|
|||||||
if adminPass == "" {
|
if adminPass == "" {
|
||||||
adminPass = "admin" // Default fallback
|
adminPass = "admin" // Default fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
reqPass := c.Get("X-Admin-Password")
|
reqPass := c.Get("X-Admin-Password")
|
||||||
if reqPass != adminPass {
|
if reqPass != adminPass {
|
||||||
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid Admin Password"})
|
return c.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error": "Invalid Admin Password"})
|
||||||
@@ -59,11 +59,11 @@ func (h *AdminHandler) checkAuth(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateUserRequest struct {
|
type CreateUserRequest struct {
|
||||||
LoginID string `json:"loginId"`
|
LoginID string `json:"loginId"`
|
||||||
Email string `json:"email"`
|
Email string `json:"email"`
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
Roles []string `json:"roles"`
|
Roles []string `json:"roles"`
|
||||||
Tenants map[string][]string `json:"tenants"` // tenantId -> roles
|
Tenants map[string][]string `json:"tenants"` // tenantId -> roles
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -76,18 +76,20 @@ func (h *AdminHandler) CheckAuth(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// ListUsers - GET /api/v1/admin/users
|
// ListUsers - GET /api/v1/admin/users
|
||||||
func (h *AdminHandler) ListUsers(c *fiber.Ctx) error {
|
func (h *AdminHandler) ListUsers(c *fiber.Ctx) error {
|
||||||
if err := h.checkAuth(c); err != nil { return err }
|
if err := h.checkAuth(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
text := c.Query("text")
|
text := c.Query("text")
|
||||||
// Limit is not directly supported in SearchAll options as a simple int in all SDK versions,
|
// Limit is not directly supported in SearchAll options as a simple int in all SDK versions,
|
||||||
// but let's check the options struct.
|
// but let's check the options struct.
|
||||||
// Based on previous inspection: SearchAll takes UserSearchOptions.
|
// Based on previous inspection: SearchAll takes UserSearchOptions.
|
||||||
|
|
||||||
var users []*descope.UserResponse
|
var users []*descope.UserResponse
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
if text != "" {
|
if text != "" {
|
||||||
options := &descope.UserSearchOptions{ Text: text, Limit: 50 }
|
options := &descope.UserSearchOptions{Text: text, Limit: 50}
|
||||||
users, _, err = h.DescopeClient.Management.User().SearchAll(context.Background(), options)
|
users, _, err = h.DescopeClient.Management.User().SearchAll(context.Background(), options)
|
||||||
} else {
|
} else {
|
||||||
// Nil options means default search (usually returns all or default page)
|
// Nil options means default search (usually returns all or default page)
|
||||||
@@ -104,13 +106,15 @@ func (h *AdminHandler) ListUsers(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// DeleteUser - DELETE /api/v1/admin/users/:loginId
|
// DeleteUser - DELETE /api/v1/admin/users/:loginId
|
||||||
func (h *AdminHandler) DeleteUser(c *fiber.Ctx) error {
|
func (h *AdminHandler) DeleteUser(c *fiber.Ctx) error {
|
||||||
if err := h.checkAuth(c); err != nil { return err }
|
if err := h.checkAuth(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
loginID := c.Params("loginId")
|
loginID := c.Params("loginId")
|
||||||
// Decode if necessary (Fiber usually decodes params, but let's be safe if it's double encoded)
|
// Decode if necessary (Fiber usually decodes params, but let's be safe if it's double encoded)
|
||||||
if decoded, err := url.QueryUnescape(loginID); err == nil {
|
if decoded, err := url.QueryUnescape(loginID); err == nil {
|
||||||
loginID = decoded
|
loginID = decoded
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("[Admin] Deleting user", "loginID", loginID)
|
slog.Info("[Admin] Deleting user", "loginID", loginID)
|
||||||
if err := h.DescopeClient.Management.User().Delete(context.Background(), loginID); err != nil {
|
if err := h.DescopeClient.Management.User().Delete(context.Background(), loginID); err != nil {
|
||||||
@@ -123,12 +127,14 @@ func (h *AdminHandler) DeleteUser(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// UpdateUserStatus - PATCH /api/v1/admin/users/:loginId/status
|
// UpdateUserStatus - PATCH /api/v1/admin/users/:loginId/status
|
||||||
func (h *AdminHandler) UpdateUserStatus(c *fiber.Ctx) error {
|
func (h *AdminHandler) UpdateUserStatus(c *fiber.Ctx) error {
|
||||||
if err := h.checkAuth(c); err != nil { return err }
|
if err := h.checkAuth(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
loginID := c.Params("loginId")
|
loginID := c.Params("loginId")
|
||||||
if decoded, err := url.QueryUnescape(loginID); err == nil {
|
if decoded, err := url.QueryUnescape(loginID); err == nil {
|
||||||
loginID = decoded
|
loginID = decoded
|
||||||
}
|
}
|
||||||
|
|
||||||
var req struct {
|
var req struct {
|
||||||
Status string `json:"status"` // "enabled" or "disabled"
|
Status string `json:"status"` // "enabled" or "disabled"
|
||||||
@@ -161,7 +167,9 @@ func (h *AdminHandler) UpdateUserStatus(c *fiber.Ctx) error {
|
|||||||
|
|
||||||
// UpdateUser - PATCH /api/v1/admin/users/:loginId
|
// UpdateUser - PATCH /api/v1/admin/users/:loginId
|
||||||
func (h *AdminHandler) UpdateUser(c *fiber.Ctx) error {
|
func (h *AdminHandler) UpdateUser(c *fiber.Ctx) error {
|
||||||
if err := h.checkAuth(c); err != nil { return err }
|
if err := h.checkAuth(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
loginID := c.Params("loginId")
|
loginID := c.Params("loginId")
|
||||||
if decoded, err := url.QueryUnescape(loginID); err == nil {
|
if decoded, err := url.QueryUnescape(loginID); err == nil {
|
||||||
@@ -213,7 +221,9 @@ func (h *AdminHandler) UpdateUser(c *fiber.Ctx) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *AdminHandler) CreateUser(c *fiber.Ctx) error {
|
func (h *AdminHandler) CreateUser(c *fiber.Ctx) error {
|
||||||
if err := h.checkAuth(c); err != nil { return err }
|
if err := h.checkAuth(c); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
if h.DescopeClient == nil {
|
if h.DescopeClient == nil {
|
||||||
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Descope Client not configured"})
|
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error": "Descope Client not configured"})
|
||||||
@@ -247,7 +257,7 @@ func (h *AdminHandler) CreateUser(c *fiber.Ctx) error {
|
|||||||
VerifiedEmail: boolPtr(req.Email != ""),
|
VerifiedEmail: boolPtr(req.Email != ""),
|
||||||
VerifiedPhone: boolPtr(normalizedPhone != ""),
|
VerifiedPhone: boolPtr(normalizedPhone != ""),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Roles if provided
|
// Add Roles if provided
|
||||||
if len(req.Roles) > 0 {
|
if len(req.Roles) > 0 {
|
||||||
userObj.Roles = req.Roles
|
userObj.Roles = req.Roles
|
||||||
@@ -278,4 +288,4 @@ func (h *AdminHandler) CreateUser(c *fiber.Ctx) error {
|
|||||||
"message": "User created successfully",
|
"message": "User created successfully",
|
||||||
"user": res,
|
"user": res,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,34 +55,34 @@ func NewAuthHandler(redisService *service.RedisService) *AuthHandler {
|
|||||||
|
|
||||||
var descopeClient *client.DescopeClient
|
var descopeClient *client.DescopeClient
|
||||||
var err error
|
var err error
|
||||||
if projectID != "" {
|
if projectID != "" {
|
||||||
descopeClient, err = client.NewWithConfig(&client.Config{
|
descopeClient, err = client.NewWithConfig(&client.Config{
|
||||||
ProjectID: projectID,
|
ProjectID: projectID,
|
||||||
ManagementKey: managementKey,
|
ManagementKey: managementKey,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Warn("Failed to initialize Descope Client", "error", err)
|
slog.Warn("Failed to initialize Descope Client", "error", err)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &AuthHandler{
|
|
||||||
ProjectID: projectID,
|
|
||||||
SmsService: service.NewSmsService(),
|
|
||||||
EmailService: service.NewEmailService(),
|
|
||||||
RedisService: redisService,
|
|
||||||
DescopeClient: descopeClient,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SendSms sends a verification code via SMS. (Restored for completeness)
|
return &AuthHandler{
|
||||||
func (h *AuthHandler) SendSms(c *fiber.Ctx) error {
|
ProjectID: projectID,
|
||||||
var req domain.SmsRequest
|
SmsService: service.NewSmsService(),
|
||||||
if err := c.BodyParser(&req); err != nil {
|
EmailService: service.NewEmailService(),
|
||||||
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
RedisService: redisService,
|
||||||
}
|
DescopeClient: descopeClient,
|
||||||
|
}
|
||||||
slog.Info("[SMS] Sending code", "phoneNumber", req.PhoneNumber)
|
}
|
||||||
sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "")
|
|
||||||
|
// SendSms sends a verification code via SMS. (Restored for completeness)
|
||||||
|
func (h *AuthHandler) SendSms(c *fiber.Ctx) error {
|
||||||
|
var req domain.SmsRequest
|
||||||
|
if err := c.BodyParser(&req); err != nil {
|
||||||
|
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error": "Invalid request body"})
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("[SMS] Sending code", "phoneNumber", req.PhoneNumber)
|
||||||
|
sanitizedPhone := strings.ReplaceAll(req.PhoneNumber, "-", "")
|
||||||
rand.Seed(time.Now().UnixNano())
|
rand.Seed(time.Now().UnixNano())
|
||||||
code := fmt.Sprintf("%06d", rand.Intn(1000000))
|
code := fmt.Sprintf("%06d", rand.Intn(1000000))
|
||||||
content := fmt.Sprintf("[Baron SSO] 인증번호: %s", code)
|
content := fmt.Sprintf("[Baron SSO] 인증번호: %s", code)
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"baron-sso-backend/internal/domain"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"baron-sso-backend/internal/domain"
|
|
||||||
|
|
||||||
"github.com/ClickHouse/clickhouse-go/v2"
|
"github.com/ClickHouse/clickhouse-go/v2"
|
||||||
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
"github.com/ClickHouse/clickhouse-go/v2/lib/driver"
|
||||||
)
|
)
|
||||||
@@ -25,7 +24,6 @@ func NewClickHouseRepository(host string, port int, user, password, db string) (
|
|||||||
},
|
},
|
||||||
Debug: false,
|
Debug: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to open clickhouse connection: %w", err)
|
return nil, fmt.Errorf("failed to open clickhouse connection: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"baron-sso-backend/internal/domain"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"baron-sso-backend/internal/domain"
|
|
||||||
"github.com/aws/aws-sdk-go-v2/aws"
|
"github.com/aws/aws-sdk-go-v2/aws"
|
||||||
"github.com/aws/aws-sdk-go-v2/config"
|
"github.com/aws/aws-sdk-go-v2/config"
|
||||||
"github.com/aws/aws-sdk-go-v2/credentials"
|
"github.com/aws/aws-sdk-go-v2/credentials"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"baron-sso-backend/internal/domain"
|
||||||
"bytes"
|
"bytes"
|
||||||
"crypto/hmac"
|
"crypto/hmac"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
@@ -14,8 +15,6 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"baron-sso-backend/internal/domain"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type SmsServiceImpl struct {
|
type SmsServiceImpl struct {
|
||||||
@@ -96,8 +95,8 @@ func (s *SmsServiceImpl) SendSms(to, content string) error {
|
|||||||
slog.Error("[SmsService] error response from naver cloud sms api", "body", string(respBody))
|
slog.Error("[SmsService] error response from naver cloud sms api", "body", string(respBody))
|
||||||
return fmt.Errorf("error sending sms: status code %d", resp.StatusCode)
|
return fmt.Errorf("error sending sms: status code %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("[SmsService] sms sent successfully", "body", string(respBody))
|
slog.Info("[SmsService] sms sent successfully", "body", string(respBody))
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,4 +112,4 @@ func (s *SmsServiceImpl) makeSignature(method, url, timestamp string) (string, e
|
|||||||
}
|
}
|
||||||
|
|
||||||
return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
|
return base64.StdEncoding.EncodeToString(h.Sum(nil)), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,39 +0,0 @@
|
|||||||
# ==========================================
|
|
||||||
# Baron SSO - Unified Environment Configuration
|
|
||||||
# ==========================================
|
|
||||||
|
|
||||||
# --- General System ---
|
|
||||||
APP_ENV=development
|
|
||||||
TZ=Asia/Seoul
|
|
||||||
|
|
||||||
# --- Infrastructure Ports ---
|
|
||||||
DB_PORT=5432
|
|
||||||
CLICKHOUSE_PORT_HTTP=8123
|
|
||||||
CLICKHOUSE_PORT_NATIVE=9000
|
|
||||||
BACKEND_PORT=3000
|
|
||||||
FRONTEND_PORT=5000
|
|
||||||
|
|
||||||
# --- Database Credentials (PostgreSQL) ---
|
|
||||||
DB_USER=baron
|
|
||||||
DB_PASSWORD=password
|
|
||||||
DB_NAME=baron_sso
|
|
||||||
|
|
||||||
# --- Backend Configuration ---
|
|
||||||
# Must be 32 bytes. Generate with `openssl rand -hex 32`
|
|
||||||
COOKIE_SECRET=super-secret-key-must-be-32-bytes!
|
|
||||||
REDIS_ADDR=redis:6379
|
|
||||||
|
|
||||||
# --- Frontend Configuration ---
|
|
||||||
# Descope Project ID (Required for Auth)
|
|
||||||
DESCOPE_PROJECT_ID=P2t...your_descope_project_id
|
|
||||||
DESCOPE_MANAGEMENT_KEY=your_descope_management_key_here
|
|
||||||
|
|
||||||
# --- Naver Cloud Services ---
|
|
||||||
NAVER_CLOUD_ACCESS_KEY=ncp_iam_...
|
|
||||||
NAVER_CLOUD_SECRET_KEY=ncp_iam_...
|
|
||||||
NAVER_CLOUD_SERVICE_ID=ncp:sms:kr:...:...
|
|
||||||
NAVER_SENDER_PHONE_NUMBER=...
|
|
||||||
|
|
||||||
# --- URLs for Proxy/Handoff ---
|
|
||||||
FRONTEND_URL=http://localhost:5000
|
|
||||||
BACKEND_URL=http://localhost:3000
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# 1단계: Go 애플리케이션 빌드
|
|
||||||
# 개발 환경과 일치하는 특정 Go 버전 사용
|
|
||||||
FROM golang:1.25-alpine AS builder
|
|
||||||
|
|
||||||
# 컨테이너 내부의 현재 작업 디렉토리 설정
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# go.mod 및 go.sum 파일 복사
|
|
||||||
COPY backend/go.mod backend/go.sum ./
|
|
||||||
|
|
||||||
# 모든 종속성 다운로드. go.mod 및 go.sum 파일이 변경되지 않으면 종속성은 캐시됩니다.
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
# 소스 코드 복사
|
|
||||||
COPY backend/ .
|
|
||||||
|
|
||||||
# Go 앱 빌드
|
|
||||||
# -ldflags="-w -s"는 디버그 정보를 제거하여 바이너리 크기를 줄입니다.
|
|
||||||
# CGO_ENABLED=0은 정적 빌드를 위해 CGO를 비활성화합니다.
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-w -s" -o /go/bin/server ./cmd/server
|
|
||||||
|
|
||||||
# 2단계: 최종 경량 이미지 생성
|
|
||||||
# 더 작고 안전한 환경을 위해 distroless 이미지 사용
|
|
||||||
FROM gcr.io/distroless/static-debian11
|
|
||||||
|
|
||||||
# 빌더 스테이지에서 빌드된 실행 파일만 복사
|
|
||||||
COPY --from=builder /go/bin/server /
|
|
||||||
|
|
||||||
# 외부 세계에 3000번 포트 노출
|
|
||||||
EXPOSE 3000
|
|
||||||
|
|
||||||
# 실행 파일을 실행하는 명령어
|
|
||||||
ENTRYPOINT ["/server"]
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
# 1단계: Flutter 웹 애플리케이션 빌드
|
|
||||||
# 신뢰할 수 있는 출처의 특정 Flutter 버전 사용
|
|
||||||
FROM ghcr.io/cirruslabs/flutter:stable AS builder
|
|
||||||
# ENV RUN_FLUTTER_AS_ROOT=true
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# Docker 캐시를 활용하기 위해 pubspec 파일들을 먼저 복사
|
|
||||||
COPY frontend/pubspec.yaml frontend/pubspec.lock ./
|
|
||||||
RUN flutter pub get
|
|
||||||
|
|
||||||
# 나머지 프론트엔드 소스 코드 복사
|
|
||||||
COPY frontend/ .
|
|
||||||
|
|
||||||
# 웹 애플리케이션 빌드
|
|
||||||
RUN flutter build web --release --no-tree-shake-icons
|
|
||||||
|
|
||||||
# 2단계: 빌드된 파일들을 Nginx로 서빙
|
|
||||||
# 경량의 공식 Nginx 이미지 사용
|
|
||||||
FROM nginx:1.27-alpine
|
|
||||||
|
|
||||||
# 기본 Nginx 설정 파일 제거
|
|
||||||
RUN rm /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
# 사용자 정의 Nginx 설정 (선택 사항이지만 라우팅 등을 위해 권장)
|
|
||||||
COPY frontend/nginx.conf /etc/nginx/conf.d/default.conf
|
|
||||||
|
|
||||||
# 빌더 스테이지에서 빌드된 웹 파일들을 복사
|
|
||||||
COPY --from=builder /app/build/web /usr/share/nginx/html
|
|
||||||
|
|
||||||
# Nginx 서버를 위해 80번 포트 노출
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
# Nginx를 포그라운드에서 시작
|
|
||||||
CMD ["nginx", "-g", "daemon off;"]
|
|
||||||
@@ -17,11 +17,11 @@ services:
|
|||||||
- CLICKHOUSE_USER=${CLICKHOUSE_USER:-baron}
|
- CLICKHOUSE_USER=${CLICKHOUSE_USER:-baron}
|
||||||
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-password}
|
- CLICKHOUSE_PASSWORD=${CLICKHOUSE_PASSWORD:-password}
|
||||||
ports:
|
ports:
|
||||||
- "${BACKEND_PORT:-3000}:3000"
|
- "${BACKEND_PORT:-3010}:3010"
|
||||||
depends_on:
|
depends_on:
|
||||||
- infra_check
|
- infra_check
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
|
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3010/health"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 3
|
retries: 3
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: frontend
|
|||||||
description: "A new Flutter project."
|
description: "A new Flutter project."
|
||||||
# The following line prevents the package from being accidentally published to
|
# The following line prevents the package from being accidentally published to
|
||||||
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
# pub.dev using `flutter pub publish`. This is preferred for private packages.
|
||||||
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
publish_to: "none" # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
# The following defines the version and build number for your application.
|
# The following defines the version and build number for your application.
|
||||||
# A version number is three numbers separated by dots, like 1.2.43
|
# A version number is three numbers separated by dots, like 1.2.43
|
||||||
@@ -62,15 +62,14 @@ dev_dependencies:
|
|||||||
|
|
||||||
# The following section is specific to Flutter packages.
|
# The following section is specific to Flutter packages.
|
||||||
flutter:
|
flutter:
|
||||||
|
|
||||||
# The following line ensures that the Material Icons font is
|
# The following line ensures that the Material Icons font is
|
||||||
# included with your application, so that you can use the icons in
|
# included with your application, so that you can use the icons in
|
||||||
# the material Icons class.
|
# the material Icons class.
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|
||||||
# To add assets to your application, add an assets section, like this:
|
# To add assets to your application, add an assets section, like this:
|
||||||
assets:
|
# assets:
|
||||||
- .env
|
# - .env
|
||||||
|
|
||||||
# An image asset can refer to one or more resolution-specific "variants", see
|
# An image asset can refer to one or more resolution-specific "variants", see
|
||||||
# https://flutter.dev/to/resolution-aware-images
|
# https://flutter.dev/to/resolution-aware-images
|
||||||
|
|||||||
@@ -5,26 +5,15 @@
|
|||||||
// gestures. You can also use WidgetTester to find child widgets in the widget
|
// gestures. You can also use WidgetTester to find child widgets in the widget
|
||||||
// tree, read text, and verify that the values of widget properties are correct.
|
// tree, read text, and verify that the values of widget properties are correct.
|
||||||
|
|
||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
import 'package:frontend/main.dart';
|
import 'package:frontend/main.dart' show BaronSSOApp;
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets('BaronSSOApp builds', (WidgetTester tester) async {
|
||||||
// Build our app and trigger a frame.
|
// runApp에서 ProviderScope로 감싸서 쓰고 있으니 테스트도 동일하게 감쌈
|
||||||
await tester.pumpWidget(const MyApp());
|
await tester.pumpWidget(const ProviderScope(child: BaronSSOApp()));
|
||||||
|
await tester.pump(); // 한 프레임 더
|
||||||
// Verify that our counter starts at 0.
|
|
||||||
expect(find.text('0'), findsOneWidget);
|
|
||||||
expect(find.text('1'), findsNothing);
|
|
||||||
|
|
||||||
// Tap the '+' icon and trigger a frame.
|
|
||||||
await tester.tap(find.byIcon(Icons.add));
|
|
||||||
await tester.pump();
|
|
||||||
|
|
||||||
// Verify that our counter has incremented.
|
|
||||||
expect(find.text('0'), findsNothing);
|
|
||||||
expect(find.text('1'), findsOneWidget);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user