Files

365 lines
9.0 KiB
Smarty

package main
import (
"context"
{{ if .UseGRPC }}
"errors"
"net"
"net/http"
{{ end }}
"os"
"os/signal"
"syscall"
"github.com/hibiken/asynq"
{{ if .UseGRPC }}
"github.com/rs/cors"
{{ end }}
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
"golang.org/x/sync/errgroup"
"{{ .ModuleName }}/internal/auth"
"{{ .ModuleName }}/internal/config"
db "{{ .ModuleName }}/internal/db/sqlc"
{{ if .UseGRPC }}
gapi "{{ .ModuleName }}/internal/handlers/grpc"
{{ end }}
{{ if not .UseGRPC }}
api "{{ .ModuleName }}/internal/handlers/http"
{{ end }}
"{{ .ModuleName }}/internal/mailer"
"{{ .ModuleName }}/internal/service"
"{{ .ModuleName }}/internal/worker"
{{ if .UseGRPC }}
"{{ .ModuleName }}/pb"
{{ end }}
"github.com/golang-migrate/migrate/v4"
_ "github.com/golang-migrate/migrate/v4/database/postgres"
_ "github.com/golang-migrate/migrate/v4/source/file"
{{ if .UseGRPC }}
"github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
"github.com/rakyll/statik/fs"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
"google.golang.org/protobuf/encoding/protojson"
_ "{{ .ModuleName }}/docs/statik"
{{ end }}
"github.com/jackc/pgx/v5/pgxpool"
_ "github.com/jackc/pgx/v5/stdlib"
)
var interruptSignals = []os.Signal{
os.Interrupt,
syscall.SIGTERM,
syscall.SIGINT,
}
// @title App Template API
// @version 1.0
// @description REST API template: accounts (register/login/email verification/refresh tokens) and multi-tenant organizations (members/invites) already wired up.
// @termsOfService http://swagger.io/terms/
// @contact.name App Template
// @license.name MIT
// @license.url https://opensource.org/licenses/MIT
// @host localhost:8080
// @BasePath /api/v1
// @schemes http https
// @securityDefinitions.apikey BearerAuth
// @in header
// @name Authorization
// @description PASETO access token. Format: "Bearer <token>". Some endpoints also accept the equivalent value via the "access_token" cookie.
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})
zerolog.SetGlobalLevel(zerolog.DebugLevel)
}
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)
orgService := service.NewOrganizationService(store, taskDist)
wg, ctx := errgroup.WithContext(ctx)
{{ if not .UseGRPC }}
services := api.Services{
UserService: userService,
AuthService: authService,
OrganizationService: orgService,
}
runGinServer(ctx, wg, config, store, taskDist, tokenMaker, services)
{{ else }}
runGatewayServer(ctx, wg, config, store, taskDist, tokenMaker, userService, authService, orgService)
runGrpcServer(ctx, wg, config, store, taskDist, tokenMaker, userService, authService, orgService)
{{ end }}
runTaskProcessor(ctx, wg, redisOpt, store, config)
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.NewSMTPSender(
config.EmailSenderName,
config.EmailSender,
config.EmailPassword,
config.SMTPAddr,
)
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")
}
{{ if not .UseGRPC }}
func runGinServer(
ctx context.Context,
wg *errgroup.Group,
config config.Config,
store db.Store,
td worker.TaskDistributor,
tokenMaker auth.Maker,
services api.Services,
) {
s, err := api.NewServer(config, store, td, tokenMaker, services)
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
})
}
{{ end }}
{{ if .UseGRPC }}
func runGrpcServer(
ctx context.Context,
wg *errgroup.Group,
config config.Config,
store db.Store,
td worker.TaskDistributor,
tokenMaker auth.Maker,
userService service.UserService,
authService service.AuthService,
organizationService service.OrganizationService,
) {
server, err := gapi.NewServer(config, store, td, tokenMaker, userService, authService, organizationService)
if err != nil {
log.Fatal().Err(err).Msg("cannot create server:")
}
grpcLogger := grpc.UnaryInterceptor(gapi.GrpcLogger)
grpcServer := grpc.NewServer(grpcLogger)
pb.Register{{ .ServiceName }}Server(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,
td worker.TaskDistributor,
tokenMaker auth.Maker,
userService service.UserService,
authService service.AuthService,
organizationService service.OrganizationService,
) {
server, err := gapi.NewServer(config, store, td, tokenMaker, userService, authService, organizationService)
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.Register{{ .ServiceName }}HandlerServer(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
})
}
{{ end }}