feat: 0.0.1

This commit is contained in:
FernandoJVideira committed 2026-01-08 01:15:07 +00:00
commit 961bdd81d4
146 files changed
+10444

No files matched your search

+300
View File
@@ -0,0 +1,300 @@
package main
import (
"context"
"errors"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"github.com/hibiken/asynq"
"github.com/rs/cors"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"golang.org/x/sync/errgroup"
"github.com/FernandoJVideira/simple_bank/internal/auth"
"github.com/FernandoJVideira/simple_bank/internal/config"
db "github.com/FernandoJVideira/simple_bank/internal/db/sqlc"
gapi "github.com/FernandoJVideira/simple_bank/internal/handlers/grpc"
api "github.com/FernandoJVideira/simple_bank/internal/handlers/http"
"github.com/FernandoJVideira/simple_bank/internal/mailer"
"github.com/FernandoJVideira/simple_bank/internal/service"
"github.com/FernandoJVideira/simple_bank/internal/worker"
"github.com/FernandoJVideira/simple_bank/pb"
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/jackc/pgx/v5/pgxpool"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/rakyll/statik/fs"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"google.golang.org/protobuf/encoding/protojson"
_ "github.com/FernandoJVideira/simple_bank/docs/statik"
)
var interruptSignals = []os.Signal{
os.Interrupt,
syscall.SIGTERM,
syscall.SIGINT,
}
// @title Simple Bank API
// @version 1.0
// @description API for Simple Bank application
// @termsOfService http://swagger.io/terms/
// @contact.name API Support
// @contact.email [email protected]
// @license.name MIT
// @license.url https://opensource.org/licenses/MIT
// @host localhost:8080
// @BasePath /v1
// @schemes http https
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description Type "Bearer" followed by a space and JWT token.
func main() {
config, err := config.LoadConfig(".")
if err != nil {
log.Fatal().Err(err).Msg("cannot load config")
}
if config.Environment == "development" {
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr})
}
ctx, cancel := signal.NotifyContext(context.Background(), interruptSignals...)
defer cancel()
connPool, err := pgxpool.New(ctx, config.DBAddr)
if err != nil {
log.Fatal().Err(err).Msg("cannot connect to db")
}
runDBMigration(config.MigrationURL, config.DBAddr)
redisOpt := asynq.RedisClientOpt{
Addr: config.RedisAddr,
Password: config.RedisPassword,
DB: config.RedisDB,
}
taskDist := worker.NewRedisTaskDistributor(redisOpt)
store := db.NewPostgresStore(connPool)
tokenMaker, err := auth.NewPasetoMaker(config)
if err != nil {
log.Fatal().Err(err).Msg("cannot create token maker")
}
userService := service.NewUserService(store, taskDist)
authService := service.NewAuthService(store, tokenMaker, config)
wg, ctx := errgroup.WithContext(ctx)
runGinServer(ctx, wg, config, store, taskDist, tokenMaker, userService, authService)
runTaskProcessor(ctx, wg, redisOpt, store, config)
//runGatewayServer(ctx, wg, config, store, taskDist, tokenMaker, userService, authService)
//runGrpcServer(ctx, wg, config, store, taskDist, tokenMaker, userService, authService)
err = wg.Wait()
if err != nil {
log.Fatal().Err(err).Msg("application terminated with error")
}
}
func runTaskProcessor(ctx context.Context, wg *errgroup.Group, redisOpt asynq.RedisClientOpt, store db.Store, config config.Config) {
mailer := mailer.NewGmailSender(config.EmailSenderName, config.EmailSender, config.EmailPassword)
taskProcessor := worker.NewRedisTaskProcessor(redisOpt, store, mailer, config)
log.Info().Msg("start task processor")
if err := taskProcessor.Start(); err != nil {
log.Fatal().Err(err).Msg("cannot start task processor")
}
wg.Go(func() error {
<-ctx.Done()
log.Info().Msg("gracefully shutting down task processor")
taskProcessor.Shutdown()
log.Info().Msg("task processor shut down")
return nil
})
}
func runDBMigration(migrationURL, dbSource string) {
migration, err := migrate.New(migrationURL, dbSource)
if err != nil {
log.Fatal().Err(err).Msg("cannot create new migrate instance")
}
if err = migration.Up(); err != nil && err != migrate.ErrNoChange {
log.Fatal().Err(err).Msg("failed to run migrate up")
}
log.Info().Msg("db migrated successfully")
}
func runGinServer(ctx context.Context, wg *errgroup.Group, config config.Config, store db.Store, td worker.TaskDistributor, tokenMaker auth.Maker, userService service.UserService, authService service.AuthService) {
s, err := api.NewServer(config, store, td, tokenMaker, userService, authService)
if err != nil {
log.Fatal().Err(err).Msg("cannot create server")
}
wg.Go(func() error {
err = s.Start(config.HTTPServerAddr)
if err != nil {
log.Fatal().Err(err).Msg("cannot start server:")
return err
}
return nil
})
wg.Go(func() error {
<-ctx.Done()
log.Info().Msg("shutting down server")
err := s.Shutdown(ctx)
if err != nil {
log.Error().Err(err).Msg("cannot shutdown server")
return err
}
log.Info().Msg("server shut down")
return nil
})
}
func runGrpcServer(ctx context.Context, wg *errgroup.Group, config config.Config, store db.Store, taskDistributor worker.TaskDistributor, tokenMaker auth.Maker, userService service.UserService, authService service.AuthService) {
server, err := gapi.NewServer(config, store, taskDistributor, tokenMaker, userService, authService)
if err != nil {
log.Fatal().Err(err).Msg("cannot create server:")
}
grpcLogger := grpc.UnaryInterceptor(gapi.GrpcLogger)
grpcServer := grpc.NewServer(grpcLogger)
pb.RegisterSimpleBankServer(grpcServer, server)
reflection.Register(grpcServer)
listener, err := net.Listen("tcp", config.GrpcServerAddr)
if err != nil {
log.Fatal().Err(err).Msg("cannot create listener")
}
wg.Go(func() error {
log.Info().Msgf("start gRPC server at %s", listener.Addr().String())
err = grpcServer.Serve(listener)
if err != nil {
if errors.Is(err, grpc.ErrServerStopped) {
return nil
}
log.Error().Err(err).Msg("gRPC server failed to serve")
return err
}
return nil
})
wg.Go(func() error {
<-ctx.Done()
log.Info().Msg("gracefully shutting down gRPC server")
grpcServer.GracefulStop()
log.Info().Msg("gRPC server shut down")
return nil
})
}
func runGatewayServer(ctx context.Context, wg *errgroup.Group, config config.Config, store db.Store, taskDistributor worker.TaskDistributor, tokenMaker auth.Maker, userService service.UserService, authService service.AuthService) {
server, err := gapi.NewServer(config, store, taskDistributor, tokenMaker, userService, authService)
if err != nil {
log.Fatal().Err(err).Msg("cannot create server:")
}
opts := runtime.WithMarshalerOption(runtime.MIMEWildcard, &runtime.JSONPb{
MarshalOptions: protojson.MarshalOptions{
UseProtoNames: true,
},
UnmarshalOptions: protojson.UnmarshalOptions{
DiscardUnknown: true,
},
})
grpcMux := runtime.NewServeMux(opts)
err = pb.RegisterSimpleBankHandlerServer(ctx, grpcMux, server)
if err != nil {
log.Fatal().Err(err).Msg("cannot register handler server")
}
mux := http.NewServeMux()
mux.Handle("/", grpcMux)
statikFS, err := fs.New()
if err != nil {
log.Fatal().Err(err).Msg("cannot create statik fs")
}
swaggerHandler := http.StripPrefix("/swagger/", http.FileServer(statikFS))
mux.Handle("/swagger/", swaggerHandler)
c := cors.New(
cors.Options{
AllowedOrigins: config.AllowedOrigins,
AllowedMethods: []string{
http.MethodGet,
http.MethodPost,
http.MethodPut,
http.MethodPatch,
http.MethodDelete,
http.MethodOptions,
},
AllowedHeaders: []string{
"Accept",
"Authorization",
"Content-Type",
"X-CSRF-Token",
},
AllowCredentials: true,
Debug: config.Environment == "development",
},
)
handler := c.Handler(gapi.HTTPLogger(mux))
httpServer := &http.Server{
Handler: handler,
Addr: config.HTTPServerAddr,
}
wg.Go(func() error {
log.Info().Msgf("start HTTP gateway server at %s", httpServer.Addr)
err = httpServer.ListenAndServe()
if err != nil {
if errors.Is(err, http.ErrServerClosed) {
return nil
}
log.Error().Err(err).Msg("cannot start http gateway server")
return err
}
return nil
})
wg.Go(func() error {
<-ctx.Done()
log.Info().Msg("gracefully shutting down HTTP gateway server")
err := httpServer.Shutdown(context.Background())
if err != nil {
log.Error().Err(err).Msg("cannot shut down http gateway server")
return err
}
log.Info().Msg("HTTP gateway server shut down")
return nil
})
}
@@ -0,0 +1,3 @@
DROP TABLE IF EXISTS "users";
DROP TABLE IF EXISTS "verify_emails";
DROP TABLE IF EXISTS "sessions";
@@ -0,0 +1,39 @@
-- SQL dump generated using DBML (dbml.dbdiagram.io)
-- Database: PostgreSQL
-- Generated at: 2025-11-30T03:13:20.470Z
CREATE TABLE "users" (
"username" varchar PRIMARY KEY,
"role" varchar NOT NULL DEFAULT 'member',
"hashed_password" varchar NOT NULL,
"full_name" varchar NOT NULL,
"email" varchar UNIQUE NOT NULL,
"is_email_verified" bool NOT NULL DEFAULT false,
"password_changed_at" timestamptz NOT NULL DEFAULT '0001-01-01',
"created_at" timestamptz NOT NULL DEFAULT (now())
);
CREATE TABLE "verify_emails" (
"id" bigserial PRIMARY KEY,
"username" varchar NOT NULL,
"email" varchar NOT NULL,
"secret_code" varchar NOT NULL,
"is_used" bool NOT NULL DEFAULT false,
"created_at" timestamptz NOT NULL DEFAULT (now()),
"expired_at" timestamptz NOT NULL DEFAULT (now() + interval '15 minutes')
);
CREATE TABLE "sessions" (
"id" uuid PRIMARY KEY,
"username" varchar NOT NULL,
"refresh_token" varchar NOT NULL,
"user_agent" varchar NOT NULL,
"client_ip" varchar NOT NULL,
"is_blocked" boolean NOT NULL DEFAULT false,
"expires_at" timestamptz NOT NULL,
"created_at" timestamptz NOT NULL DEFAULT (now())
);
ALTER TABLE "verify_emails" ADD FOREIGN KEY ("username") REFERENCES "users" ("username");
ALTER TABLE "sessions" ADD FOREIGN KEY ("username") REFERENCES "users" ("username");
+17
View File
@@ -0,0 +1,17 @@
-- name: CreateSession :one
INSERT INTO sessions (
id,
username,
refresh_token,
user_agent,
client_ip,
is_blocked,
expires_at
) VALUES (
$1, $2, $3, $4, $5, $6, $7
)
RETURNING *;
-- name: GetSession :one
SELECT * FROM sessions
WHERE id = $1 LIMIT 1;
+25
View File
@@ -0,0 +1,25 @@
-- name: CreateUser :one
INSERT INTO users (
username,
hashed_password,
full_name,
email
) VALUES (
$1, $2, $3, $4
)
RETURNING *;
-- name: GetUser :one
SELECT * FROM users
WHERE username = $1 LIMIT 1;
-- name: UpdateUser :one
UPDATE users
SET
hashed_password = COALESCE(sqlc.narg('hashed_password'), hashed_password),
password_changed_at = COALESCE(sqlc.narg('password_changed_at'), password_changed_at),
full_name = COALESCE(sqlc.narg('full_name'), full_name),
email = COALESCE(sqlc.narg('email'), email),
is_email_verified = COALESCE(sqlc.narg('is_email_verified'), is_email_verified)
WHERE username = sqlc.arg('username')
RETURNING *;
+21
View File
@@ -0,0 +1,21 @@
-- name: CreateVerifyEmail :one
INSERT INTO verify_emails (
username,
email,
secret_code
) VALUES (
$1, $2, $3
)
RETURNING *;
-- name: UpdateVerifyEmail :one
UPDATE verify_emails
SET
is_used = TRUE
WHERE
id = @id
AND secret_code = @secret_code
AND is_used = FALSE
AND expired_at > NOW()
RETURNING *;