Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
a5fd763
docs: Add security considerations section to README and mark critical…
mohammad-farrokhnia Jun 1, 2026
3bb1b87
style: Remove redundant godot linter configuration setting
mohammad-farrokhnia Jun 1, 2026
b79f394
refactor: Clean up comments and add migrate-force command to Makefile
mohammad-farrokhnia Jun 1, 2026
e5d4239
docs: Update Swagger schema references to use fully qualified package…
mohammad-farrokhnia Jun 1, 2026
9aa9607
refactor: Standardize PORT environment variable and add version injec…
mohammad-farrokhnia Jun 1, 2026
94155b3
docs: Add configuration documentation for local vs Docker vs producti…
mohammad-farrokhnia Jun 1, 2026
624437d
refactor: Simplify .env.example and fix Docker port configuration
mohammad-farrokhnia Jun 1, 2026
a0f7b31
feat: Add nationalCode field to customer signup flow
mohammad-farrokhnia Jun 2, 2026
62b62ae
refactor: Make ISBN field optional and add database constraint handling
mohammad-farrokhnia Jun 2, 2026
e3a8f1f
refactor: Add PostgreSQL error mapping to RemoveCopies and improve er…
mohammad-farrokhnia Jun 2, 2026
25ab6a3
feat: Add unique constraint and error handling for customer national …
mohammad-farrokhnia Jun 2, 2026
def339d
refactor: Fix error context fields and add employee constraint handling
mohammad-farrokhnia Jun 2, 2026
d7e3d68
refactor: Remove customError alias and standardize error package imports
mohammad-farrokhnia Jun 2, 2026
bcd8598
refactor: Fix SQL parameter count and add constraint error handling t…
mohammad-farrokhnia Jun 2, 2026
02a8407
refactor: Make nationalCode optional in customer signup and change to…
mohammad-farrokhnia Jun 2, 2026
69241bc
refactor: Change customer and employee contact fields to optional poi…
mohammad-farrokhnia Jun 2, 2026
9dbc354
refactor: Add SuperAdmin role to backoffice routes and fix borrow end…
mohammad-farrokhnia Jun 2, 2026
24d4bf7
refactor: Standardize JSON field naming and improve code formatting
mohammad-farrokhnia Jun 2, 2026
6ca7966
fix get customer borrowed books
mohammad-farrokhnia Jun 2, 2026
569f140
refactor: Move borrow validation to repository layer with constraint …
mohammad-farrokhnia Jun 3, 2026
7460114
bugFix: fix incorrect id structure and type inputs in requests
mohammad-farrokhnia Jun 3, 2026
5f7825c
update git ignore to include jetbrains ide files
mohammad-farrokhnia Jun 3, 2026
59569c6
bugFix: fix borrow service test mismatch with new changes in borrow repo
mohammad-farrokhnia Jun 3, 2026
60763c9
bugFix: fix unit test problem
mohammad-farrokhnia Jun 3, 2026
63efd18
bugFix: fix lint and go version mismatch problems
mohammad-farrokhnia Jun 3, 2026
f4311e7
bugFix: update git ci to use latest version of golangci-lint so it wo…
mohammad-farrokhnia Jun 3, 2026
4c3da7b
bugFix: update git ci to use latest version of golangci-lint so it wo…
mohammad-farrokhnia Jun 3, 2026
af7f9ea
bugFix: fix lint problems and update golang ci to newest version
mohammad-farrokhnia Jun 3, 2026
b36af89
bugFix: fix lint problems and update golang ci to newest version
mohammad-farrokhnia Jun 3, 2026
d99e6a3
bugFix: fix lint problems and update golang ci to newest version
mohammad-farrokhnia Jun 3, 2026
1c86e0d
bugFix: fix lint problems and update golang ci to newest version
mohammad-farrokhnia Jun 3, 2026
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
17 changes: 15 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
APP_NAME=library
APP_VERSION=0.1.0
APP_ENV=development
Port=8080

# Application Port
# For local development (make run-api): the port your app listens on
# For Docker (make docker-up): container always listens on 8080 (hardcoded in docker-compose.yml)
PORT=8080

# Docker Host Port (optional, only used by docker-compose)
# Maps host port to container port 8080
# HOST_PORT=8080

DATABASE_URL=postgres://library:secret@localhost:5432/library?sslmode=disable
POSTGRES_USER=library
POSTGRES_PASSWORD=secret
POSTGRES_DB=library

REDIS_URL=redis://localhost:6379
# ⚠️ For local development (make run-api): use localhost:6380
# ⚠️ For Docker (make docker-up): IGNORED - hardcoded to redis://redis:6379 in docker-compose.yml
REDIS_URL=redis://localhost:6380

JWT_SECRET=secret
ACCESS_TOKEN_EXPIRY=30
Expand All @@ -20,6 +30,9 @@ GOOGLE_REDIRECT_URL=http://localhost:8080/api/v1/portal/auth/google/callback

OTP_EXPIRY=1

# Elasticsearch Configuration
# ⚠️ For local development (make run-api): use localhost:9200
# ⚠️ For Docker (make docker-up): IGNORED - hardcoded to http://elasticsearch:9200 in docker-compose.yml
ELASTICSEARCH_URL=http://localhost:9200

REQUEST_TIMEOUT=30s
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ jobs:

- uses: actions/setup-go@v5
with:
go-version: '1.23'
go-version: '1.26'
cache: true

- name: golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v8
with:
version: latest
args: --timeout=5m
Expand All @@ -33,7 +33,7 @@ jobs:

- uses: actions/setup-go@v5
with:
go-version: '1.23'
go-version: '1.26'
cache: true

- name: Download modules
Expand All @@ -56,7 +56,7 @@ jobs:

- uses: actions/setup-go@v5
with:
go-version: '1.23'
go-version: '1.26'
cache: true

- name: Download modules
Expand All @@ -76,7 +76,7 @@ jobs:

- uses: actions/setup-go@v5
with:
go-version: '1.23'
go-version: '1.26'
cache: true

- name: Build all binaries
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,5 @@ go.work.sum
.env

# Editor/IDE
# .idea/
.idea/
.vscode/
53 changes: 29 additions & 24 deletions .golangci.yml
Original file line number Diff line number Diff line change
@@ -1,45 +1,50 @@
version: "2"
run:
timeout: 5m
go: '1.23'
go: '1.26'

linters:
enable:
- errcheck
- gosimple
- govet
- ineffassign
- staticcheck
- unused
- gofmt
- misspell
- revive
- exhaustive
- godot
- noctx
- bodyclose

linters-settings:
godot:
all: false
exclude:
- '^ *@'
revive:
settings:
revive:
rules:
- name: exported
disabled: true
- name: package-comments
disabled: true
- name: redefines-builtin-id
severity: warning
- name: unexported-return
severity: warning
- name: blank-imports
severity: warning
exhaustive:
default-signifies-exhaustive: true
exclusions:
rules:
- name: exported
severity: warning
- name: unused-parameter
severity: warning
- path: _test\.go
linters: [errcheck, noctx]
- path: tests/
linters: [errcheck, noctx, revive]
- path: docs/
linters: [all]
- path: pkg/i18n/fa\.go
linters: [staticcheck]

exhaustive:
default-signifies-exhaustive: true
formatters:
enable:
- gofmt

issues:
exclude-rules:
- path: _test\.go
linters: [errcheck, gosimple]

- path: docs/
linters: [all]

max-issues-per-linter: 0
max-same-issues: 0
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ RUN go mod download

COPY . .

RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o main ./cmd/api
ARG VERSION=dev
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo \
-ldflags "-s -w -X main.Version=${VERSION}" \
-o main ./cmd/api

FROM alpine:latest

Expand Down
14 changes: 10 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ GO_FILES := $(shell find . -type f -name '*.go' -not -path './vendor/*' -not -

VERSION := $(shell git describe --tags --always --dirty 2>/dev/null || echo "dev")
LDFLAGS := -ldflags "-s -w -X main.Version=$(VERSION)"

GO_VERSION=1.26
.PHONY: help run-api build build-all test test-unit test-integration test-coverage \
lint fmt vet tidy docker-up docker-down docker-logs docker-psql \
migrate migrate-down migrate-version seed update-swagger clean
migrate migrate-down migrate-version migrate-force seed update-swagger clean

docker-up:
docker compose up -d --build
VERSION=$(VERSION) docker compose up -d --build

docker-down:
docker compose down
Expand Down Expand Up @@ -40,6 +40,11 @@ migrate-down:
migrate-version:
go run ./cmd/migrate -cmd=version

migrate-force:
@echo "Forcing migration to version $(VERSION)"
@if [ -z "$(VERSION)" ]; then echo "Error: VERSION is required. Usage: make migrate-force VERSION=4"; exit 1; fi
go run ./cmd/migrate -cmd=force -version=$(VERSION)

seed:
go run ./cmd/seed --email=$(EMAIL) --password=$(PASSWORD) --mobile=$(MOBILE) --national-code=$(NATIONAL_CODE)

Expand Down Expand Up @@ -115,11 +120,12 @@ help:
@echo ""
@echo "Database:"
@echo " docker-deps Start postgres, redis, elasticsearch"
@echo " docker-up Start all services with docker compose"
@echo " docker-up Start all services with docker compose (builds with version $(VERSION))"
@echo " docker-down Stop all services"
@echo " migrate Run migrations up"
@echo " migrate-down Run migrations down (1 step)"
@echo " migrate-version Show migration version"
@echo " migrate-force Force migration to version (usage: make migrate-force VERSION=4)"
@echo " seed Seed super admin user"
@echo ""
@echo "Other:"
Expand Down
92 changes: 86 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ git clone https://github.com/YOUR_USERNAME/librecore.git
cd librecore

cp .env.example .env
# Edit .env if needed (see Configuration section below)

docker compose up -d

Expand All @@ -104,6 +105,30 @@ make run
The API will be available at `http://localhost:8080`.
Swagger UI at `http://localhost:8080/docs`.

### Configuration

The `.env` file contains different connection strings depending on how you run the application:

**For local development (`make run-api`):**
- Use `localhost` with host-mapped ports
- Redis: `redis://localhost:6380` (Docker maps 6380→6379)
- Elasticsearch: `http://localhost:9200`

**For Docker (`make docker-up`):**
- Environment variables are automatically overridden in `docker-compose.yml`
- Redis: `redis://redis:6379` (internal Docker network)
- Elasticsearch: `http://elasticsearch:9200` (internal Docker network)

**For production:**
- Point to your managed services (AWS ElastiCache, Elastic Cloud, etc.)
- Set environment variables to override defaults:
```bash
export REDIS_URL=redis://your-redis-cluster.amazonaws.com:6379
export ELASTICSEARCH_URL=https://your-es-cluster.elastic-cloud.com:9200
docker compose up -d
```
- Or update your production `.env` file directly with external service URLs

### Makefile Commands

```bash
Expand All @@ -120,21 +145,27 @@ make lint # run golangci-lint
The API binary embeds version information at build time using git tags or commit hash:

```bash
# Build with version (uses git tag or commit hash)
# Build locally with version (uses git tag or commit hash)
make build-api

# Build and run with Docker (automatically injects version)
make docker-up

# The version is injected at compile time and logged on startup:
# [library] listening on :8080 env=development version=1.2.3
# [library] listening on :8080 env=development version=e5d4239
```

For manual builds without the Makefile:
For manual builds:

```bash
# Build with version from git
# Local build with version from git
go build -ldflags "-X main.Version=$(git describe --tags --always --dirty)" -o bin/library ./cmd/api

# Or use a specific version
go build -ldflags "-X main.Version=1.2.3" -o bin/library ./cmd/api
# Docker build with specific version
docker build --build-arg VERSION=1.2.3 -t library-api .

# Docker Compose with custom version
VERSION=1.2.3 docker compose up -d --build
```

---
Expand Down Expand Up @@ -201,7 +232,56 @@ Request → Middleware (auth, logger) → Handler → Service → Repository →
Each layer has one responsibility and only communicates with the layer directly below it.
Business rules live exclusively in `service/`. Handlers never touch the database.

---

## Security Considerations & Known Limitations

⚠️ **This project is for educational/portfolio purposes. Before deploying to production, address the following:**

### 🔴 Critical Security Issues

1. **Google OAuth State Parameter (CSRF Vulnerability)**
- **Issue**: The `GoogleCallback` handler does not verify the OAuth `state` parameter
- **Risk**: Attackers can forge OAuth callbacks and potentially hijack authentication flows
- **Fix Required**: Store state in Redis with 5-minute TTL before redirecting to Google, then verify it in the callback:
```go
// Before redirect
state := generateRandomState()
rdb.Set(ctx, "oauth:state:"+state, userID, 5*time.Minute)

// In callback
storedUserID := rdb.Get(ctx, "oauth:state:"+state)
if storedUserID == "" {
return errors.New("invalid state")
}
```

2. **Hardcoded OTP for Password Reset**
- **Issue**: Email OTP verification is not implemented; OTPs are logged but not sent
- **Risk**: Password reset flow is non-functional in production
- **Fix Required**: Integrate a real email service (SendGrid, AWS SES, etc.) in `pkg/mailer`

### 🟡 Data Integrity Limitations

3. **No Soft Deletes**
- **Issue**: Deleting customers/employees permanently removes records and orphans borrow history
- **Impact**: Historical data is lost; reports become inaccurate
- **Recommendation**: Add `deleted_at TIMESTAMPTZ` columns and filter with `WHERE deleted_at IS NULL`

4. **Elasticsearch Sync Has No Recovery**
- **Issue**: Book indexing is fire-and-forget; if the API crashes between DB write and ES index, search results become stale
- **Impact**: Search index can drift from database state
- **Proper Solution**: Implement an outbox pattern with `es_sync_queue` table and background worker
- **Current Workaround**: Manually reindex via admin script if drift is detected

### ✅ Already Implemented Correctly

- **Unique Constraint Checking**: Uses pgx error codes (`23505`) instead of fragile string matching
- **JWT Security**: RS256 with proper key rotation support
- **SQL Injection Protection**: All queries use parameterized statements
- **Password Hashing**: bcrypt with cost factor 12

---

## License

Expand Down
4 changes: 2 additions & 2 deletions cmd/api/backoffice_router.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ func registerBackofficeRoutes(api *gin.RouterGroup, deps *Dependencies) {
{
registerAuthRoutes(apiV1BackOffice, deps)
apiV1BackOffice.Use(middleware.RequireAuth(deps.AuthService, deps.SessionStore, domain.SubjectTypeEmployee))
apiV1BackOffice.Use(middleware.RequireRole(domain.RoleManager))
apiV1BackOffice.Use(middleware.RequireRole(domain.RoleManager, domain.RoleSuperAdmin))
registerBookRoutes(apiV1BackOffice, deps)
registerCustomerRoutes(apiV1BackOffice, deps)
registerEmployeeRoutes(apiV1BackOffice, deps)
Expand Down Expand Up @@ -84,7 +84,7 @@ func registerBorrowRoutes(api *gin.RouterGroup, deps *Dependencies) {
)
borrows.POST("", handler.Borrow)
borrows.PUT("/:id/return", handler.Return)
borrows.GET("/:id/customers/:id", handler.ListByCustomer)
borrows.GET("/customers/:id", handler.ListByCustomer)
}
}

Expand Down
Loading
Loading