feat: 0.0.1
This commit is contained in:
146 files changed
+10444
No files matched your search
+300
@@ -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");
|
||||
@@ -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;
|
||||
@@ -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 *;
|
||||
@@ -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 *;
|
||||
Reference in new issue
Block a user