301 lines
8.4 KiB
Go
301 lines
8.4 KiB
Go
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
|
|
})
|
|
|
|
}
|