Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@
go.work

# End of https://www.toptal.com/developers/gitignore/api/go,dotenv
.gocache
4 changes: 4 additions & 0 deletions .idea/customer-service.iml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions .idea/go.imports.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions .idea/material_theme_project_new.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions .idea/vcs.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

111 changes: 111 additions & 0 deletions .idea/workspace.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

64 changes: 51 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ It uses **PostgreSQL** as the database, **RabbitMQ** for asynchronous communicat
## 🚀 Features

- Management of **companies** (`companies`)
- Management of **users** (`users`)
- Management of **users** (`users-company`)
- Lookup of company users by **Keycloak ID**
- Global **CORS** support with `OPTIONS` preflight handling
- Publishing RabbitMQ events (`CompanyCreated`, `UserCreated`)
- Modular and extensible microservice architecture
- Persistence via PostgreSQL
Expand All @@ -19,10 +21,13 @@ It uses **PostgreSQL** as the database, **RabbitMQ** for asynchronous communicat
├── config/
│ └── config.go # Configuration & env loading
├── internal/
│ ├── customer/
│ │ ├── company/ # "Company" business logic
│ │ └── user/ # "User" business logic
│ ├── company/ # "Company" business logic
│ ├── user/ # "User" business logic
│ └── event/ # RabbitMQ publishing
├── docs/
│ ├── API.md # Route-by-route API reference
│ └── available-keycloak-users-contract.md
│ # Proposed dropdown contract for frontend integration
├── routes/
│ └── route.go # Application routes
├── docker-compose.yml # Full stack (Go + Postgres + RabbitMQ)
Expand All @@ -45,6 +50,8 @@ Create a `.env` file at the root of the project and fill in the variables:
cp .env.example .env
```

The service only needs database and RabbitMQ configuration from `.env`.

## 🐳 Running the project

From the root of the project:
Expand All @@ -57,31 +64,62 @@ docker compose up --build

📋 API (via Postman, curl, etc.)

| Method | Endpoint | Description |
| ------ | ---------------------- | --------------------------------- |
| POST | `/companies` | Create a company |
| GET | `/companies/:id` | Retrieve a company |
| POST | `/companies/:id/users` | Create a user linked to a company |
| GET | `/users/:id` | Retrieve a user |
| Method | Endpoint | Description |
| ------ | -------- | ----------- |
| GET | `/health` | Health check |
| POST | `/companies` | Create a company |
| GET | `/companies` | List companies |
| GET | `/companies/:id` | Retrieve a company |
| PUT | `/companies/:id` | Update a company |
| DELETE | `/companies/:id` | Delete a company |
| POST | `/companies/:id/users-company` | Create a user linked to a company |
| GET | `/companies/:id/users-company` | List users linked to a company |
| GET | `/users-company/keycloak/:keycloak_id` | Retrieve a user by Keycloak ID |
| GET | `/users-company/keycloak/:keycloak_id/role` | Retrieve only the user role by Keycloak ID |
| GET | `/users-company/keycloak/:keycloak_id/company-id` | Retrieve only the company ID by Keycloak ID |
| GET | `/users-company/:id` | Retrieve a user |
| PUT | `/users-company/:id` | Update a user |
| DELETE | `/users-company/:id` | Delete a user |

Full route-by-route documentation with direct URLs and `curl` examples is available in [`docs/API.md`](docs/API.md).

The proposed backend contract for the frontend "available Keycloak users" dropdown is documented in [`docs/available-keycloak-users-contract.md`](docs/available-keycloak-users-contract.md).

## 🌐 CORS

> Sample CURLs are available in each resource-specific `repository`.
The API exposes global CORS headers for browser clients:

- `Access-Control-Allow-Origin: *`
- `Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS`
- `Access-Control-Allow-Headers: Origin, Content-Type, Accept, Authorization`
- `OPTIONS` preflight requests return `204 No Content`

## 🗄️ Accessing PostgreSQL

From your terminal:

```bash
psql -h localhost -p 5433 -U user -d customers
psql -h localhost -p 5434 -U user -d customer_service_db
```

Then:

```sql
\dt -- List tables
SELECT * FROM companies;
SELECT * FROM users;
SELECT * FROM user_companies;
```

## Create User
Please run this create for adding your keycloak account to an admin account.
```bash
curl -X POST http://localhost:8080/companies/1/users-company \
-H "Content-Type: application/json" \
-d '{
"id_auth_kc": "08d697a6-8b89-40fc-aaf3-bdaaa65e0ae4",
"role": "admin"
}'
```
## 🧹 Cleanup

To stop and remove containers:
Expand Down
6 changes: 5 additions & 1 deletion cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import (
)

func main() {
cfg := config.LoadConfig()
cfg, err := config.LoadConfig()
if err != nil {
log.Fatal("Invalid configuration:", err)
}

// PostgreSQL connection
log.Println("Connecting to Postgres with DSN:", cfg.PostgresDSN())
Expand Down Expand Up @@ -76,6 +79,7 @@ func main() {

// Router
r := gin.Default()
r.Use(routes.WithConfig(cfg), routes.CORSMiddleware())

// Fetch trusted proxies from .env
proxies := os.Getenv("TRUSTED_PROXIES")
Expand Down
28 changes: 23 additions & 5 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ package config

import (
"fmt"
"log"
"os"
"strings"
)

type Config struct {
Expand All @@ -14,7 +14,7 @@ type Config struct {
RabbitMQ string
}

func LoadConfig() *Config {
func LoadConfig() (*Config, error) {
cfg := &Config{
DBHost: os.Getenv("DB_HOST"),
DBUser: os.Getenv("DB_USER"),
Expand All @@ -23,11 +23,29 @@ func LoadConfig() *Config {
RabbitMQ: os.Getenv("RABBITMQ_HOST"),
}

if cfg.DBHost == "" || cfg.DBUser == "" || cfg.DBPassword == "" || cfg.DBName == "" || cfg.RabbitMQ == "" {
log.Fatal("Missing required environment variables")
var missing []string
requiredValues := []struct {
name string
value string
}{
{name: "DB_HOST", value: cfg.DBHost},
{name: "DB_USER", value: cfg.DBUser},
{name: "DB_PASSWORD", value: cfg.DBPassword},
{name: "DB_NAME", value: cfg.DBName},
{name: "RABBITMQ_HOST", value: cfg.RabbitMQ},
}

return cfg
for _, item := range requiredValues {
if item.value == "" {
missing = append(missing, item.name)
}
}

if len(missing) > 0 {
return nil, fmt.Errorf("missing required environment variables: %s", strings.Join(missing, ", "))
}

return cfg, nil
}

func (c *Config) PostgresDSN() string {
Expand Down
39 changes: 39 additions & 0 deletions config/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package config

import (
"strings"
"testing"
)

func setRequiredEnv(t *testing.T) {
t.Helper()

t.Setenv("DB_HOST", "localhost")
t.Setenv("DB_USER", "user")
t.Setenv("DB_PASSWORD", "secret")
t.Setenv("DB_NAME", "customer_service_db")
t.Setenv("RABBITMQ_HOST", "rabbitmq")
}

func TestLoadConfig_Success(t *testing.T) {
setRequiredEnv(t)

_, err := LoadConfig()
if err != nil {
t.Fatalf("LoadConfig returned error: %v", err)
}
}

func TestLoadConfig_MissingRequiredEnv(t *testing.T) {
setRequiredEnv(t)
t.Setenv("RABBITMQ_HOST", "")

_, err := LoadConfig()
if err == nil {
t.Fatal("expected LoadConfig to fail when a required env var is missing")
}

if !strings.Contains(err.Error(), "RABBITMQ_HOST") {
t.Fatalf("expected error to mention RABBITMQ_HOST, got %v", err)
}
}
4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ services:
env_file:
- .env
ports:
- "5433:5432"
- "5434:5432"
volumes:
- ./scripts:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER"]
test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"]
interval: 5s
timeout: 5s
retries: 5
Expand Down
Loading
Loading