Skip to content

Commit 6573aa7

Browse files
authored
Initial (#1)
* feat: basic project * feat: basic converter * feat: create\update account * feat: basic transaction logic * feat: basic stats * fix: boilerplate * feat: basic workflows * fix: image * fix: image repo * feat: add config reader * feat: base app config * feat: users * feat: add envs
1 parent 3c25a75 commit 6573aa7

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

54 files changed

+2486
-0
lines changed

.github/workflows/general.yaml

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
on:
2+
push:
3+
branches:
4+
- master
5+
- initial
6+
7+
env:
8+
version: 0.0.0.${{github.run_number}}
9+
jobs:
10+
version:
11+
runs-on: ubuntu-latest
12+
outputs:
13+
versionOut: ${{ steps.generateVersion.outputs.version }}
14+
steps:
15+
- id: generateVersion
16+
run: echo "version=$(date '+%Y.%m.%d.%H%M%S').${{ github.run_number }}" >> "$GITHUB_OUTPUT"
17+
image:
18+
runs-on: ubuntu-latest
19+
needs:
20+
- version
21+
env:
22+
DOCKER_SERVER_IMAGE_NAME: "ghcr.io/${{ github.repository }}/go-money-backend:${{needs.version.outputs.versionOut}}"
23+
steps:
24+
- name: Checkout code
25+
uses: actions/checkout@v4
26+
27+
- uses: docker/setup-buildx-action@v3
28+
- run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u ${{ github.actor }} --password-stdin
29+
30+
- uses: actions/checkout@v4
31+
- run: make build-docker
32+
- run: docker push ${DOCKER_SERVER_IMAGE_NAME}

.github/workflows/pull_request.yaml

+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
on:
2+
pull_request:
3+
push:
4+
branches:
5+
- master
6+
7+
jobs:
8+
lint:
9+
runs-on: ubuntu-latest
10+
container: golang:1.24-alpine
11+
env:
12+
ENVIRONMENT: ci
13+
steps:
14+
- uses: actions/checkout@v4
15+
- run: apk update && apk add curl openssl git openssh-client build-base && mkdir -p /root/.ssh
16+
- uses: golangci/[email protected]
17+
if: github.ref != 'refs/heads/master'
18+
with:
19+
version: latest
20+
args: --timeout=5m --tests=false ./...
21+
test:
22+
runs-on: ubuntu-latest
23+
container: golang:1.24-alpine
24+
services:
25+
redis:
26+
image: redis:latest
27+
postgres:
28+
image: postgres:latest
29+
env:
30+
POSTGRES_PASSWORD: test
31+
POSTGRES_HOST_AUTH_METHOD: trust
32+
env:
33+
ENVIRONMENT: ci
34+
DB_DB: money
35+
DB_HOST: postgres
36+
DB_USER: postgres
37+
steps:
38+
- uses: actions/checkout@v4
39+
- run: apk update && apk add curl openssl git openssh-client build-base && mkdir -p /root/.ssh
40+
- run: wget -O /usr/bin/mockgen https://github.com/skynet2/mock/releases/latest/download/mockgen && chmod 777 /usr/bin/mockgen
41+
- run: make generate
42+
- run: environment=ci go test -json -coverprofile=/root/coverage_temp.txt -covermode=atomic ./... > /root/test.json
43+
- run: cat /root/coverage_temp.txt | grep -v "_mock.go" | grep -v "_mocks.go" | grep -v "_mocks_test.go" | grep -v "_mock_test.go" | grep -v "main.go" > /root/coverage.txt || true
44+
- name: Upload coverage report
45+
uses: codecov/codecov-action@v3
46+
with:
47+
token: ${{ secrets.CODECOV_TOKEN }}
48+
files: /root/coverage.txt
49+
flags: unittests
50+
- run: cat /root/test.json
51+
if: always()
52+
- run: wget https://github.com/mfridman/tparse/releases/latest/download/tparse_linux_x86_64 -O tparse && chmod 777 tparse && ./tparse -all -file=/root/test.json
53+
if: always()
54+
- uses: guyarb/[email protected]
55+
if: always()
56+
with:
57+
test-results: /root/test.json

.gitignore

+2
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,5 @@ go.work.sum
2323

2424
# env file
2525
.env
26+
27+
.idea/

Makefile

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
.PHONY: lint
2+
lint:
3+
golangci-lint run
4+
5+
.PHONY: lint-dev
6+
lint-dev:
7+
golangci-lint run --tests=false
8+
9+
.PHONY: generate
10+
generate:
11+
go generate ./...
12+
13+
.PHONY: update-pb
14+
update-pb:
15+
go get github.com/ft-t/go-money-pb@master
16+
go mod tidy
17+
18+
.PHONY: test
19+
test:
20+
AUTO_CREATE_CI_DB=true go test ./...
21+
22+
23+
DOCKER_SERVER_IMAGE_NAME ?= "go-money-server:latest"
24+
25+
.PHONY: build-docker
26+
build-docker:
27+
docker build -f ./cmd/server/Dockerfile --build-arg="SOURCE_PATH=cmd/server" -t ${DOCKER_SERVER_IMAGE_NAME} .

cmd/jwt-key-generator/main.go

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package main
2+
3+
import (
4+
"github.com/ft-t/go-money/pkg/jwt"
5+
"github.com/rs/zerolog/log"
6+
)
7+
8+
func main() {
9+
keyGenerator := jwt.NewKeyGenerator()
10+
11+
key := keyGenerator.Generate()
12+
13+
log.Info().Msg("New key generated")
14+
log.Info().Msgf("%s", keyGenerator.Serialize(key))
15+
16+
if err := keyGenerator.Save(key, "private.key"); err != nil {
17+
panic(err)
18+
}
19+
}

cmd/server/Dockerfile

+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
FROM golang:1.24-alpine as builder
2+
RUN apk update && apk add openssh-client git
3+
ARG SOURCE_PATH=""
4+
ADD . /src
5+
WORKDIR /src/$SOURCE_PATH
6+
RUN GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o /src/dist/app
7+
8+
FROM alpine:latest as done
9+
COPY --from=builder /src/dist /opt/app
10+
WORKDIR /opt/app
11+
ENTRYPOINT ["./app"]

cmd/server/config.go

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package main
2+
3+
import (
4+
"connectrpc.com/connect"
5+
"context"
6+
configurationv1 "github.com/ft-t/go-money-pb/gen/gomoneypb/configuration/v1"
7+
"github.com/ft-t/go-money-pb/gen/gomoneypb/configuration/v1/accountsv1connect"
8+
"github.com/ft-t/go-money/pkg/boilerplate"
9+
)
10+
11+
type ConfigApi struct {
12+
configSvc ConfigSvc
13+
}
14+
15+
func NewConfigApi(
16+
mux *boilerplate.DefaultGrpcServer,
17+
userSvc ConfigSvc,
18+
) (*ConfigApi, error) {
19+
res := &ConfigApi{
20+
configSvc: userSvc,
21+
}
22+
23+
mux.GetMux().Handle(
24+
accountsv1connect.NewConfigurationServiceHandler(res, mux.GetDefaultHandlerOptions()...),
25+
)
26+
27+
return res, nil
28+
}
29+
30+
func (a *ConfigApi) GetConfiguration(
31+
ctx context.Context,
32+
c *connect.Request[configurationv1.GetConfigurationRequest],
33+
) (*connect.Response[configurationv1.GetConfigurationResponse], error) {
34+
res, err := a.configSvc.GetConfiguration(ctx, c.Msg)
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
return connect.NewResponse(res), nil
40+
}

cmd/server/interfaces.go

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package main
2+
3+
import (
4+
"context"
5+
configurationv1 "github.com/ft-t/go-money-pb/gen/gomoneypb/configuration/v1"
6+
usersv1 "github.com/ft-t/go-money-pb/gen/gomoneypb/users/v1"
7+
)
8+
9+
type UserSvc interface {
10+
Login(
11+
ctx context.Context,
12+
req *usersv1.LoginRequest,
13+
) (*usersv1.LoginResponse, error)
14+
15+
Create(
16+
ctx context.Context,
17+
req *usersv1.CreateRequest,
18+
) (*usersv1.CreateResponse, error)
19+
}
20+
21+
type ConfigSvc interface {
22+
GetConfiguration(
23+
ctx context.Context,
24+
_ *configurationv1.GetConfigurationRequest,
25+
) (*configurationv1.GetConfigurationResponse, error)
26+
}

cmd/server/main.go

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"github.com/ft-t/go-money/pkg/appcfg"
6+
"github.com/ft-t/go-money/pkg/boilerplate"
7+
"github.com/ft-t/go-money/pkg/configuration"
8+
"github.com/ft-t/go-money/pkg/jwt"
9+
"github.com/ft-t/go-money/pkg/users"
10+
"github.com/rs/zerolog/log"
11+
"os"
12+
"os/signal"
13+
"syscall"
14+
)
15+
16+
func main() {
17+
sig := make(chan os.Signal, 1)
18+
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
19+
20+
config := configuration.GetConfiguration()
21+
_, cancel := context.WithCancel(context.Background())
22+
23+
logger := log.Logger
24+
25+
if config.JwtPrivateKey == "" {
26+
logger.Warn().Msgf("jwt private key is empty. Will create a new temporary key")
27+
28+
keyGen := jwt.NewKeyGenerator()
29+
newKey := keyGen.Generate()
30+
31+
config.JwtPrivateKey = string(keyGen.Serialize(newKey))
32+
}
33+
34+
grpcServer := boilerplate.GetDefaultGrpcServerBuilder().Build()
35+
36+
jwtService, err := users.NewJwtGenerator(config.JwtPrivateKey)
37+
if err != nil {
38+
log.Logger.Fatal().Err(err).Msg("failed to create jwt service")
39+
}
40+
41+
userService := users.NewService(&users.ServiceConfig{
42+
JwtSvc: jwtService,
43+
})
44+
45+
_, err = NewUserApi(grpcServer, userService)
46+
if err != nil {
47+
log.Logger.Fatal().Err(err).Msg("failed to create user handler")
48+
}
49+
50+
_, err = NewConfigApi(grpcServer, appcfg.NewService(&appcfg.ServiceConfig{
51+
UserSvc: userService,
52+
}))
53+
if err != nil {
54+
log.Logger.Fatal().Err(err).Msg("failed to create config handler")
55+
}
56+
57+
go func() {
58+
grpcServer.ServeAsync(config.GrpcPort)
59+
60+
log.Logger.Info().Msgf("server started on port %v", config.GrpcPort)
61+
}()
62+
63+
sg := <-sig
64+
65+
log.Logger.Info().Msgf("GOT SIGNAL %v", sg.String())
66+
log.Logger.Info().Msgf("[Graceful Shutdown] GOT SIGNAL %v", sg.String())
67+
68+
log.Logger.Info().Msgf("[Graceful Shutdown] Shutting down webservers")
69+
70+
cancel()
71+
_ = grpcServer.Shutdown(context.TODO())
72+
73+
log.Logger.Info().Msg("[Graceful Shutdown] Exit")
74+
}

cmd/server/user.go

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package main
2+
3+
import (
4+
"connectrpc.com/connect"
5+
"context"
6+
usersv1 "github.com/ft-t/go-money-pb/gen/gomoneypb/users/v1"
7+
"github.com/ft-t/go-money-pb/gen/gomoneypb/users/v1/usersv1connect"
8+
"github.com/ft-t/go-money/pkg/boilerplate"
9+
)
10+
11+
type UserApi struct {
12+
userSvc UserSvc
13+
}
14+
15+
func NewUserApi(
16+
mux *boilerplate.DefaultGrpcServer,
17+
userSvc UserSvc,
18+
) (*UserApi, error) {
19+
res := &UserApi{
20+
userSvc: userSvc,
21+
}
22+
23+
mux.GetMux().Handle(
24+
usersv1connect.NewUsersServiceHandler(res, mux.GetDefaultHandlerOptions()...),
25+
)
26+
27+
return res, nil
28+
}
29+
30+
func (u *UserApi) Create(ctx context.Context, c *connect.Request[usersv1.CreateRequest]) (*connect.Response[usersv1.CreateResponse], error) {
31+
resp, err := u.userSvc.Create(ctx, c.Msg)
32+
if err != nil {
33+
return nil, err
34+
}
35+
36+
return connect.NewResponse(resp), nil
37+
}
38+
39+
func (u *UserApi) Login(
40+
ctx context.Context,
41+
c *connect.Request[usersv1.LoginRequest],
42+
) (*connect.Response[usersv1.LoginResponse], error) {
43+
resp, err := u.userSvc.Login(ctx, c.Msg)
44+
if err != nil {
45+
return nil, err
46+
}
47+
48+
return connect.NewResponse(resp), nil
49+
}

cmd/sync-exchange-rates/main.go

+27
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"github.com/ft-t/go-money/pkg/currency"
6+
"net/http"
7+
"os"
8+
)
9+
10+
const (
11+
defaultExchangeRatesURL = "https://localhost/latest.json" // todo
12+
)
13+
14+
func main() {
15+
fetchURL := defaultExchangeRatesURL
16+
17+
if v := os.Getenv("CUSTOM_EXCHANGE_RATES_URL"); v != "" {
18+
fetchURL = v
19+
}
20+
21+
ctx := context.TODO()
22+
23+
sync := currency.NewSyncer(http.DefaultClient)
24+
if err := sync.Sync(ctx, fetchURL); err != nil {
25+
panic(err)
26+
}
27+
}

compose/.env.example

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
TAG=2025.02.23.200442.3
2+
GRPC_PORT=52055
3+
PUBLIC_PORT=52055
4+
5+
## APP
6+
DB_HOST=db
7+
DB_PORT=5432
8+
DB_USER=money
9+
DB_PASSWORD=XcteUSxT2wP68ZW$
10+
DB_DB=money

0 commit comments

Comments
 (0)