Compare commits

..

43 Commits

Author SHA1 Message Date
3a98b72ffd Merge pull request 'feature/epic2-core-services' (#6) from feature/epic2-core-services into main
All checks were successful
CI / Test (push) Successful in 3m38s
CI / Lint (push) Successful in 54s
CI / Build (push) Successful in 45s
CI / Format Check (push) Successful in 2s
Reviewed-on: #6
2025-11-07 10:23:19 +01:00
a785cd73de perf(ci): pre-install all tools in pre-commit Docker image
All checks were successful
CI / Test (pull_request) Successful in 53s
CI / Build (pull_request) Successful in 38s
CI / Format Check (pull_request) Successful in 2s
CI / Lint (pull_request) Successful in 26s
- Move Go 1.25.3 installation to Dockerfile
- Pre-install protobuf plugins and golangci-lint in image
- Set environment variables in Dockerfile
- Remove runtime installation steps from pre-commit script
- Significantly improves pre-commit check performance
2025-11-07 10:16:12 +01:00
75c5293c8c fix(lint): add bounds checking for integer conversions to fix gosec warnings
All checks were successful
CI / Test (pull_request) Successful in 57s
CI / Lint (pull_request) Successful in 27s
CI / Build (pull_request) Successful in 39s
CI / Format Check (pull_request) Successful in 2s
- Add bounds checking for Limit and Offset conversions in audit_client.go
- Add bounds checking for t, m, and p conversions in password.go
- Add nolint comments with explanations for safe conversions
2025-11-07 10:04:14 +01:00
9712e50f6c fix(scripts): disable VCS stamping in pre-commit container 2025-11-07 09:55:24 +01:00
8eda9af769 fix(scripts): ensure working directory is correct for make commands 2025-11-07 09:54:00 +01:00
4b33ed522d fix(scripts): install make in pre-commit container 2025-11-07 09:52:45 +01:00
bc740f7b1f fix(scripts): use make fmt-check instead of go fmt 2025-11-07 09:52:17 +01:00
e98e4d3099 fix(scripts): use make fmt-check in pre-commit script 2025-11-07 09:51:23 +01:00
93623e6865 fix(scripts): install Go 1.25.3 in pre-commit container and fix formatting 2025-11-07 09:47:50 +01:00
b531f92436 feat(ci): add pre-commit check script using wirelos/pre-commit image 2025-11-07 09:46:35 +01:00
31e8ca7ce9 fix(lint): use explicit safe type conversions for gosec
Some checks failed
CI / Test (pull_request) Successful in 51s
CI / Lint (pull_request) Failing after 26s
CI / Build (pull_request) Successful in 38s
CI / Format Check (pull_request) Failing after 2s
Use separate variables with explicit else branches to make type
conversions safe and satisfy gosec integer overflow checks.
2025-11-07 09:37:53 +01:00
e673fcae6f fix(lint): fix all linting errors
Some checks failed
CI / Test (pull_request) Successful in 53s
CI / Lint (pull_request) Failing after 26s
CI / Build (pull_request) Successful in 39s
CI / Format Check (pull_request) Failing after 2s
- Check BindEnv return values in config.go
- Add bounds checks for int->int32/uint32 conversions to prevent overflow
- Remove unused test helper functions
2025-11-07 09:34:38 +01:00
131e44f3d4 fix(ci): install latest golangci-lint using official script
Some checks failed
CI / Test (pull_request) Successful in 52s
CI / Lint (pull_request) Failing after 26s
CI / Build (pull_request) Successful in 38s
CI / Format Check (pull_request) Successful in 2s
2025-11-07 09:29:49 +01:00
0d6094267a fix(ci): install golangci-lint v2.4.0 for Go 1.25 and config v2 support 2025-11-07 09:27:27 +01:00
4b8536e34a fix(ci): use official install script for golangci-lint 2025-11-07 09:27:17 +01:00
e509faea25 fix(ci): install golangci-lint v2 for config compatibility
Some checks failed
CI / Test (pull_request) Successful in 51s
CI / Build (pull_request) Successful in 37s
CI / Lint (pull_request) Failing after 5s
CI / Format Check (pull_request) Successful in 3s
2025-11-07 09:27:02 +01:00
355008a3a2 fix(ci): build golangci-lint from source for Go 1.25 support
Some checks failed
CI / Build (pull_request) Successful in 37s
CI / Test (pull_request) Successful in 51s
CI / Lint (pull_request) Failing after 5s
CI / Format Check (pull_request) Successful in 2s
2025-11-07 09:24:58 +01:00
c8d944e9ea fix(ci): let golangci-lint-action auto-select compatible version
Some checks failed
CI / Test (pull_request) Successful in 52s
CI / Lint (pull_request) Failing after 7s
CI / Build (pull_request) Successful in 37s
CI / Format Check (pull_request) Successful in 2s
2025-11-07 09:22:52 +01:00
d24bb96d62 fix(ci): use golangci-lint v1.64.0 2025-11-07 09:22:33 +01:00
61d614690f fix(ci): use golangci-lint v1.65.0 for Go 1.25 support
Some checks failed
CI / Test (pull_request) Successful in 52s
CI / Lint (pull_request) Failing after 6s
CI / Build (pull_request) Successful in 37s
CI / Format Check (pull_request) Successful in 2s
2025-11-07 09:19:54 +01:00
5f2e1104f2 fix(ci): use golangci-lint latest version
Some checks failed
CI / Test (pull_request) Successful in 52s
CI / Lint (pull_request) Failing after 6s
CI / Build (pull_request) Successful in 37s
CI / Format Check (pull_request) Successful in 2s
2025-11-07 09:17:22 +01:00
7c0aefb7f4 fix(fmt): formatting 2025-11-07 09:10:22 +01:00
8cfdfbc951 fix(schema): remove duplicate auditlog.go schema 2025-11-07 09:04:05 +01:00
0912f0f81b feat(schema): restore complete Ent schema files 2025-11-07 09:03:43 +01:00
1f8c2626dc fix(gitignore): allow ent/schema/ directory 2025-11-07 08:42:06 +01:00
fb10051443 fix(ci): generate Ent from ent/schema and copy to internal/ent 2025-11-07 08:41:03 +01:00
8bb36b5735 fix(ci): correctly copy Ent files excluding schema directory 2025-11-07 08:39:43 +01:00
837b04b433 fix(ci): use find to copy all Ent generated files 2025-11-07 08:39:26 +01:00
868649d6d2 fix(ci): add debug output to Ent generation step 2025-11-07 08:38:22 +01:00
13da884a21 fix(ci): exclude generate.go from Ent code copy 2025-11-07 08:21:40 +01:00
c2e2ab01f2 fix(ci): generate Ent code and copy to internal/ent 2025-11-07 08:21:31 +01:00
8c10c3dba9 fix(ci): use full module path for Ent target directory 2025-11-07 08:20:56 +01:00
b6eb8d75bb fix(ci): generate Ent code to internal/ent directory 2025-11-07 08:17:32 +01:00
4c62817cff fix(ci): revert to use apk for Alpine runner 2025-11-06 22:49:45 +01:00
0edeb67075 test: add comprehensive tests and fix CI build 2025-11-06 22:49:13 +01:00
b3c8f68989 fix(ci): update to use Makefile commands 2025-11-06 22:42:17 +01:00
3f18163313 fix(gitignore): exclude generated protobuf and Ent files 2025-11-06 22:39:55 +01:00
d42b1cd5f1 fix(proto): fix protobuf generation and update gateway tests 2025-11-06 22:39:43 +01:00
471a057d25 fix(ci): fix CI build and update Makefile to build all services 2025-11-06 22:34:49 +01:00
ad4ecaed1f fix(ci): update to use Alpine package manager (apk) 2025-11-06 22:30:39 +01:00
6ce1007f73 fix(ci): update CI to generate protobuf and Ent ORM files 2025-11-06 22:26:54 +01:00
4e6db9995f fix(gitignore): remove generated files from git tracking 2025-11-06 22:15:08 +01:00
dbe29bfb82 fix(consul): fix gRPC health checks and add API Gateway Consul registration 2025-11-06 22:04:55 +01:00
30 changed files with 870 additions and 91 deletions

View File

@@ -43,12 +43,20 @@ jobs:
- name: Generate code
run: |
make generate-proto
if [ -d "ent" ] && [ -f "ent/generate.go" ]; then
echo "Checking for Ent schema directory..."
if [ -d "ent/schema" ]; then
echo "Generating Ent code..."
go install entgo.io/ent/cmd/ent@latest
cd ent && go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
mkdir -p ../internal/ent
cd ent/schema && go run -mod=mod entgo.io/ent/cmd/ent generate .
echo "Copying Ent code to internal/ent..."
cd .. && mkdir -p ../internal/ent
cp -r *.go */ ../internal/ent/ 2>/dev/null || true
rm -f ../internal/ent/generate.go
rm -rf ../internal/ent/schema
echo "Verifying internal/ent/ent.go exists..."
ls -la ../internal/ent/ent.go || echo "ERROR: ent.go not found!"
else
echo "WARNING: ent/schema directory not found!"
fi
- name: Check for test files
@@ -107,18 +115,27 @@ jobs:
- name: Generate code
run: |
make generate-proto
if [ -d "ent" ] && [ -f "ent/generate.go" ]; then
echo "Checking for Ent schema directory..."
if [ -d "ent/schema" ]; then
echo "Generating Ent code..."
go install entgo.io/ent/cmd/ent@latest
cd ent && go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
mkdir -p ../internal/ent
cd ent/schema && go run -mod=mod entgo.io/ent/cmd/ent generate .
echo "Copying Ent code to internal/ent..."
cd .. && mkdir -p ../internal/ent
cp -r *.go */ ../internal/ent/ 2>/dev/null || true
rm -f ../internal/ent/generate.go
rm -rf ../internal/ent/schema
echo "Verifying internal/ent/ent.go exists..."
ls -la ../internal/ent/ent.go || echo "ERROR: ent.go not found!"
else
echo "WARNING: ent/schema directory not found!"
fi
- name: Install golangci-lint
run: |
go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
echo "$HOME/go/bin" >> $GITHUB_PATH
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin
echo "$(go env GOPATH)/bin" >> $GITHUB_PATH
golangci-lint --version
- name: Run linters
run: make lint
@@ -156,12 +173,20 @@ jobs:
- name: Generate code
run: |
make generate-proto
if [ -d "ent" ] && [ -f "ent/generate.go" ]; then
echo "Checking for Ent schema directory..."
if [ -d "ent/schema" ]; then
echo "Generating Ent code..."
go install entgo.io/ent/cmd/ent@latest
cd ent && go run -mod=mod entgo.io/ent/cmd/ent generate ./schema
mkdir -p ../internal/ent
cd ent/schema && go run -mod=mod entgo.io/ent/cmd/ent generate .
echo "Copying Ent code to internal/ent..."
cd .. && mkdir -p ../internal/ent
cp -r *.go */ ../internal/ent/ 2>/dev/null || true
rm -f ../internal/ent/generate.go
rm -rf ../internal/ent/schema
echo "Verifying internal/ent/ent.go exists..."
ls -la ../internal/ent/ent.go || echo "ERROR: ent.go not found!"
else
echo "WARNING: ent/schema directory not found!"
fi
- name: Build

6
.gitignore vendored
View File

@@ -69,7 +69,9 @@ Thumbs.db
# Generated protobuf files
api/proto/generated/
# Generated Ent ORM files
# Generated Ent ORM files (but keep schema source files)
internal/ent/
ent/
ent/*.go
ent/*/
!ent/schema/
git.dcentral.systems/

View File

@@ -197,10 +197,14 @@ When working on this project, follow this workflow:
### 7. Commit Changes
- **ALWAYS commit** after successful implementation
- Ensure the code builds (`go build`)
- Ensure all tests pass (`go test`)
- Ensure there are no linter issues (`make lint`)
- Ensure there are no fmt issues (`make fmt-check`)
- Verify that everything is in order before commit:
- there is a Gitea Runner image in ci/pre-commit
- run scripts/pre-commit-check.sh
- Ensure the code builds (`make build`)
- Ensure all tests pass (`make test`)
- Ensure there are no linter issues (`make lint`)
- Ensure there are no fmt issues (`make fmt-check`)
- If there are issues, fix them before comitting
- Verify all acceptance criteria are met
- Write a clear, descriptive commit message

40
ci/pre-commit/Dockerfile Normal file
View File

@@ -0,0 +1,40 @@
FROM alpine:latest
# Install system dependencies
RUN apk add --no-cache \
nodejs \
npm \
gcc \
build-base \
musl-dev \
curl \
make \
wget \
tar \
bash \
git \
protobuf \
protobuf-dev
# Install Go 1.25.3
RUN cd /tmp && \
wget -q https://go.dev/dl/go1.25.3.linux-amd64.tar.gz && \
tar -C /usr/local -xzf go1.25.3.linux-amd64.tar.gz && \
rm go1.25.3.linux-amd64.tar.gz
# Set up Go environment
ENV PATH=/usr/local/go/bin:$PATH:/root/go/bin
ENV GOROOT=/usr/local/go
ENV GOPATH=/root/go
ENV CGO_ENABLED=1
ENV GOFLAGS=-buildvcs=false
# Install Go protobuf plugins
RUN go install google.golang.org/protobuf/cmd/protoc-gen-go@latest && \
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# Install golangci-lint
RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /root/go/bin
# Set working directory
WORKDIR /workspace

View File

@@ -5,6 +5,7 @@ package main
import (
"context"
"fmt"
"math"
"net"
"time"
@@ -275,9 +276,16 @@ func (s *auditServerImpl) Query(ctx context.Context, req *auditv1.QueryRequest)
})
}
total := len(protoEntries)
var totalInt32 int32
if total > math.MaxInt32 {
totalInt32 = math.MaxInt32
} else {
totalInt32 = int32(total)
}
return &auditv1.QueryResponse{
Entries: protoEntries,
Total: int32(len(protoEntries)),
Total: totalInt32,
}, nil
}

View File

@@ -8,6 +8,7 @@ import (
"crypto/subtle"
"encoding/base64"
"fmt"
"math"
"net"
"strings"
"time"
@@ -72,7 +73,17 @@ func verifyPassword(password, hash string) (bool, error) {
if err != nil {
return false, err
}
actualHash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, uint32(len(expectedHash)))
hashLen := len(expectedHash)
if hashLen < 0 || hashLen > math.MaxUint32 {
return false, fmt.Errorf("invalid hash length: %d", hashLen)
}
var hashLenUint32 uint32
if hashLen > math.MaxUint32 {
hashLenUint32 = math.MaxUint32
} else {
hashLenUint32 = uint32(hashLen)
}
actualHash := argon2.IDKey([]byte(password), salt, 3, 64*1024, 4, hashLenUint32)
return subtle.ConstantTimeCompare(expectedHash, actualHash) == 1, nil
}

59
ent/schema/audit_log.go Normal file
View File

@@ -0,0 +1,59 @@
// Package schema defines the Ent schema for domain entities.
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)
// AuditLog holds the schema definition for the AuditLog entity.
type AuditLog struct {
ent.Schema
}
// Fields of the AuditLog.
func (AuditLog) Fields() []ent.Field {
return []ent.Field{
field.String("id").
Unique().
Immutable(),
field.String("user_id").
NotEmpty().
Comment("ID of the user/actor performing the action"),
field.String("action").
NotEmpty().
Comment("Action performed (e.g., user.create, user.update)"),
field.String("resource").
Optional().
Comment("Resource type (e.g., user, role)"),
field.String("resource_id").
Optional().
Comment("ID of the target resource"),
field.String("ip_address").
Optional().
Comment("IP address of the client"),
field.String("user_agent").
Optional().
Comment("User agent of the client"),
field.JSON("metadata", map[string]interface{}{}).
Optional().
Comment("Additional metadata as JSON"),
field.Time("timestamp").
Default(time.Now).
Immutable(),
}
}
// Indexes of the AuditLog.
func (AuditLog) Indexes() []ent.Index {
return []ent.Index{
index.Fields("user_id"),
index.Fields("resource_id"),
index.Fields("timestamp"),
index.Fields("action"),
index.Fields("resource"),
}
}

32
ent/schema/permission.go Normal file
View File

@@ -0,0 +1,32 @@
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)
// Permission holds the schema definition for the Permission entity.
type Permission struct {
ent.Schema
}
// Fields of the Permission.
func (Permission) Fields() []ent.Field {
return []ent.Field{
field.String("id").
Unique().
Immutable(),
field.String("name").
Unique().
NotEmpty().
Comment("Format: module.resource.action"),
}
}
// Edges of the Permission.
func (Permission) Edges() []ent.Edge {
return []ent.Edge{
edge.To("role_permissions", RolePermission.Type),
}
}

View File

@@ -0,0 +1,44 @@
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)
// RefreshToken holds the schema definition for the RefreshToken entity.
type RefreshToken struct {
ent.Schema
}
// Fields of the RefreshToken.
func (RefreshToken) Fields() []ent.Field {
return []ent.Field{
field.String("id").
Unique().
Immutable(),
field.String("user_id").
NotEmpty().
Comment("ID of the user who owns this refresh token"),
field.String("token_hash").
NotEmpty().
Sensitive().
Comment("SHA256 hash of the refresh token"),
field.Time("expires_at").
Comment("When the refresh token expires"),
field.Time("created_at").
Default(time.Now).
Immutable(),
}
}
// Indexes of the RefreshToken.
func (RefreshToken) Indexes() []ent.Index {
return []ent.Index{
index.Fields("user_id"),
index.Fields("token_hash"),
index.Fields("expires_at"),
}
}

39
ent/schema/role.go Normal file
View File

@@ -0,0 +1,39 @@
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)
// Role holds the schema definition for the Role entity.
type Role struct {
ent.Schema
}
// Fields of the Role.
func (Role) Fields() []ent.Field {
return []ent.Field{
field.String("id").
Unique().
Immutable(),
field.String("name").
Unique().
NotEmpty(),
field.String("description").
Optional(),
field.Time("created_at").
Default(time.Now).
Immutable(),
}
}
// Edges of the Role.
func (Role) Edges() []ent.Edge {
return []ent.Edge{
edge.To("role_permissions", RolePermission.Type),
edge.To("user_roles", UserRole.Type),
}
}

View File

@@ -0,0 +1,34 @@
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)
// RolePermission holds the schema definition for the RolePermission entity (many-to-many relationship).
type RolePermission struct {
ent.Schema
}
// Fields of the RolePermission.
func (RolePermission) Fields() []ent.Field {
return []ent.Field{
field.String("role_id"),
field.String("permission_id"),
}
}
// Edges of the RolePermission.
func (RolePermission) Edges() []ent.Edge {
return []ent.Edge{
edge.To("role", Role.Type).
Unique().
Required().
Field("role_id"),
edge.To("permission", Permission.Type).
Unique().
Required().
Field("permission_id"),
}
}

57
ent/schema/user.go Normal file
View File

@@ -0,0 +1,57 @@
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)
// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("id").
Unique().
Immutable(),
field.String("email").
Unique().
NotEmpty(),
field.String("username").
Optional(),
field.String("first_name").
Optional(),
field.String("last_name").
Optional(),
field.String("password_hash").
NotEmpty(),
field.Bool("verified").
Default(false),
field.String("email_verification_token").
Optional().
Sensitive(),
field.String("password_reset_token").
Optional().
Sensitive(),
field.Time("password_reset_expires_at").
Optional(),
field.Time("created_at").
Default(time.Now).
Immutable(),
field.Time("updated_at").
Default(time.Now).
UpdateDefault(time.Now),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("user_roles", UserRole.Type),
}
}

34
ent/schema/user_role.go Normal file
View File

@@ -0,0 +1,34 @@
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)
// UserRole holds the schema definition for the UserRole entity (many-to-many relationship).
type UserRole struct {
ent.Schema
}
// Fields of the UserRole.
func (UserRole) Fields() []ent.Field {
return []ent.Field{
field.String("user_id"),
field.String("role_id"),
}
}
// Edges of the UserRole.
func (UserRole) Edges() []ent.Edge {
return []ent.Edge{
edge.To("user", User.Type).
Unique().
Required().
Field("user_id"),
edge.To("role", Role.Type).
Unique().
Required().
Field("role_id"),
}
}

View File

@@ -4,6 +4,7 @@ package grpc
import (
"context"
"fmt"
"math"
auditv1 "git.dcentral.systems/toolz/goplt/api/proto/generated/audit/v1"
"git.dcentral.systems/toolz/goplt/pkg/registry"
@@ -86,9 +87,24 @@ func (c *AuditClient) Query(ctx context.Context, filters *services.AuditLogFilte
return nil, err
}
var limitInt32, offsetInt32 int32
if filters.Limit > math.MaxInt32 {
limitInt32 = math.MaxInt32
} else if filters.Limit < math.MinInt32 {
limitInt32 = math.MinInt32
} else {
limitInt32 = int32(filters.Limit) //nolint:gosec // bounds checked above
}
if filters.Offset > math.MaxInt32 {
offsetInt32 = math.MaxInt32
} else if filters.Offset < math.MinInt32 {
offsetInt32 = math.MinInt32
} else {
offsetInt32 = int32(filters.Offset) //nolint:gosec // bounds checked above
}
req := &auditv1.QueryRequest{
Limit: int32(filters.Limit),
Offset: int32(filters.Offset),
Limit: limitInt32,
Offset: offsetInt32,
}
if filters.UserID != nil {

View File

@@ -204,7 +204,7 @@ func (c *IdentityClient) VerifyPassword(ctx context.Context, email, password str
resp, err := c.client.VerifyPassword(ctx, &identityv1.VerifyPasswordRequest{
Email: email,
Password: password,
Password: password,
})
if err != nil {
return nil, fmt.Errorf("verify password failed: %w", err)

View File

@@ -100,9 +100,15 @@ func LoadConfig(env string) (config.ConfigProvider, error) {
// e.g., DATABASE_DSN -> database.dsn, SERVER_PORT -> server.port
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
// Bind specific environment variables to config keys
v.BindEnv("database.dsn", "DATABASE_DSN")
v.BindEnv("registry.consul.address", "REGISTRY_CONSUL_ADDRESS")
v.BindEnv("registry.type", "REGISTRY_TYPE")
if err := v.BindEnv("database.dsn", "DATABASE_DSN"); err != nil {
return nil, fmt.Errorf("failed to bind DATABASE_DSN: %w", err)
}
if err := v.BindEnv("registry.consul.address", "REGISTRY_CONSUL_ADDRESS"); err != nil {
return nil, fmt.Errorf("failed to bind REGISTRY_CONSUL_ADDRESS: %w", err)
}
if err := v.BindEnv("registry.type", "REGISTRY_TYPE"); err != nil {
return nil, fmt.Errorf("failed to bind REGISTRY_TYPE: %w", err)
}
return NewViperConfig(v), nil
}

View File

@@ -0,0 +1,59 @@
// Package schema defines the Ent schema for domain entities.
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)
// AuditLog holds the schema definition for the AuditLog entity.
type AuditLog struct {
ent.Schema
}
// Fields of the AuditLog.
func (AuditLog) Fields() []ent.Field {
return []ent.Field{
field.String("id").
Unique().
Immutable(),
field.String("user_id").
NotEmpty().
Comment("ID of the user/actor performing the action"),
field.String("action").
NotEmpty().
Comment("Action performed (e.g., user.create, user.update)"),
field.String("resource").
Optional().
Comment("Resource type (e.g., user, role)"),
field.String("resource_id").
Optional().
Comment("ID of the target resource"),
field.String("ip_address").
Optional().
Comment("IP address of the client"),
field.String("user_agent").
Optional().
Comment("User agent of the client"),
field.JSON("metadata", map[string]interface{}{}).
Optional().
Comment("Additional metadata as JSON"),
field.Time("timestamp").
Default(time.Now).
Immutable(),
}
}
// Indexes of the AuditLog.
func (AuditLog) Indexes() []ent.Index {
return []ent.Index{
index.Fields("user_id"),
index.Fields("resource_id"),
index.Fields("timestamp"),
index.Fields("action"),
index.Fields("resource"),
}
}

View File

@@ -0,0 +1,32 @@
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)
// Permission holds the schema definition for the Permission entity.
type Permission struct {
ent.Schema
}
// Fields of the Permission.
func (Permission) Fields() []ent.Field {
return []ent.Field{
field.String("id").
Unique().
Immutable(),
field.String("name").
Unique().
NotEmpty().
Comment("Format: module.resource.action"),
}
}
// Edges of the Permission.
func (Permission) Edges() []ent.Edge {
return []ent.Edge{
edge.To("role_permissions", RolePermission.Type),
}
}

View File

@@ -0,0 +1,44 @@
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/field"
"entgo.io/ent/schema/index"
)
// RefreshToken holds the schema definition for the RefreshToken entity.
type RefreshToken struct {
ent.Schema
}
// Fields of the RefreshToken.
func (RefreshToken) Fields() []ent.Field {
return []ent.Field{
field.String("id").
Unique().
Immutable(),
field.String("user_id").
NotEmpty().
Comment("ID of the user who owns this refresh token"),
field.String("token_hash").
NotEmpty().
Sensitive().
Comment("SHA256 hash of the refresh token"),
field.Time("expires_at").
Comment("When the refresh token expires"),
field.Time("created_at").
Default(time.Now).
Immutable(),
}
}
// Indexes of the RefreshToken.
func (RefreshToken) Indexes() []ent.Index {
return []ent.Index{
index.Fields("user_id"),
index.Fields("token_hash"),
index.Fields("expires_at"),
}
}

View File

@@ -0,0 +1,39 @@
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)
// Role holds the schema definition for the Role entity.
type Role struct {
ent.Schema
}
// Fields of the Role.
func (Role) Fields() []ent.Field {
return []ent.Field{
field.String("id").
Unique().
Immutable(),
field.String("name").
Unique().
NotEmpty(),
field.String("description").
Optional(),
field.Time("created_at").
Default(time.Now).
Immutable(),
}
}
// Edges of the Role.
func (Role) Edges() []ent.Edge {
return []ent.Edge{
edge.To("role_permissions", RolePermission.Type),
edge.To("user_roles", UserRole.Type),
}
}

View File

@@ -0,0 +1,34 @@
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)
// RolePermission holds the schema definition for the RolePermission entity (many-to-many relationship).
type RolePermission struct {
ent.Schema
}
// Fields of the RolePermission.
func (RolePermission) Fields() []ent.Field {
return []ent.Field{
field.String("role_id"),
field.String("permission_id"),
}
}
// Edges of the RolePermission.
func (RolePermission) Edges() []ent.Edge {
return []ent.Edge{
edge.To("role", Role.Type).
Unique().
Required().
Field("role_id"),
edge.To("permission", Permission.Type).
Unique().
Required().
Field("permission_id"),
}
}

View File

@@ -0,0 +1,57 @@
package schema
import (
"time"
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)
// User holds the schema definition for the User entity.
type User struct {
ent.Schema
}
// Fields of the User.
func (User) Fields() []ent.Field {
return []ent.Field{
field.String("id").
Unique().
Immutable(),
field.String("email").
Unique().
NotEmpty(),
field.String("username").
Optional(),
field.String("first_name").
Optional(),
field.String("last_name").
Optional(),
field.String("password_hash").
NotEmpty(),
field.Bool("verified").
Default(false),
field.String("email_verification_token").
Optional().
Sensitive(),
field.String("password_reset_token").
Optional().
Sensitive(),
field.Time("password_reset_expires_at").
Optional(),
field.Time("created_at").
Default(time.Now).
Immutable(),
field.Time("updated_at").
Default(time.Now).
UpdateDefault(time.Now),
}
}
// Edges of the User.
func (User) Edges() []ent.Edge {
return []ent.Edge{
edge.To("user_roles", UserRole.Type),
}
}

View File

@@ -0,0 +1,34 @@
package schema
import (
"entgo.io/ent"
"entgo.io/ent/schema/edge"
"entgo.io/ent/schema/field"
)
// UserRole holds the schema definition for the UserRole entity (many-to-many relationship).
type UserRole struct {
ent.Schema
}
// Fields of the UserRole.
func (UserRole) Fields() []ent.Field {
return []ent.Field{
field.String("user_id"),
field.String("role_id"),
}
}
// Edges of the UserRole.
func (UserRole) Edges() []ent.Edge {
return []ent.Edge{
edge.To("user", User.Type).
Unique().
Required().
Field("user_id"),
edge.To("role", Role.Type).
Unique().
Required().
Field("role_id"),
}
}

57
scripts/pre-commit-check.sh Executable file
View File

@@ -0,0 +1,57 @@
#!/bin/bash
# Pre-commit check script that runs lint, fmt-check, test, and build in gitea-runner container
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
IMAGE_NAME="wirelos/pre-commit"
CONTAINER_NAME="goplt-pre-commit-check"
echo "🔍 Checking for Docker image: $IMAGE_NAME"
if ! docker images --format "{{.Repository}}:{{.Tag}}" | grep -q "^${IMAGE_NAME}:latest$"; then
echo "📦 Image not found. Building $IMAGE_NAME from ci/pre-commit/Dockerfile..."
docker build -t "$IMAGE_NAME:latest" -f "$PROJECT_ROOT/ci/pre-commit/Dockerfile" "$PROJECT_ROOT/ci/pre-commit" || {
echo "❌ Failed to build Docker image"
exit 1
}
echo "✅ Image built successfully"
else
echo "✅ Image found locally"
fi
echo "🧹 Cleaning up any existing container..."
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
echo "🚀 Starting pre-commit container..."
docker run --rm \
--name "$CONTAINER_NAME" \
-v "$PROJECT_ROOT:/workspace" \
-w /workspace \
"$IMAGE_NAME:latest" \
sh -c "
echo '📋 Running make fmt-check...'
make fmt-check || exit 1
echo '🔍 Running make lint...'
make lint || exit 1
echo '🧪 Running make test...'
make test || exit 1
echo '🔨 Running make build...'
make build || exit 1
echo '✅ All checks passed!'
"
EXIT_CODE=$?
if [ $EXIT_CODE -eq 0 ]; then
echo "✅ All pre-commit checks passed!"
else
echo "❌ Pre-commit checks failed. Please fix the issues above."
fi
exit $EXIT_CODE

View File

@@ -3,6 +3,7 @@ package api
import (
"context"
"math"
auditv1 "git.dcentral.systems/toolz/goplt/api/proto/generated/audit/v1"
"git.dcentral.systems/toolz/goplt/services/audit/internal/service"
@@ -118,8 +119,15 @@ func (s *Server) Query(ctx context.Context, req *auditv1.QueryRequest) (*auditv1
})
}
total := len(protoEntries)
var totalInt32 int32
if total > math.MaxInt32 {
totalInt32 = math.MaxInt32
} else {
totalInt32 = int32(total)
}
return &auditv1.QueryResponse{
Entries: protoEntries,
Total: int32(len(protoEntries)), // Note: This is a simplified total, actual total would require a count query
Total: totalInt32, // Note: This is a simplified total, actual total would require a count query
}, nil
}

View File

@@ -182,4 +182,3 @@ func TestGateway_matchRoute(t *testing.T) {
})
}
}

View File

@@ -456,20 +456,20 @@ func (m *mockAuthClient) Logout(ctx context.Context, refreshToken string) error
// mockIdentityClient implements services.IdentityServiceClient for testing.
type mockIdentityClient struct {
getUserResp *services.User
getUserErr error
getUserByEmailResp *services.User
getUserByEmailErr error
createUserResp *services.User
createUserErr error
updateUserResp *services.User
updateUserErr error
deleteUserErr error
verifyEmailErr error
requestPasswordResetErr error
resetPasswordErr error
verifyPasswordResp *services.User
verifyPasswordErr error
getUserResp *services.User
getUserErr error
getUserByEmailResp *services.User
getUserByEmailErr error
createUserResp *services.User
createUserErr error
updateUserResp *services.User
updateUserErr error
deleteUserErr error
verifyEmailErr error
requestPasswordResetErr error
resetPasswordErr error
verifyPasswordResp *services.User
verifyPasswordErr error
}
func (m *mockIdentityClient) GetUser(ctx context.Context, id string) (*services.User, error) {

View File

@@ -61,7 +61,7 @@ func TestGateway_handleLogin(t *testing.T) {
name: "client error - unauthorized",
requestBody: map[string]string{
"email": "test@example.com",
"password": "wrongpassword",
"password": "wrongpassword",
},
clientErr: status.Error(codes.Unauthenticated, "invalid credentials"),
expectedStatus: http.StatusUnauthorized,
@@ -70,7 +70,7 @@ func TestGateway_handleLogin(t *testing.T) {
name: "client error - internal",
requestBody: map[string]string{
"email": "test@example.com",
"password": "password123",
"password": "password123",
},
clientErr: status.Error(codes.Internal, "internal error"),
expectedStatus: http.StatusInternalServerError,
@@ -132,7 +132,7 @@ func TestGateway_handleRefreshToken(t *testing.T) {
expectedStatus: http.StatusOK,
},
{
name: "invalid request body",
name: "invalid request body",
requestBody: map[string]string{
// missing refresh_token
},
@@ -200,7 +200,7 @@ func TestGateway_handleValidateToken(t *testing.T) {
expectedStatus: http.StatusOK,
},
{
name: "invalid request body",
name: "invalid request body",
requestBody: map[string]string{
// missing token
},
@@ -261,7 +261,7 @@ func TestGateway_handleLogout(t *testing.T) {
expectedStatus: http.StatusOK,
},
{
name: "invalid request body",
name: "invalid request body",
requestBody: map[string]string{
// missing refresh_token
},
@@ -316,11 +316,11 @@ func TestGateway_handleGetUser(t *testing.T) {
}{
{
name: "successful get user",
userID: "user-123",
userID: "user-123",
clientResp: &services.User{
ID: "user-123",
Email: "test@example.com",
Username: "testuser",
Username: "testuser",
FirstName: "Test",
LastName: "User",
},
@@ -374,31 +374,31 @@ func TestGateway_handleCreateUser(t *testing.T) {
name: "successful create",
requestBody: services.CreateUserRequest{
Email: "test@example.com",
Username: "testuser",
Password: "password123",
FirstName: "Test",
LastName: "User",
Username: "testuser",
Password: "password123",
FirstName: "Test",
LastName: "User",
},
clientResp: &services.User{
ID: "user-123",
Email: "test@example.com",
Username: "testuser",
Username: "testuser",
FirstName: "Test",
LastName: "User",
},
expectedStatus: http.StatusCreated,
},
{
name: "invalid JSON",
requestBody: "not a json object",
name: "invalid JSON",
requestBody: "not a json object",
expectedStatus: http.StatusBadRequest,
},
{
name: "client error - already exists",
requestBody: services.CreateUserRequest{
Email: "existing@example.com",
Username: "existing",
Password: "password123",
Username: "existing",
Password: "password123",
},
clientErr: status.Error(codes.AlreadyExists, "user already exists"),
expectedStatus: http.StatusConflict,
@@ -517,4 +517,3 @@ func TestGateway_handleGRPCError(t *testing.T) {
})
}
}

View File

@@ -7,6 +7,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"math"
"strings"
"golang.org/x/crypto/argon2"
@@ -77,7 +78,44 @@ func Verify(password, hash string) (bool, error) {
}
// Compute hash with same parameters
actualHash := argon2.IDKey([]byte(password), salt, uint32(t), uint32(m), uint8(p), uint32(len(expectedHash)))
hashLen := len(expectedHash)
if hashLen < 0 || hashLen > math.MaxUint32 {
return false, fmt.Errorf("invalid hash length: %d", hashLen)
}
var hashLenUint32 uint32
if hashLen > math.MaxUint32 {
hashLenUint32 = math.MaxUint32
} else {
hashLenUint32 = uint32(hashLen)
}
// Bounds check for t and m to prevent overflow
var tUint32, mUint32 uint32
if t > math.MaxUint32 {
tUint32 = math.MaxUint32
} else if t < 0 {
tUint32 = 0
} else {
tUint32 = uint32(t) //nolint:gosec // bounds checked above
}
if m > math.MaxUint32 {
mUint32 = math.MaxUint32
} else if m < 0 {
mUint32 = 0
} else {
mUint32 = uint32(m) //nolint:gosec // bounds checked above
}
var pUint8 uint8
if p > math.MaxUint8 {
pUint8 = math.MaxUint8
} else if p < 0 {
pUint8 = 0
} else {
pUint8 = uint8(p)
}
actualHash := argon2.IDKey([]byte(password), salt, tUint32, mUint32, pUint8, hashLenUint32)
// Constant-time comparison
if subtle.ConstantTimeCompare(expectedHash, actualHash) == 1 {

View File

@@ -208,35 +208,3 @@ func TestHash_Uniqueness(t *testing.T) {
assert.True(t, valid, "All hashes should verify correctly")
}
}
// Helper functions for test
func splitHash(hash string) []string {
parts := make([]string, 0, 6)
current := ""
for _, char := range hash {
if char == '$' {
if current != "" {
parts = append(parts, current)
current = ""
}
} else {
current += string(char)
}
}
if current != "" {
parts = append(parts, current)
}
return parts
}
func joinHash(parts []string) string {
result := ""
for i, part := range parts {
if i > 0 {
result += "$"
}
result += part
}
return result
}