From 016b820b459a415ab1df666ad6551b35beff93c1 Mon Sep 17 00:00:00 2001 From: Vincent Vu Date: Fri, 1 Sep 2023 09:37:16 -0700 Subject: [PATCH] forgot password flow (#4) * complete password registration flow * dont load .env on prod * use TLS in db conn * fix not use SSL * add error logs + better error http response scheme * add config object * tryna see resend_api_key issues * woops * use resend api key from config * don't log it cmon --- apps/api/.dockerignore | 6 + apps/api/.env.example | 6 +- apps/api/Dockerfile | 17 + apps/api/db/main.go | 49 +- apps/api/db/redis.go | 17 + apps/api/docker-compose.yml | 12 +- apps/api/email/errors.go | 8 + apps/api/email/main.go | 69 ++ apps/api/email/templates/forgot-password.mjml | 13 + apps/api/email/templates/verify-email.mjml | 12 + apps/api/go.mod | 12 +- apps/api/go.sum | 20 + apps/api/http/response.go | 24 +- apps/api/loader/main.go | 3 +- apps/api/main.go | 28 +- apps/api/middleware/middleware.go | 18 +- apps/api/migrations/20230830050922.sql | 14 + apps/api/migrations/atlas.sum | 3 +- apps/api/package.json | 20 +- apps/api/scripts/main.go | 18 + apps/api/scripts/{ => src}/migrate.go | 10 +- apps/api/supernova_tasks/handlers.go | 6 +- apps/api/user/errors.go | 7 + apps/api/user/handlers.go | 79 +- apps/api/user/model.go | 115 ++- apps/api/user/routes.go | 16 +- apps/api/utils/config.go | 54 ++ apps/api/utils/errors.go | 23 + apps/desktop/lib/src/screens/auth.dart | 11 +- apps/desktop/lib/src/utils/constants.dart | 6 + apps/www/.env.example | 2 + apps/www/app/forgot-password/page.tsx | 102 +++ apps/www/app/forgot-password/verify/page.tsx | 145 ++++ apps/www/app/page.tsx | 12 +- apps/www/components/horizontal-logo.tsx | 21 + apps/www/services/backend.ts | 33 + pnpm-lock.yaml | 807 +++++++++++++++++- 37 files changed, 1686 insertions(+), 132 deletions(-) create mode 100644 apps/api/.dockerignore create mode 100644 apps/api/Dockerfile create mode 100644 apps/api/db/redis.go create mode 100644 apps/api/email/errors.go create mode 100644 apps/api/email/main.go create mode 100644 apps/api/email/templates/forgot-password.mjml create mode 100644 apps/api/email/templates/verify-email.mjml create mode 100644 apps/api/migrations/20230830050922.sql create mode 100644 apps/api/scripts/main.go rename apps/api/scripts/{ => src}/migrate.go (90%) create mode 100644 apps/api/user/errors.go create mode 100644 apps/api/utils/config.go create mode 100644 apps/api/utils/errors.go create mode 100644 apps/www/.env.example create mode 100644 apps/www/app/forgot-password/page.tsx create mode 100644 apps/www/app/forgot-password/verify/page.tsx create mode 100644 apps/www/components/horizontal-logo.tsx create mode 100644 apps/www/services/backend.ts diff --git a/apps/api/.dockerignore b/apps/api/.dockerignore new file mode 100644 index 0000000..30907d3 --- /dev/null +++ b/apps/api/.dockerignore @@ -0,0 +1,6 @@ +# flyctl launch added from .gitignore +**/.idea +**/bin +**/.env +**/gin-bin +fly.toml diff --git a/apps/api/.env.example b/apps/api/.env.example index 7b256f5..b77056a 100644 --- a/apps/api/.env.example +++ b/apps/api/.env.example @@ -4,4 +4,8 @@ DB_NAME="supernova" DB_USERNAME="root" DB_PASSWORD="example" DB_PORT=5432 -JWT_SECRET="A super secret password" # you can use `openssl rand -base64 32` to generate a random string \ No newline at end of file +JWT_SECRET="A super secret password" # you can use `openssl rand -base64 32` to generate a random string +RESEND_API_KEY="" +BASE_URL_WEB_APP="" # for various things that happens not on the desktop +REDIS_URL="127.0.0.1:6379" +REDIS_PASSWORD="example" \ No newline at end of file diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 0000000..3a4227b --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,17 @@ +# Use the official Go base image +FROM golang:1.20 AS build + +# Set the working directory inside the container +WORKDIR /app + +# Copy the Go application source code into the container +COPY . . + +# Build the Go application +RUN go build -o /supernova + +# Expose the port that the Go application will listen on +EXPOSE 3001 + +# Define the command to run your Go application +CMD ["/supernova"] diff --git a/apps/api/db/main.go b/apps/api/db/main.go index 41f71fb..e3e49ba 100644 --- a/apps/api/db/main.go +++ b/apps/api/db/main.go @@ -2,9 +2,8 @@ package db import ( "fmt" - "os" - "github.com/joho/godotenv" + "github.com/trysupernova/supernova-api/utils" "gorm.io/driver/mysql" "gorm.io/gorm" ) @@ -16,20 +15,12 @@ var DB *gorm.DB * Returns a database connection URL suitable for use with Atlas */ func GetDatabaseUrl() string { - // set config file for dev environment only - if os.Getenv("ENVIRONMENT") == "dev" || os.Getenv("ENVIRONMENT") == "" { - err := godotenv.Load(".env") - if err != nil { - panic("Error loading .env file") - } - } - //db config vars - dbHost := os.Getenv("DB_HOST") - dbName := os.Getenv("DB_NAME") - dbUser := os.Getenv("DB_USERNAME") - dbPassword := os.Getenv("DB_PASSWORD") - dbPort := os.Getenv("DB_PORT") + dbHost := utils.GetConfig().DB_HOST + dbName := utils.GetConfig().DB_NAME + dbUser := utils.GetConfig().DB_USERNAME + dbPassword := utils.GetConfig().DB_PASSWORD + dbPort := utils.GetConfig().DB_PORT //build connection string var dbConnectionString string = fmt.Sprintf("mysql://%s:%s@%s:%s/%s", dbUser, dbPassword, dbHost, dbPort, dbName) @@ -41,20 +32,12 @@ func GetDatabaseUrl() string { * Returns a database connection DSN suitable for use with GORM */ func GetDatabaseDSN() string { - // set config file for dev environment only - if os.Getenv("ENVIRONMENT") == "dev" || os.Getenv("ENVIRONMENT") == "" { - err := godotenv.Load(".env") - if err != nil { - panic("Error loading .env file") - } - } - //db config vars - dbHost := os.Getenv("DB_HOST") - dbName := os.Getenv("DB_NAME") - dbUser := os.Getenv("DB_USERNAME") - dbPassword := os.Getenv("DB_PASSWORD") - dbPort := os.Getenv("DB_PORT") + dbHost := utils.GetConfig().DB_HOST + dbName := utils.GetConfig().DB_NAME + dbUser := utils.GetConfig().DB_USERNAME + dbPassword := utils.GetConfig().DB_PASSWORD + dbPort := utils.GetConfig().DB_PORT //build connection string var dbConnectionString string = fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", dbUser, dbPassword, dbHost, dbPort, dbName) @@ -66,16 +49,8 @@ func GetDatabaseDSN() string { * Setup database connection and return a pointer to the connection */ func SetupDB() *gorm.DB { - // set config file for dev environment only - if os.Getenv("ENVIRONMENT") == "dev" || os.Getenv("ENVIRONMENT") == "" { - err := godotenv.Load(".env") - if err != nil { - panic("Error loading .env file") - } - } - //build connection string - var dbConnectionString string = GetDatabaseDSN() + dbConnectionString := GetDatabaseDSN() //connect to db db, dbError := gorm.Open(mysql.Open(dbConnectionString), &gorm.Config{SkipDefaultTransaction: true}) if dbError != nil { diff --git a/apps/api/db/redis.go b/apps/api/db/redis.go new file mode 100644 index 0000000..21d09d4 --- /dev/null +++ b/apps/api/db/redis.go @@ -0,0 +1,17 @@ +package db + +import ( + "github.com/redis/go-redis/v9" + "github.com/trysupernova/supernova-api/utils" +) + +var Redis *redis.Client + +func SetupRedis() *redis.Client { + Redis = redis.NewClient(&redis.Options{ + Addr: utils.GetConfig().REDIS_URL, + Password: utils.GetConfig().REDIS_PASSWORD, + DB: 0, + }) + return Redis +} diff --git a/apps/api/docker-compose.yml b/apps/api/docker-compose.yml index 04b154a..936273e 100644 --- a/apps/api/docker-compose.yml +++ b/apps/api/docker-compose.yml @@ -11,6 +11,16 @@ services: - "3306:3306" volumes: - db:/var/lib/mysql + redis: + image: redis:6 + container_name: redis + ports: + - "6379:6379" + volumes: + - redis:/data + environment: + - REDIS_PASSWORD=example volumes: - db: \ No newline at end of file + db: + redis: \ No newline at end of file diff --git a/apps/api/email/errors.go b/apps/api/email/errors.go new file mode 100644 index 0000000..0a2326c --- /dev/null +++ b/apps/api/email/errors.go @@ -0,0 +1,8 @@ +package email + +import "github.com/trysupernova/supernova-api/utils" + +const ( + ErrEmailSendFailed utils.AppErrorType = "email_send_failed" + ErrEmailCompileFailed utils.AppErrorType = "email_compile_failed" +) diff --git a/apps/api/email/main.go b/apps/api/email/main.go new file mode 100644 index 0000000..9c6ea2f --- /dev/null +++ b/apps/api/email/main.go @@ -0,0 +1,69 @@ +package email + +import ( + "bytes" + "context" + "embed" + "text/template" + + "github.com/Boostport/mjml-go" + "github.com/resendlabs/resend-go" + "github.com/trysupernova/supernova-api/utils" +) + +type EmailClient struct{} + +func New() *EmailClient { + return &EmailClient{} +} + +type EmailSend struct { + From string + To []string + Subject string + Body string + ReplyTo string +} + +func (e *EmailClient) SendEmail(send EmailSend) (string, error) { + apiKey := utils.GetConfig().RESEND_API_KEY + client := resend.NewClient(apiKey) + params := &resend.SendEmailRequest{ + To: send.To, + From: send.From, + Html: send.Body, + Subject: send.Subject, + ReplyTo: send.ReplyTo, + } + + sent, err := client.Emails.Send(params) + if err != nil { + return "", utils.NewAppError(ErrEmailSendFailed, "Failed to send email: "+err.Error()) + } + return sent.Id, nil +} + +//go:embed templates/* +var resources embed.FS + +var tmpl = template.Must(template.ParseFS(resources, "templates/*")) + +func CompileEmailForgotPassword(resetUrl string) (string, error) { + // open a file + // read the contents of the file into a string + var result bytes.Buffer + err := tmpl.Execute(&result, struct { + Url string + }{ + Url: resetUrl, + }) + if err != nil { + return "", err + } + renderedMjml := result.String() + renderedHtml, err := mjml.ToHTML(context.Background(), renderedMjml, mjml.WithMinify(true)) + if err != nil { + return "", utils.NewAppError(ErrEmailSendFailed, "Failed to compile email template to HTML: "+err.Error()) + } + return renderedHtml, nil +} diff --git a/apps/api/email/templates/forgot-password.mjml b/apps/api/email/templates/forgot-password.mjml new file mode 100644 index 0000000..170b49c --- /dev/null +++ b/apps/api/email/templates/forgot-password.mjml @@ -0,0 +1,13 @@ + + + + + Forgot your password? + Please click on the following link to reset your password: + + Reset Password + + + + + \ No newline at end of file diff --git a/apps/api/email/templates/verify-email.mjml b/apps/api/email/templates/verify-email.mjml new file mode 100644 index 0000000..9981a51 --- /dev/null +++ b/apps/api/email/templates/verify-email.mjml @@ -0,0 +1,12 @@ + + + + + + + Hello World + + + + + \ No newline at end of file diff --git a/apps/api/go.mod b/apps/api/go.mod index ff45f95..ebb8dfe 100644 --- a/apps/api/go.mod +++ b/apps/api/go.mod @@ -6,6 +6,7 @@ require ( ariga.io/atlas-go-sdk v0.1.0 ariga.io/atlas-provider-gorm v0.1.0 github.com/dgrijalva/jwt-go v3.2.0+incompatible + github.com/golang-jwt/jwt/v5 v5.0.0 github.com/gorilla/mux v1.8.0 github.com/joho/godotenv v1.5.1 golang.org/x/crypto v0.12.0 @@ -16,14 +17,18 @@ require ( require ( ariga.io/atlas v0.12.1 // indirect + github.com/Boostport/mjml-go v0.14.3 // indirect github.com/agext/levenshtein v1.2.1 // indirect github.com/alecthomas/kong v0.8.0 // indirect + github.com/andybalholm/brotli v1.0.5 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect + github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/felixge/httpsnoop v1.0.1 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-openapi/inflect v0.19.0 // indirect github.com/go-sql-driver/mysql v1.7.1 // indirect - github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/google/go-cmp v0.5.9 // indirect github.com/gorilla/handlers v1.5.1 // indirect github.com/hashicorp/hcl v1.0.0 // indirect @@ -31,6 +36,7 @@ require ( github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect github.com/jackc/pgx/v5 v5.4.3 // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect github.com/jinzhu/gorm v1.9.16 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.5 // indirect @@ -39,14 +45,18 @@ require ( github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/redis/go-redis/v9 v9.1.0 // indirect + github.com/resendlabs/resend-go v1.7.0 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect github.com/spf13/viper v1.16.0 // indirect github.com/subosito/gotenv v1.4.2 // indirect + github.com/tetratelabs/wazero v1.2.1 // indirect github.com/zclconf/go-cty v1.8.0 // indirect golang.org/x/mod v0.12.0 // indirect + golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.11.0 // indirect golang.org/x/text v0.12.0 // indirect golang.org/x/tools v0.12.0 // indirect diff --git a/apps/api/go.sum b/apps/api/go.sum index 10492fb..628f7fa 100644 --- a/apps/api/go.sum +++ b/apps/api/go.sum @@ -42,6 +42,8 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +github.com/Boostport/mjml-go v0.14.3 h1:9IrvOzyXYhCAMbuQjZ57exLFu9yL4q1xC24YjUwwkzg= +github.com/Boostport/mjml-go v0.14.3/go.mod h1:AEETIXG89nBhebuWKZUcntNMRlJWgDHK4pT17nEyC74= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/PuerkitoBio/goquery v1.5.1/go.mod h1:GsLWisAFVj4WgDibEWF4pvYnkVQBpKBKeU+7zCJoLcc= @@ -49,12 +51,18 @@ github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tj github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/kong v0.8.0 h1:ryDCzutfIqJPnNn0omnrgHLbAggDQM2VWHikE1xqK7s= github.com/alecthomas/kong v0.8.0/go.mod h1:n1iCIO2xS46oE8ZfYCNDqdR0b0wZNrXAIAqro/2132U= +github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= +github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= +github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= @@ -67,6 +75,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/denisenkom/go-mssqldb v0.0.0-20191124224453-732737034ffd/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumCAMpl/TFQ4/5kLM= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -169,6 +179,8 @@ github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= github.com/jackc/pgx/v5 v5.4.3 h1:cxFyXhxlvAifxnkKKdlxv8XqUf59tDlYjnV5YYfsJJY= github.com/jackc/pgx/v5 v5.4.3/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jinzhu/gorm v1.9.16 h1:+IyIjPEABKRpsu/F8OvDPy9fyQlgsg2luMV2ZIH5i5o= github.com/jinzhu/gorm v1.9.16/go.mod h1:G3LB3wezTOWM2ITLzPxEXgSkOXAntiLHS7UdBefADcs= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= @@ -202,6 +214,10 @@ github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINE github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/redis/go-redis/v9 v9.1.0 h1:137FnGdk+EQdCbye1FW+qOEcY5S+SpY9T0NiuqvtfMY= +github.com/redis/go-redis/v9 v9.1.0/go.mod h1:urWj3He21Dj5k4TK1y59xH8Uj6ATueP8AH1cY3lZl4c= +github.com/resendlabs/resend-go v1.7.0 h1:DycOqSXtw2q7aB+Nt9DDJUDtaYcrNPGn1t5RFposas0= +github.com/resendlabs/resend-go v1.7.0/go.mod h1:yip1STH7Bqfm4fD0So5HgyNbt5taG5Cplc4xXxETyLI= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= @@ -228,6 +244,8 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/tetratelabs/wazero v1.2.1 h1:J4X2hrGzJvt+wqltuvcSjHQ7ujQxA9gb6PeMs4qlUWs= +github.com/tetratelabs/wazero v1.2.1/go.mod h1:wYx2gNRg8/WihJfSDxA1TIL8H+GkfLYm+bIfbblu9VQ= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= @@ -345,6 +363,8 @@ golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= +golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/apps/api/http/response.go b/apps/api/http/response.go index a264373..6aeeaff 100644 --- a/apps/api/http/response.go +++ b/apps/api/http/response.go @@ -4,12 +4,18 @@ import ( "encoding/json" "log" "net/http" + "os" + + "github.com/trysupernova/supernova-api/utils" ) +var customLogErr = log.New(os.Stderr, "[Error]: ", log.LstdFlags) + type ErrorResponse struct { - Error bool `json:"error"` - Message string `json:"message"` - StatusCode int `json:"statusCode"` + Error bool `json:"error"` + Message string `json:"message"` + StatusCode int `json:"statusCode"` + ErrorCode utils.AppErrorType `json:"errorCode,omitempty"` } type SuccessResponse[T any] struct { @@ -22,17 +28,17 @@ type SuccessResponse[T any] struct { HTTP Response handling for errors, Returns valid JSON with error type and response code */ -func NewErrorResponse(w http.ResponseWriter, statusCode int, response string) { +func NewErrorResponse(w http.ResponseWriter, statusCode int, appErr error) { error := ErrorResponse{ - true, - response, - statusCode, + Error: true, + Message: appErr.Error(), + StatusCode: statusCode, } w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) err := json.NewEncoder(w).Encode(&error) if err != nil { - log.Println("Error sending encoded JSON response: ", err) + customLogErr.Println("Error sending encoded JSON response: ", err) return } } @@ -46,7 +52,7 @@ func NewSuccessResponse[T any](w http.ResponseWriter, statusCode int, resp Succe w.WriteHeader(statusCode) err := json.NewEncoder(w).Encode(&resp) if err != nil { - log.Println("Error sending encoded JSON response: ", err) + customLogErr.Println("Error sending encoded JSON response: ", err) return } } diff --git a/apps/api/loader/main.go b/apps/api/loader/main.go index 637899b..f73f1b9 100644 --- a/apps/api/loader/main.go +++ b/apps/api/loader/main.go @@ -7,11 +7,12 @@ import ( _ "ariga.io/atlas-go-sdk/recordriver" "ariga.io/atlas-provider-gorm/gormschema" + "github.com/trysupernova/supernova-api/supernova_tasks" "github.com/trysupernova/supernova-api/user" ) func main() { - stmts, err := gormschema.New("mysql").Load(&user.User{}) + stmts, err := gormschema.New("mysql").Load(&user.User{}, &supernova_tasks.SupernovaTask{}) if err != nil { fmt.Fprintf(os.Stderr, "failed to load gorm schema: %v\n", err) os.Exit(1) diff --git a/apps/api/main.go b/apps/api/main.go index c41a763..5b78015 100644 --- a/apps/api/main.go +++ b/apps/api/main.go @@ -3,11 +3,10 @@ package main import ( "log" "net/http" - "os" - "github.com/joho/godotenv" "github.com/trysupernova/supernova-api/db" "github.com/trysupernova/supernova-api/supernova_tasks" + "github.com/trysupernova/supernova-api/utils" "github.com/gorilla/mux" "github.com/trysupernova/supernova-api/middleware" @@ -16,13 +15,15 @@ import ( ) func main() { - //init router - err := godotenv.Load(".env") - if err != nil { - log.Fatal("Error loading .env file") + //init environments + utils.InitConfig() + if utils.GetConfig().ENVIRONMENT == "prod" { + log.Println("🤖 Running in production mode") + } else { + log.Println("🤖 Running in development mode") } - port := os.Getenv("PORT") + port := utils.GetConfig().PORT if port == "" { port = "8000" } @@ -34,9 +35,14 @@ func main() { sqlDb, _ := db.DB.DB() defer sqlDb.Close() + // setup redis + db.Redis = db.SetupRedis() + defer db.Redis.Close() + //create http server log.Println("🤖 Starting server on port " + port) - log.Fatal(http.ListenAndServe(":"+port, router)) + http.Handle("/", middleware.CORSMiddleware(router)) + log.Fatal(http.ListenAndServe(":"+port, nil)) } /* @@ -48,10 +54,7 @@ func BuildAppRouter() *mux.Router { router := mux.NewRouter() // add middlewares - router.Use(middleware.CORSMiddleware) - router.Use(middleware.LoggingMiddleware) - - //append user routes + //append routes customRouter.AppRoutes = append(customRouter.AppRoutes, user.Routes, supernova_tasks.Routes) for _, route := range customRouter.AppRoutes { @@ -64,6 +67,7 @@ func BuildAppRouter() *mux.Router { var handler http.Handler handler = r.HandlerFunc + handler = middleware.LoggingMiddleware(handler) //check to see if route should be protected with jwt if r.Protected { diff --git a/apps/api/middleware/middleware.go b/apps/api/middleware/middleware.go index 99d408c..19e2a2d 100644 --- a/apps/api/middleware/middleware.go +++ b/apps/api/middleware/middleware.go @@ -1,6 +1,7 @@ package middleware import ( + "errors" "net/http" "os" "strconv" @@ -16,13 +17,13 @@ func JWTMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { tokenString := r.Header.Get("Authorization") if len(tokenString) == 0 { - customHTTP.NewErrorResponse(w, http.StatusUnauthorized, "Authentication failure") + customHTTP.NewErrorResponse(w, http.StatusUnauthorized, errors.New("authentication failure")) return } tokenString = strings.Replace(tokenString, "Bearer ", "", 1) claims, err := VerifyToken(tokenString) if err != nil { - customHTTP.NewErrorResponse(w, http.StatusUnauthorized, "Error verifying JWT token: "+err.Error()) + customHTTP.NewErrorResponse(w, http.StatusUnauthorized, errors.New("error verifying JWT token: "+err.Error())) return } @@ -36,12 +37,15 @@ func JWTMiddleware(next http.Handler) http.Handler { func CORSMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - //allow all origins w.Header().Set("Access-Control-Allow-Origin", "*") - //allow all headers - w.Header().Set("Access-Control-Allow-Headers", "*") - //allow all methods - w.Header().Set("Access-Control-Allow-Methods", "*") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS") + // Check if the request method is OPTIONS (preflight request) + if r.Method == "OPTIONS" { + // Respond with HTTP 200 OK for preflight requests + w.WriteHeader(http.StatusOK) + return + } next.ServeHTTP(w, r) }) } diff --git a/apps/api/migrations/20230830050922.sql b/apps/api/migrations/20230830050922.sql new file mode 100644 index 0000000..304ddc9 --- /dev/null +++ b/apps/api/migrations/20230830050922.sql @@ -0,0 +1,14 @@ +-- Create "supernova_tasks" table +CREATE TABLE `supernova_tasks` ( + `id` bigint unsigned NOT NULL AUTO_INCREMENT, + `title` varchar(255) NOT NULL, + `done` bool NOT NULL, + `expected_duration` bigint unsigned NULL, + `user_id` bigint unsigned NOT NULL, + `created_at` datetime(3) NULL, + `updated_at` datetime(3) NULL, + `deleted_at` datetime(3) NULL, + PRIMARY KEY (`id`), + INDEX `fk_users_tasks` (`user_id`), + CONSTRAINT `fk_users_tasks` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON UPDATE CASCADE ON DELETE CASCADE +) CHARSET utf8mb4 COLLATE utf8mb4_0900_ai_ci; diff --git a/apps/api/migrations/atlas.sum b/apps/api/migrations/atlas.sum index 993c8f0..47a9ac2 100644 --- a/apps/api/migrations/atlas.sum +++ b/apps/api/migrations/atlas.sum @@ -1,2 +1,3 @@ -h1:/sqP1nwVqMf1hzxZUZCkyM+ZvUv02T0dYy900dFj9FQ= +h1:9rVDrK++fU9jHp51HwnoaZqtrZTpwMWImJMmiwf3CKs= 20230828025141.sql h1:ViVWlMQMvOVkjO+HWRd2eXfNqh81Bcc9Jt46JDPA9Ds= +20230830050922.sql h1:dhFy8EMmM44nM/vSGW0T2bFESz2/TGhq95kq4JTVWYw= diff --git a/apps/api/package.json b/apps/api/package.json index e76da61..6ca54ea 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -2,6 +2,7 @@ "name": "@supernova/api", "version": "1.0.0", "description": "Server for Supernova, built with Golang", + "type": "module", "scripts": { "dev": "ENVIRONMENT=dev go run main.go", "build": "go build -o ./bin/supernova-api", @@ -9,10 +10,21 @@ "format": "go fmt ./...", "lint": "golangci-lint run", "db:start": "docker-compose up -d", - "db:migrate": "go run scripts/migrate.go", + "db:migrate": "go run scripts/main.go --tool migrate", "db:diff": "atlas migrate diff --env gorm" }, - "keywords": [], - "author": "", - "license": "ISC" + "keywords": [ + "productivity", + "golang", + "api" + ], + "author": "Vincent Vu", + "dependencies": { + "handlebars": "^4.7.8", + "mjml": "^4.14.1" + }, + "devDependencies": { + "@types/mjml": "^4.7.1", + "@types/node": "^20.4.9" + } } diff --git a/apps/api/scripts/main.go b/apps/api/scripts/main.go new file mode 100644 index 0000000..52c8dbe --- /dev/null +++ b/apps/api/scripts/main.go @@ -0,0 +1,18 @@ +package main + +import ( + "flag" + + scripts "github.com/trysupernova/supernova-api/scripts/src" +) + +func main() { + tool := flag.String("tool", "migrate", "The tool to run") + flag.Parse() + switch *tool { + case "migrate": + scripts.Migrate() + default: + panic("Tool not found") + } +} diff --git a/apps/api/scripts/migrate.go b/apps/api/scripts/src/migrate.go similarity index 90% rename from apps/api/scripts/migrate.go rename to apps/api/scripts/src/migrate.go index 64d06c6..7ddb716 100644 --- a/apps/api/scripts/migrate.go +++ b/apps/api/scripts/src/migrate.go @@ -1,4 +1,4 @@ -package main +package scripts import ( "context" @@ -7,9 +7,13 @@ import ( "ariga.io/atlas-go-sdk/atlasexec" "github.com/trysupernova/supernova-api/db" + "github.com/trysupernova/supernova-api/utils" ) func Migrate() { + // Initialize the configuration. + utils.InitConfig() + // Define the execution context, supplying a migration directory // and potentially an `atlas.hcl` configuration file using `atlasexec.WithHCL`. // Initialize the client. @@ -35,7 +39,3 @@ func Migrate() { // show applied migrations and duration in seconds to 2 decimal places fmt.Printf("🚀 Applied %d migrations in %.2f secs", len(res.Applied), res.End.Sub(res.Start).Seconds()) } - -func main() { - Migrate() -} diff --git a/apps/api/supernova_tasks/handlers.go b/apps/api/supernova_tasks/handlers.go index e752041..4f8b003 100644 --- a/apps/api/supernova_tasks/handlers.go +++ b/apps/api/supernova_tasks/handlers.go @@ -27,7 +27,7 @@ func CreateTaskHandler(w http.ResponseWriter, r *http.Request) { ExpectedDuration uint `json:"expectedDuration,omitempty"` } if err := customHTTP.ParseBodyJSON(r, &taskBody); err != nil { - customHTTP.NewErrorResponse(w, http.StatusBadRequest, "Error: "+err.Error()) + customHTTP.NewErrorResponse(w, http.StatusBadRequest, err) return } var task SupernovaTask @@ -36,13 +36,13 @@ func CreateTaskHandler(w http.ResponseWriter, r *http.Request) { // parse userId from header userIdInt, err := strconv.ParseInt(r.Header.Get("userId"), 10, 64) if err != nil { - customHTTP.NewErrorResponse(w, http.StatusInternalServerError, "Error: "+err.Error()) + customHTTP.NewErrorResponse(w, http.StatusInternalServerError, err) return } task.UserID = uint(userIdInt) err = db.DB.Create(&task).Error if err != nil { - customHTTP.NewErrorResponse(w, http.StatusInternalServerError, "Error: "+err.Error()) + customHTTP.NewErrorResponse(w, http.StatusInternalServerError, err) return } customHTTP.NewSuccessResponse(w, http.StatusOK, customHTTP.SuccessResponse[SupernovaTask]{Message: "Task created", StatusCode: http.StatusOK, Data: task}) diff --git a/apps/api/user/errors.go b/apps/api/user/errors.go new file mode 100644 index 0000000..910dadf --- /dev/null +++ b/apps/api/user/errors.go @@ -0,0 +1,7 @@ +package user + +import "github.com/trysupernova/supernova-api/utils" + +const ( + ErrStaleToken utils.AppErrorType = "stale_token" +) diff --git a/apps/api/user/handlers.go b/apps/api/user/handlers.go index 4c43632..5df129d 100644 --- a/apps/api/user/handlers.go +++ b/apps/api/user/handlers.go @@ -1,11 +1,14 @@ package user import ( + "errors" "log" "net/http" + "github.com/asaskevich/govalidator" "github.com/trysupernova/supernova-api/db" customHTTP "github.com/trysupernova/supernova-api/http" + "gorm.io/gorm" "github.com/gorilla/mux" ) @@ -23,7 +26,7 @@ func CreateHandler(w http.ResponseWriter, r *http.Request) { Password string `json:"password"` } if err := customHTTP.ParseBodyJSON(r, &userBody); err != nil { - customHTTP.NewErrorResponse(w, http.StatusBadRequest, "Error: "+err.Error()) + customHTTP.NewErrorResponse(w, http.StatusBadRequest, err) return } var user User @@ -33,7 +36,7 @@ func CreateHandler(w http.ResponseWriter, r *http.Request) { user.Hash = user.hashPassword(userBody.Password) err := db.DB.Create(&user).Error if err != nil { - customHTTP.NewErrorResponse(w, http.StatusUnauthorized, "Error: "+err.Error()) + customHTTP.NewErrorResponse(w, http.StatusUnauthorized, err) return } customHTTP.NewSuccessResponse(w, http.StatusOK, customHTTP.SuccessResponse[User]{Message: "User created", StatusCode: http.StatusOK, Data: user}) @@ -46,7 +49,7 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { } if err := customHTTP.ParseBodyJSON(r, &userBody); err != nil { log.Println(userBody) - customHTTP.NewErrorResponse(w, http.StatusBadRequest, "Error: "+err.Error()) + customHTTP.NewErrorResponse(w, http.StatusBadRequest, err) return } var user User @@ -54,12 +57,12 @@ func LoginHandler(w http.ResponseWriter, r *http.Request) { if user.checkPassword(userBody.Password) { token, err := user.generateJWT() if err != nil { - customHTTP.NewErrorResponse(w, http.StatusUnauthorized, "Error: "+err.Error()) + customHTTP.NewErrorResponse(w, http.StatusUnauthorized, err) return } customHTTP.NewSuccessResponse(w, http.StatusOK, customHTTP.SuccessResponse[string]{Message: "User logged in", StatusCode: http.StatusOK, Data: token.Token}) } else { - customHTTP.NewErrorResponse(w, http.StatusUnauthorized, "Password incorrect") + customHTTP.NewErrorResponse(w, http.StatusUnauthorized, errors.New("Password incorrect")) return } } @@ -75,3 +78,69 @@ func DeleteHandler(w http.ResponseWriter, r *http.Request) { db.DB.Find(&users) customHTTP.NewSuccessResponse(w, http.StatusOK, customHTTP.SuccessResponse[[]User]{Message: "User deleted", StatusCode: http.StatusOK, Data: users}) } + +func ForgotPasswordHandler(w http.ResponseWriter, r *http.Request) { + var userBody struct { + Email string `json:"email"` + } + if err := customHTTP.ParseBodyJSON(r, &userBody); err != nil { + customHTTP.NewErrorResponse(w, http.StatusBadRequest, err) + return + } + var user User + // if user not found, return error + if err := db.DB.Where("email = ?", userBody.Email).Find(&user).Error; err != nil { + customHTTP.NewErrorResponse(w, http.StatusUnauthorized, errors.New("User not found")) + return + } + // send email for forgot password + err := user.generateAndSendPasswordReset() + if err != nil { + customHTTP.NewErrorResponse(w, http.StatusUnauthorized, err) + return + } + customHTTP.NewSuccessResponse(w, http.StatusOK, customHTTP.SuccessResponse[any]{Message: "Password reset token generated", StatusCode: http.StatusOK}) +} + +func ChangePasswordHandler(w http.ResponseWriter, r *http.Request) { + var body struct { + Password string `json:"password" valid:"required~password is required"` + Token string `json:"token" valid:"required~token is required"` + } + + if err := customHTTP.ParseBodyJSON(r, &body); err != nil { + customHTTP.NewErrorResponse(w, http.StatusBadRequest, err) + return + } + + if _, err := govalidator.ValidateStruct(body); err != nil { + customHTTP.NewErrorResponse(w, http.StatusBadRequest, err) + return + } + + requestingUserId, err := verifyPasswordResetToken(body.Token) + if err != nil { + customHTTP.NewErrorResponse(w, http.StatusBadRequest, err) + return + } + + // update their password + var user User + // find the user first; if not associated to the correct user then + // err out + if err := db.DB.First(&user, requestingUserId).Error; err != nil { + if errors.Is(err, gorm.ErrRecordNotFound) { + customHTTP.NewErrorResponse(w, http.StatusBadRequest, errors.New("User not found")) + return + } + customHTTP.NewErrorResponse(w, http.StatusBadRequest, err) + return + } + user.Hash = user.hashPassword(body.Password) + if err := db.DB.Save(&user).Error; err != nil { + customHTTP.NewErrorResponse(w, http.StatusBadRequest, err) + return + } + + customHTTP.NewSuccessResponse(w, http.StatusOK, customHTTP.SuccessResponse[any]{Message: "Password updated", StatusCode: http.StatusOK}) +} diff --git a/apps/api/user/model.go b/apps/api/user/model.go index deb2ebb..fd3d9c5 100644 --- a/apps/api/user/model.go +++ b/apps/api/user/model.go @@ -1,20 +1,41 @@ package user import ( + "context" + "errors" + "fmt" "os" "time" + "crypto/rand" + "encoding/base64" + "github.com/golang-jwt/jwt/v5" + "github.com/redis/go-redis/v9" + "github.com/trysupernova/supernova-api/db" + "github.com/trysupernova/supernova-api/email" + "github.com/trysupernova/supernova-api/supernova_tasks" + "github.com/trysupernova/supernova-api/utils" "golang.org/x/crypto/bcrypt" "gorm.io/gorm" - "github.com/trysupernova/supernova-api/supernova_tasks" ) +const ( + resetEmailFrom = "Supernova " + resetEmailSubject = "Password Reset" + resetTokenLength = 32 + resetTokenExpiryDuration = 10 * time.Minute +) + +func GetBaseUrlWebApp() string { + return os.Getenv("BASE_URL_WEB_APP") +} + type User struct { - ID uint `gorm:"primaryKey" json:"id"` - Email string `gorm:"type:varchar(255);uniqueIndex;column:email;not null" json:"email"` - Name string `gorm:"type:varchar(255);column:name;not null" json:"name"` - Hash string `gorm:"type:text;column:hash;not null" json:"-"` //hides from any json marshalling output + ID uint `gorm:"primaryKey" json:"id"` + Email string `gorm:"type:varchar(255);uniqueIndex;column:email;not null" json:"email"` + Name string `gorm:"type:varchar(255);column:name;not null" json:"name"` + Hash string `gorm:"type:text;column:hash;not null" json:"-"` //hides from any json marshalling output Tasks []supernova_tasks.SupernovaTask `gorm:"foreignKey:UserID;constraint:OnUpdate:CASCADE,OnDelete:CASCADE;" json:"tasks"` CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"` @@ -45,3 +66,87 @@ func (u User) generateJWT() (JWTToken, error) { tokenString, err := token.SignedString(signingKey) return JWTToken{tokenString}, err } + +func (u User) generateAndSendPasswordReset() error { + // send the token in the email + // when the user clicks the link, we can verify the token and allow them to reset their password + + // generate the token + var token string + var err error + for { + token, err = GenerateRandomID(resetTokenLength) + if err != nil { + return err + } + cmd := db.Redis.Get(context.Background(), token) + if errors.Is(cmd.Err(), redis.Nil) { + // key haven't been used so quit the loop and set the token + break + } + } + if os.Getenv("ENVIRONMENT") == "dev" { + println("reset token generated: " + token) + } + + // create a temporary token that lasts for 10 minutes in our Redis store + // use Redis SetEX command to set the token with an expiration time of 10 minutes + // key = token & value = user ID + // user ID is for us to verify that this token belongs to the user in /verify + cmd := db.Redis.SetEx(context.Background(), token, fmt.Sprint(u.ID), resetTokenExpiryDuration) + if cmd.Err() != nil { + return cmd.Err() + } + + // compile the templated email + compiledEmail, err := email.CompileEmailForgotPassword(fmt.Sprintf("%s/forgot-password/verify?token=%s", GetBaseUrlWebApp(), token)) + if err != nil { + return err + } + + // send the email + emailClient := email.New() + if _, err := emailClient.SendEmail(email.EmailSend{ + From: resetEmailFrom, + To: []string{u.Email}, + Subject: resetEmailSubject, + Body: compiledEmail, + }); err != nil { + return err + } + + return nil +} + +func verifyPasswordResetToken(token string) (string, error) { + // check if token exists in Redis store + // if it does, then we can allow the user to reset their password and delete the token + // if it doesn't, then we can't allow the user to reset their password + cmd := db.Redis.Get(context.Background(), token) + + // key doesn't exist + if errors.Is(cmd.Err(), redis.Nil) { + return "", utils.NewAppError(ErrStaleToken, "Invalid token; token may have expired") + } + + // the value of the key is the user id + // as stored in the generateAndSendPasswordReset function + userId := cmd.Val() + + // delete the token from Redis + icmd := db.Redis.Del(context.Background(), token) + if icmd.Err() != nil { + return "", icmd.Err() + } + return userId, nil +} + +// GenerateRandomID generates a random string of length n +func GenerateRandomID(length int) (string, error) { + randomBytes := make([]byte, length) + _, err := rand.Read(randomBytes) + if err != nil { + return "", err + } + return base64.URLEncoding.EncodeToString(randomBytes)[:length], nil +} diff --git a/apps/api/user/routes.go b/apps/api/user/routes.go index c99757c..caba6c3 100644 --- a/apps/api/user/routes.go +++ b/apps/api/user/routes.go @@ -26,5 +26,19 @@ var Routes = router.RoutePrefix{ HandlerFunc: LoginHandler, Protected: false, }, + { + Name: "ForgotPassword", + Method: "POST", + Pattern: "/forgot-password", + HandlerFunc: ForgotPasswordHandler, + Protected: false, + }, + { + Name: "ChangePassword", + Method: "POST", + Pattern: "/forgot-password/verify", + HandlerFunc: ChangePasswordHandler, + Protected: false, + }, }, -} \ No newline at end of file +} diff --git a/apps/api/utils/config.go b/apps/api/utils/config.go new file mode 100644 index 0000000..9957690 --- /dev/null +++ b/apps/api/utils/config.go @@ -0,0 +1,54 @@ +package utils + +import ( + "log" + "os" + + "github.com/joho/godotenv" +) + +type Config struct { + PORT string + DB_HOST string + DB_NAME string + DB_USERNAME string + DB_PASSWORD string + DB_PORT string + JWT_SECRET string + RESEND_API_KEY string + BASE_URL_WEB_APP string + REDIS_URL string + REDIS_PASSWORD string + ENVIRONMENT string +} + +var config *Config + +func InitConfig() { + //init router + if os.Getenv("ENVIRONMENT") == "dev" || os.Getenv("ENVIRONMENT") == "" { + err := godotenv.Load(".env") + if err != nil { + log.Fatal("Error loading .env file") + } + } + + config = &Config{ + PORT: os.Getenv("PORT"), + DB_HOST: os.Getenv("DB_HOST"), + DB_NAME: os.Getenv("DB_NAME"), + DB_USERNAME: os.Getenv("DB_USERNAME"), + DB_PASSWORD: os.Getenv("DB_PASSWORD"), + DB_PORT: os.Getenv("DB_PORT"), + JWT_SECRET: os.Getenv("JWT_SECRET"), + RESEND_API_KEY: os.Getenv("RESEND_API_KEY"), + BASE_URL_WEB_APP: os.Getenv("BASE_URL_WEB_APP"), + REDIS_URL: os.Getenv("REDIS_URL"), + REDIS_PASSWORD: os.Getenv("REDIS_PASSWORD"), + ENVIRONMENT: os.Getenv("ENVIRONMENT"), + } +} + +func GetConfig() *Config { + return config +} diff --git a/apps/api/utils/errors.go b/apps/api/utils/errors.go new file mode 100644 index 0000000..52ff43b --- /dev/null +++ b/apps/api/utils/errors.go @@ -0,0 +1,23 @@ +package utils + +import ( + "fmt" +) + +type AppErrorType string + +type AppError struct { + Type AppErrorType `json:"type"` + Message string `json:"message"` +} + +func (e *AppError) Error() string { + return fmt.Sprintf("%s: %s", e.Type, e.Message) +} + +func NewAppError(errType AppErrorType, message string) *AppError { + return &AppError{ + Type: errType, + Message: message, + } +} diff --git a/apps/desktop/lib/src/screens/auth.dart b/apps/desktop/lib/src/screens/auth.dart index ccafc6b..715096b 100644 --- a/apps/desktop/lib/src/screens/auth.dart +++ b/apps/desktop/lib/src/screens/auth.dart @@ -1,9 +1,12 @@ import 'package:desktop_flutter/src/providers/auth_providers.dart'; import 'package:desktop_flutter/src/screens/today_view.dart'; import 'package:desktop_flutter/src/services/sup_backend.dart'; +import 'package:desktop_flutter/src/utils/constants.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'package:url_launcher/url_launcher_string.dart'; class AuthScreen extends ConsumerWidget { const AuthScreen({super.key}); @@ -131,7 +134,13 @@ class LoginForm extends StatelessWidget { ); } }, - child: const Text("Login")) + child: const Text("Login")), + TextButton( + onPressed: () { + // ignore: use_build_context_synchronously + launchUrlString("$wwwAppBaseUrl/forgot-password"); + }, + child: const Text("Forgot password?")), ], )); } diff --git a/apps/desktop/lib/src/utils/constants.dart b/apps/desktop/lib/src/utils/constants.dart index 1e017fb..d027dbd 100644 --- a/apps/desktop/lib/src/utils/constants.dart +++ b/apps/desktop/lib/src/utils/constants.dart @@ -4,3 +4,9 @@ final githubAuthorizationEndpoint = Uri.parse('https://github.com/login/oauth/authorize'); final githubTokenEndpoint = Uri.parse('https://github.com/login/oauth/access_token'); + +const environmentMode = + String.fromEnvironment('ENVIRONMENT', defaultValue: "dev"); +final wwwAppBaseUrl = environmentMode == "dev" + ? Uri.parse('http://localhost:3000') + : Uri.parse('https://trysupernova.one'); diff --git a/apps/www/.env.example b/apps/www/.env.example new file mode 100644 index 0000000..b2a9b6c --- /dev/null +++ b/apps/www/.env.example @@ -0,0 +1,2 @@ +NEXT_PUBLIC_API_BASE_URL= +NEXT_PUBLIC_WAITLIST_ID= \ No newline at end of file diff --git a/apps/www/app/forgot-password/page.tsx b/apps/www/app/forgot-password/page.tsx new file mode 100644 index 0000000..deec28e --- /dev/null +++ b/apps/www/app/forgot-password/page.tsx @@ -0,0 +1,102 @@ +"use client"; + +import { useState } from "react"; +import { toast } from "sonner"; +import HorizontalLogo from "../../components/horizontal-logo"; +import { Input } from "../../components/input"; +import { Button } from "../../components/button"; +import { sendForgotPasswordEmail } from "../../services/backend"; +import { Paragraph } from "../../components/typography"; + +const RESET_PASSWORD_EMAIL_FROM = "Supernova "; + +const ForgotPasswordPage = () => { + return ( +
+ +
+ +
+

Forgot Password

+

+ Enter your email below; we will send a reset password link to your + email +

+ +
+
+ ); +}; + +const ForgotPasswordForm = () => { + const [email, setEmail] = useState(""); + const [fetchState, setFetchState] = useState<{ + error: string | null; + success: boolean; + }>({ + error: null, + success: false, + }); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + // send email to user through API + sendForgotPasswordEmail(email) + .then(() => { + toast.success("Email sent to " + email, { + description: + "If you don't see it, check your spam folder for an email from " + + RESET_PASSWORD_EMAIL_FROM, + }); + setFetchState({ + error: null, + success: true, + }); + }) + .catch((e) => { + toast.error(e.message); + setFetchState({ + error: e.message, + success: false, + }); + }); + }; + + return ( +
+ {!fetchState.success ? ( +
+
+ + { + setEmail(ev.target.value); + }} + /> +
+ +
+ ) : ( +
+

Email sent to {email}

+
+ )} + {fetchState.error !== null && ( + + {fetchState.error} + + )} +
+ ); +}; + +export default ForgotPasswordPage; diff --git a/apps/www/app/forgot-password/verify/page.tsx b/apps/www/app/forgot-password/verify/page.tsx new file mode 100644 index 0000000..26d4adf --- /dev/null +++ b/apps/www/app/forgot-password/verify/page.tsx @@ -0,0 +1,145 @@ +"use client"; + +import { useState } from "react"; +import HorizontalLogo from "../../../components/horizontal-logo"; +import { resetPassword } from "../../../services/backend"; +import { useSearchParams } from "next/navigation"; +import { toast } from "sonner"; +import { Button } from "../../../components/button"; +import { Input } from "../../../components/input"; +import { Paragraph } from "../../../components/typography"; + +const VerifyPasswordResetPage = () => { + return ( +
+ +
+ +
+

Enter your new password

+

+ +
+
+ ); +}; + +const VerifyPasswordForm = () => { + const [pwd, setPwd] = useState(""); + const [confirmPwd, setConfirmPwd] = useState(""); + // get the token from the URL + const router = useSearchParams(); + const token = router.get("token"); + const [resetPasswordState, setResetPasswordState] = useState<{ + error: string | null; + success: boolean; + }>({ + error: null, + success: false, + }); + + // TODO: we could technically provide an endpoint to verify the token actually exists + // and is valid, and then show an error message if it's not valid (maybe even via server-side) + // but for now, we'll just check if it's null for simplicity and assume it's valid + // until we denied by the API + if (token === null) { + return ( + + Invalid token. Please try again. + + ); + } + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + // re-enter password and password must match + if (pwd !== confirmPwd) { + toast.error("Passwords do not match", { + description: "Please re-enter your password", + }); + setResetPasswordState({ + error: "Passwords do not match", + success: false, + }); + return; + } + + // send email to user through API + resetPassword(pwd, token) + .then(() => { + toast.success("Reset password successful", { + description: + "Feel free to login to Supernova with your new password (in the desktop app)", + }); + setResetPasswordState({ + error: null, + success: true, + }); + }) + .catch((e) => { + toast.error("Could not reset your password", { + description: "Please try again later", + }); + setResetPasswordState({ + error: e.message, + success: false, + }); + }); + }; + + if (resetPasswordState.success) { + return ( + + Your password has been reset. Feel free to login to Supernova with your + new password (in the desktop app) + + ); + } + + return ( +
+
+
+ + { + setPwd(ev.target.value); + }} + /> +
+
+ + { + setConfirmPwd(ev.target.value); + }} + /> +
+
+ + {resetPasswordState.error !== null && ( + + {resetPasswordState.error} + + )} +
+ ); +}; + +export default VerifyPasswordResetPage; diff --git a/apps/www/app/page.tsx b/apps/www/app/page.tsx index 5ae3af9..3606df9 100644 --- a/apps/www/app/page.tsx +++ b/apps/www/app/page.tsx @@ -11,6 +11,7 @@ import { BsDiscord, BsGithub, BsTwitter } from "react-icons/bs"; import { toast } from "sonner"; import Link from "next/link"; +import HorizontalLogo from "../components/horizontal-logo"; export default function Home() { const [isMobile, setIsMobile] = useState(null); @@ -53,16 +54,7 @@ export default function Home() { return (
-
- Supernova's logo, a ball with linear gradient from left to right, light teal to orange -

Supernova

-
+ Your superhuman productivity sidekick diff --git a/apps/www/components/horizontal-logo.tsx b/apps/www/components/horizontal-logo.tsx new file mode 100644 index 0000000..b7acd48 --- /dev/null +++ b/apps/www/components/horizontal-logo.tsx @@ -0,0 +1,21 @@ +"use client"; + +import Image from "next/image"; +import { H1 } from "./typography"; + +export const HorizontalLogo = () => { + return ( +
+ Supernova's logo, a ball with linear gradient from left to right, light teal to orange +

Supernova

+
+ ); +}; + +export default HorizontalLogo; diff --git a/apps/www/services/backend.ts b/apps/www/services/backend.ts new file mode 100644 index 0000000..e4e693c --- /dev/null +++ b/apps/www/services/backend.ts @@ -0,0 +1,33 @@ +"use client"; + +if (process.env.NEXT_PUBLIC_API_BASE_URL === undefined) { + console.warn("NEXT_PUBLIC_API_BASE_URL is not defined"); +} + +const apiBaseUrl = process.env.NEXT_PUBLIC_API_BASE_URL; + +export const sendForgotPasswordEmail = async (email: string) => { + const response = await fetch(`${apiBaseUrl}/users/forgot-password`, { + method: "POST", + body: JSON.stringify({ email }), + headers: { "Content-Type": "application/json" }, + }); + const data = await response.json(); + if (response.status !== 200) { + throw new Error(data.message); + } + return data; +}; + +export const resetPassword = async (password: string, token: string) => { + const response = await fetch(`${apiBaseUrl}/users/forgot-password/verify`, { + method: "POST", + body: JSON.stringify({ password, token }), + headers: { "Content-Type": "application/json" }, + }); + const data = await response.json(); + if (response.status !== 200) { + throw new Error(data.message); + } + return data; +}; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index adab5aa..cd886f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,7 +20,21 @@ importers: specifier: latest version: 1.10.1 - apps/api: {} + apps/api: + dependencies: + handlebars: + specifier: ^4.7.8 + version: 4.7.8 + mjml: + specifier: ^4.14.1 + version: 4.14.1 + devDependencies: + '@types/mjml': + specifier: ^4.7.1 + version: 4.7.1 + '@types/node': + specifier: ^20.4.9 + version: 20.4.9 apps/desktop: {} @@ -371,6 +385,10 @@ packages: '@nodelib/fs.scandir': 2.1.5 fastq: 1.15.0 + /@one-ini/wasm@0.1.1: + resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==} + dev: false + /@pkgr/utils@2.4.2: resolution: {integrity: sha512-POgTXhjrTfbTV63DiFXav4lBHiICLKKwDeaKn9Nphwj7WH6m0hMMCaJkMyRWjgtPFyRKRVoMXXjczsTQRDEhYw==} engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} @@ -434,7 +452,7 @@ packages: resolution: {integrity: sha512-ZUxbzKl0IfJILTS6t7ip5fQQM/J3TJYubDm3nMbgubNNYS62eXeUpoLUC8/7fJNiFYHTrGPQn7hspDUzIHX3UA==} dependencies: '@types/minimatch': 5.1.2 - '@types/node': 18.16.16 + '@types/node': 20.4.9 dev: true /@types/inquirer@6.5.0: @@ -452,8 +470,14 @@ packages: resolution: {integrity: sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==} dev: true - /@types/node@18.16.16: - resolution: {integrity: sha512-NpaM49IGQQAUlBhHMF82QH80J08os4ZmyF9MkpCzWAGuOHqE4gTEbhzd7L3l5LmWuZ6E0OiC1FweQ4tsiW35+g==} + /@types/mjml-core@4.7.1: + resolution: {integrity: sha512-k5IRafi93tyZBGF+0BTrcBDvG47OueI+Q7TC4V4UjGQn0AMVvL3Y+S26QF/UHMmMJW5r1hxLyv3StX2/+FatFg==} + dev: true + + /@types/mjml@4.7.1: + resolution: {integrity: sha512-if/IM7j3B1ZQucsakrJI4qaU8MWBS1NzQ9PfMY3Bxj4vY1Bp2jMXmVJhAUU2eLvts0a+bFL6Efk182BB/g492Q==} + dependencies: + '@types/mjml-core': 4.7.1 dev: true /@types/node@20.4.9: @@ -484,7 +508,7 @@ packages: /@types/through@0.0.30: resolution: {integrity: sha512-FvnCJljyxhPM3gkRgWmxmDZyAQSiBQQWLI0A0VFL0K7W1oRUrPJSqNO0NvTnLkBcotdlp3lKvaT0JrnyRDkzOg==} dependencies: - '@types/node': 18.16.16 + '@types/node': 20.4.9 dev: true /@typescript-eslint/parser@5.62.0(eslint@8.46.0)(typescript@5.1.6): @@ -549,6 +573,10 @@ packages: eslint-visitor-keys: 3.4.2 dev: false + /abbrev@1.1.1: + resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + dev: false + /acorn-jsx@5.3.2(acorn@7.4.1): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -609,7 +637,6 @@ packages: /ansi-colors@4.1.3: resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} engines: {node: '>=6'} - dev: true /ansi-escapes@4.3.2: resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} @@ -812,6 +839,10 @@ packages: readable-stream: 3.6.2 dev: true + /boolbase@1.0.0: + resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + dev: false + /bplist-parser@0.2.0: resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==} engines: {node: '>= 5.10.0'} @@ -829,7 +860,6 @@ packages: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} dependencies: balanced-match: 1.0.2 - dev: true /braces@3.0.2: resolution: {integrity: sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==} @@ -891,7 +921,6 @@ packages: dependencies: no-case: 2.3.2 upper-case: 1.1.3 - dev: true /camelcase-css@2.0.1: resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==} @@ -945,6 +974,30 @@ packages: resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} dev: true + /cheerio-select@2.1.0: + resolution: {integrity: sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==} + dependencies: + boolbase: 1.0.0 + css-select: 5.1.0 + css-what: 6.1.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + dev: false + + /cheerio@1.0.0-rc.12: + resolution: {integrity: sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==} + engines: {node: '>= 6'} + dependencies: + cheerio-select: 2.1.0 + dom-serializer: 2.0.0 + domhandler: 5.0.3 + domutils: 3.1.0 + htmlparser2: 8.0.2 + parse5: 7.1.2 + parse5-htmlparser2-tree-adapter: 7.0.0 + dev: false + /chokidar@3.5.3: resolution: {integrity: sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==} engines: {node: '>= 8.10.0'} @@ -960,6 +1013,13 @@ packages: fsevents: 2.3.2 dev: false + /clean-css@4.2.4: + resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==} + engines: {node: '>= 4.0'} + dependencies: + source-map: 0.6.1 + dev: false + /clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} @@ -986,6 +1046,14 @@ packages: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} dev: false + /cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + dev: false + /clone@1.0.4: resolution: {integrity: sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==} engines: {node: '>=0.8'} @@ -1013,16 +1081,31 @@ packages: /commander@10.0.1: resolution: {integrity: sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==} engines: {node: '>=14'} - dev: true + + /commander@2.20.3: + resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} + dev: false /commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} dev: false + /commander@6.2.1: + resolution: {integrity: sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==} + engines: {node: '>= 6'} + dev: false + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + /config-chain@1.1.13: + resolution: {integrity: sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==} + dependencies: + ini: 1.3.8 + proto-list: 1.2.4 + dev: false + /constant-case@2.0.0: resolution: {integrity: sha512-eS0N9WwmjTqrOmR3o83F5vW8Z+9R1HnVz3xmzT2PMFug9ly+Au/fxRWlEBSb6LcZwspSsEn9Xs1uw9YgzAg1EQ==} dependencies: @@ -1047,6 +1130,21 @@ packages: shebang-command: 2.0.0 which: 2.0.2 + /css-select@5.1.0: + resolution: {integrity: sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==} + dependencies: + boolbase: 1.0.0 + css-what: 6.1.0 + domhandler: 5.0.3 + domutils: 3.1.0 + nth-check: 2.1.1 + dev: false + + /css-what@6.1.0: + resolution: {integrity: sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==} + engines: {node: '>= 6'} + dev: false + /cssesc@3.0.0: resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} engines: {node: '>=4'} @@ -1147,6 +1245,14 @@ packages: engines: {node: '>=6'} dev: false + /detect-node@2.0.4: + resolution: {integrity: sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==} + dev: false + + /detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + dev: false + /didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} dev: false @@ -1179,19 +1285,86 @@ packages: dependencies: esutils: 2.0.3 + /dom-serializer@1.4.1: + resolution: {integrity: sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==} + dependencies: + domelementtype: 2.3.0 + domhandler: 4.3.1 + entities: 2.2.0 + dev: false + + /dom-serializer@2.0.0: + resolution: {integrity: sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + entities: 4.5.0 + dev: false + + /domelementtype@2.3.0: + resolution: {integrity: sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==} + dev: false + + /domhandler@3.3.0: + resolution: {integrity: sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: false + + /domhandler@4.3.1: + resolution: {integrity: sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: false + + /domhandler@5.0.3: + resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} + engines: {node: '>= 4'} + dependencies: + domelementtype: 2.3.0 + dev: false + + /domutils@2.8.0: + resolution: {integrity: sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==} + dependencies: + dom-serializer: 1.4.1 + domelementtype: 2.3.0 + domhandler: 4.3.1 + dev: false + + /domutils@3.1.0: + resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==} + dependencies: + dom-serializer: 2.0.0 + domelementtype: 2.3.0 + domhandler: 5.0.3 + dev: false + /dot-case@2.1.1: resolution: {integrity: sha512-HnM6ZlFqcajLsyudHq7LeeLDr2rFAVYtDv/hV5qchQEidSck8j9OPUsXY9KwJv/lHMtYlX4DjRQqwFYa+0r8Ug==} dependencies: no-case: 2.3.2 dev: true + /editorconfig@1.0.4: + resolution: {integrity: sha512-L9Qe08KWTlqYMVvMcTIvMAdl1cDUubzRNYL+WfA4bLDMHe4nemKkpmYzkznE1FwLKu0EEmy6obgQKzMJrg4x9Q==} + engines: {node: '>=14'} + hasBin: true + dependencies: + '@one-ini/wasm': 0.1.1 + commander: 10.0.1 + minimatch: 9.0.1 + semver: 7.5.4 + dev: false + /electron-to-chromium@1.4.477: resolution: {integrity: sha512-shUVy6Eawp33dFBFIoYbIwLHrX0IZ857AlH9ug2o4rvbWmpaCUdBpQ5Zw39HRrfzAFm4APJE9V+E2A/WB0YqJw==} dev: false /emoji-regex@8.0.0: resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} - dev: true /emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -1213,6 +1386,15 @@ packages: strip-ansi: 6.0.1 dev: true + /entities@2.2.0: + resolution: {integrity: sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==} + dev: false + + /entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} + dev: false + /es-abstract@1.22.1: resolution: {integrity: sha512-ioRRcXMO6OFyRpyzV3kE1IIBd4WG5/kltnzdxSCqoP8CMGs/Li+M1uF5o7lOkZVFjDs+NLesthnF66Pg/0q0Lw==} engines: {node: '>= 0.4'} @@ -1287,6 +1469,11 @@ packages: engines: {node: '>=6'} dev: false + /escape-goat@3.0.0: + resolution: {integrity: sha512-w3PwNZJwRxlp47QGzhuEBldEqVHHhh8/tIPcl6ecf2Bou99cdAt0knihBV0Ecc7CGxYduXVBDheH1K2oADRlvw==} + engines: {node: '>=10'} + dev: false + /escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -1812,6 +1999,11 @@ packages: resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} dev: false + /get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + dev: false + /get-intrinsic@1.2.1: resolution: {integrity: sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==} dependencies: @@ -1889,6 +2081,17 @@ packages: once: 1.4.0 path-is-absolute: 1.0.1 + /glob@8.1.0: + resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} + engines: {node: '>=12'} + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 5.1.6 + once: 1.4.0 + dev: false + /globals@13.20.0: resolution: {integrity: sha512-Qg5QtVkCy/kv3FUSlu4ukeZDVf9ee0iXLAUYX13gbR17bnejFTzr4iS9bY7kwCf1NztRNm1t91fjOiyx4CSwPQ==} engines: {node: '>=8'} @@ -1952,8 +2155,8 @@ packages: resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} dev: false - /handlebars@4.7.7: - resolution: {integrity: sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==} + /handlebars@4.7.8: + resolution: {integrity: sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==} engines: {node: '>=0.4.7'} hasBin: true dependencies: @@ -1963,7 +2166,6 @@ packages: wordwrap: 1.0.0 optionalDependencies: uglify-js: 3.17.4 - dev: true /has-bigints@1.0.2: resolution: {integrity: sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==} @@ -2007,6 +2209,11 @@ packages: dependencies: function-bind: 1.1.1 + /he@1.2.0: + resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==} + hasBin: true + dev: false + /header-case@1.0.1: resolution: {integrity: sha512-i0q9mkOeSuhXw6bGgiQCCBgY/jlZuV/7dZXyZ9c6LcBrqwvT8eT719E9uxE5LiZftdl+z81Ugbg/VvXV4OJOeQ==} dependencies: @@ -2014,6 +2221,38 @@ packages: upper-case: 1.1.3 dev: true + /html-minifier@4.0.0: + resolution: {integrity: sha512-aoGxanpFPLg7MkIl/DDFYtb0iWz7jMFGqFhvEDZga6/4QTjneiD8I/NXL1x5aaoCp7FSIT6h/OhykDdPsbtMig==} + engines: {node: '>=6'} + hasBin: true + dependencies: + camel-case: 3.0.0 + clean-css: 4.2.4 + commander: 2.20.3 + he: 1.2.0 + param-case: 2.1.1 + relateurl: 0.2.7 + uglify-js: 3.17.4 + dev: false + + /htmlparser2@5.0.1: + resolution: {integrity: sha512-vKZZra6CSe9qsJzh0BjBGXo8dvzNsq/oGvsjfRdOrrryfeD9UOBEEQdeoqCRmKZchF5h2zOBMQ6YuQ0uRUmdbQ==} + dependencies: + domelementtype: 2.3.0 + domhandler: 3.3.0 + domutils: 2.8.0 + entities: 2.2.0 + dev: false + + /htmlparser2@8.0.2: + resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + dependencies: + domelementtype: 2.3.0 + domhandler: 5.0.3 + domutils: 3.1.0 + entities: 4.5.0 + dev: false + /human-signals@2.1.0: resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==} engines: {node: '>=10.17.0'} @@ -2077,7 +2316,6 @@ packages: /ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - dev: true /inquirer@7.3.3: resolution: {integrity: sha512-JG3eIAj5V9CwcGvuOmoo6LB9kbAYT8HXffUl6memuszlwDC/qvFAJw49XJ5NROSFNPxp3iQg1GqkFhaY/CR0IA==} @@ -2193,7 +2431,6 @@ packages: /is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - dev: true /is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} @@ -2331,6 +2568,17 @@ packages: hasBin: true dev: false + /js-beautify@1.14.9: + resolution: {integrity: sha512-coM7xq1syLcMyuVGyToxcj2AlzhkDjmfklL8r0JgJ7A76wyGMpJ1oA35mr4APdYNO/o/4YY8H54NQIJzhMbhBg==} + engines: {node: '>=12'} + hasBin: true + dependencies: + config-chain: 1.1.13 + editorconfig: 1.0.4 + glob: 8.1.0 + nopt: 6.0.0 + dev: false + /js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -2384,6 +2632,20 @@ packages: object.values: 1.1.6 dev: false + /juice@9.1.0: + resolution: {integrity: sha512-odblShmPrUoHUwRuC8EmLji5bPP2MLO1GL+gt4XU3tT2ECmbSrrMjtMQaqg3wgMFP2zvUzdPZGfxc5Trk3Z+fQ==} + engines: {node: '>=10.0.0'} + hasBin: true + dependencies: + cheerio: 1.0.0-rc.12 + commander: 6.2.1 + mensch: 0.3.4 + slick: 1.12.2 + web-resource-inliner: 6.0.1 + transitivePeerDependencies: + - encoding + dev: false + /language-subtag-registry@0.3.22: resolution: {integrity: sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==} dev: false @@ -2430,7 +2692,6 @@ packages: /lodash@4.17.21: resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true /log-symbols@4.1.0: resolution: {integrity: sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==} @@ -2455,7 +2716,6 @@ packages: /lower-case@1.1.4: resolution: {integrity: sha512-2Fgx1Ycm599x+WGpIYwJOvsjmXFzTSc34IwDWALRA/8AopUKAVPwfJ+h5+f85BCp0PWmmJcWzEpxOpoXycMpdA==} - dev: true /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} @@ -2475,6 +2735,10 @@ packages: resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} dev: true + /mensch@0.3.4: + resolution: {integrity: sha512-IAeFvcOnV9V0Yk+bFhYR07O3yNina9ANIN5MoXBKYJ/RLYPurd2d0yw14MDhpr9/momp0WofT1bPUh3hkzdi/g==} + dev: false + /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: false @@ -2490,6 +2754,12 @@ packages: braces: 3.0.2 picomatch: 2.3.1 + /mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + dev: false + /mimic-fn@2.1.0: resolution: {integrity: sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==} engines: {node: '>=6'} @@ -2504,6 +2774,20 @@ packages: dependencies: brace-expansion: 1.1.11 + /minimatch@5.1.6: + resolution: {integrity: sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==} + engines: {node: '>=10'} + dependencies: + brace-expansion: 2.0.1 + dev: false + + /minimatch@9.0.1: + resolution: {integrity: sha512-0jWhJpD/MdhPXwPuiRkCbfYfSKp2qnn2eOc279qI7f+osl/l+prKSrvhg157zSYvx/1nmgn2NqdT6k2Z7zSH9w==} + engines: {node: '>=16 || 14 >=14.17'} + dependencies: + brace-expansion: 2.0.1 + dev: false + /minimatch@9.0.3: resolution: {integrity: sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==} engines: {node: '>=16 || 14 >=14.17'} @@ -2514,6 +2798,369 @@ packages: /minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + /mjml-accordion@4.14.1: + resolution: {integrity: sha512-dpNXyjnhYwhM75JSjD4wFUa9JgHm86M2pa0CoTzdv1zOQz67ilc4BoK5mc2S0gOjJpjBShM5eOJuCyVIuAPC6w==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-body@4.14.1: + resolution: {integrity: sha512-YpXcK3o2o1U+fhI8f60xahrhXuHmav6BZez9vIN3ZEJOxPFSr+qgr1cT2iyFz50L5+ZsLIVj2ZY+ALQjdsg8ig==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-button@4.14.1: + resolution: {integrity: sha512-V1Tl1vQ3lXYvvqHJHvGcc8URr7V1l/ZOsv7iLV4QRrh7kjKBXaRS7uUJtz6/PzEbNsGQCiNtXrODqcijLWlgaw==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-carousel@4.14.1: + resolution: {integrity: sha512-Ku3MUWPk/TwHxVgKEUtzspy/ePaWtN/3z6/qvNik0KIn0ZUIZ4zvR2JtaVL5nd30LHSmUaNj30XMPkCjYiKkFA==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-cli@4.14.1: + resolution: {integrity: sha512-Gy6MnSygFXs0U1qOXTHqBg2vZX2VL/fAacgQzD4MHq4OuybWaTNSzXRwxBXYCxT3IJB874n2Q0Mxp+Xka+tnZg==} + hasBin: true + dependencies: + '@babel/runtime': 7.22.6 + chokidar: 3.5.3 + glob: 7.2.3 + html-minifier: 4.0.0 + js-beautify: 1.14.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + mjml-migrate: 4.14.1 + mjml-parser-xml: 4.14.1 + mjml-validator: 4.13.0 + yargs: 16.2.0 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-column@4.14.1: + resolution: {integrity: sha512-iixVCIX1YJtpQuwG2WbDr7FqofQrlTtGQ4+YAZXGiLThs0En3xNIJFQX9xJ8sgLEGGltyooHiNICBRlzSp9fDg==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-core@4.14.1: + resolution: {integrity: sha512-di88rSfX+8r4r+cEqlQCO7CRM4mYZrfe2wSCu2je38i+ujjkLpF72cgLnjBlSG5aOUCZgYvlsZ85stqIz9LQfA==} + dependencies: + '@babel/runtime': 7.22.6 + cheerio: 1.0.0-rc.12 + detect-node: 2.1.0 + html-minifier: 4.0.0 + js-beautify: 1.14.9 + juice: 9.1.0 + lodash: 4.17.21 + mjml-migrate: 4.14.1 + mjml-parser-xml: 4.14.1 + mjml-validator: 4.13.0 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-divider@4.14.1: + resolution: {integrity: sha512-agqWY0aW2xaMiUOhYKDvcAAfOLalpbbtjKZAl1vWmNkURaoK4L7MgDilKHSJDFUlHGm2ZOArTrq8i6K0iyThBQ==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-group@4.14.1: + resolution: {integrity: sha512-dJt5batgEJ7wxlxzqOfHOI94ABX+8DZBvAlHuddYO4CsLFHYv6XRIArLAMMnAKU76r6p3X8JxYeOjKZXdv49kg==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head-attributes@4.14.1: + resolution: {integrity: sha512-XdUNOp2csK28kBDSistInOyzWNwmu5HDNr4y1Z7vSQ1PfkmiuS6jWG7jHUjdoMhs27e6Leuyyc6a8gWSpqSWrg==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head-breakpoint@4.14.1: + resolution: {integrity: sha512-Qw9l/W/I5Z9p7I4ShgnEpAL9if4472ejcznbBnp+4Gq+sZoPa7iYoEPsa9UCGutlaCh3N3tIi2qKhl9qD8DFxA==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head-font@4.14.1: + resolution: {integrity: sha512-oBYm1gaOdEMjE5BoZouRRD4lCNZ1jcpz92NR/F7xDyMaKCGN6T/+r4S5dq1gOLm9zWqClRHaECdFJNEmrDpZqA==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head-html-attributes@4.14.1: + resolution: {integrity: sha512-vlJsJc1Sm4Ml2XvLmp01zsdmWmzm6+jNCO7X3eYi9ngEh8LjMCLIQOncnOgjqm9uGpQu2EgUhwvYFZP2luJOVg==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head-preview@4.14.1: + resolution: {integrity: sha512-89gQtt3fhl2dkYpHLF5HDQXz/RLpzecU6wmAIT7Dz6etjLGE1dgq2Ay6Bu/OeHjDcT1gbM131zvBwuXw8OydNw==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head-style@4.14.1: + resolution: {integrity: sha512-XryOuf32EDuUCBT2k99C1+H87IOM919oY6IqxKFJCDkmsbywKIum7ibhweJdcxiYGONKTC6xjuibGD3fQTTYNQ==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head-title@4.14.1: + resolution: {integrity: sha512-aIfpmlQdf1eJZSSrFodmlC4g5GudBti2eMyG42M7/3NeLM6anEWoe+UkF/6OG4Zy0tCQ40BDJ5iBZlMsjQICzw==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-head@4.14.1: + resolution: {integrity: sha512-KoCbtSeTAhx05Ugn9TB2UYt5sQinSCb7RGRer5iPQ3CrXj8hT5B5Svn6qvf/GACPkWl4auExHQh+XgLB+r3OEA==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-hero@4.14.1: + resolution: {integrity: sha512-TQJ3yfjrKYGkdEWjHLHhL99u/meKFYgnfJvlo9xeBvRjSM696jIjdqaPHaunfw4CP6d2OpCIMuacgOsvqQMWOA==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-image@4.14.1: + resolution: {integrity: sha512-jfKLPHXuFq83okwlNM1Um/AEWeVDgs2JXIOsWp2TtvXosnRvGGMzA5stKLYdy1x6UfKF4c1ovpMS162aYGp+xQ==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-migrate@4.14.1: + resolution: {integrity: sha512-d+9HKQOhZi3ZFAaFSDdjzJX9eDQGjMf3BArLWNm2okC4ZgfJSpOc77kgCyFV8ugvwc8fFegPnSV60Jl4xtvK2A==} + hasBin: true + dependencies: + '@babel/runtime': 7.22.6 + js-beautify: 1.14.9 + lodash: 4.17.21 + mjml-core: 4.14.1 + mjml-parser-xml: 4.14.1 + yargs: 16.2.0 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-navbar@4.14.1: + resolution: {integrity: sha512-rNy1Kw8CR3WQ+M55PFBAUDz2VEOjz+sk06OFnsnmNjoMVCjo1EV7OFLDAkmxAwqkC8h4zQWEOFY0MBqqoAg7+A==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-parser-xml@4.14.1: + resolution: {integrity: sha512-9WQVeukbXfq9DUcZ8wOsHC6BTdhaVwTAJDYMIQglXLwKwN7I4pTCguDDHy5d0kbbzK5OCVxCdZe+bfVI6XANOQ==} + dependencies: + '@babel/runtime': 7.22.6 + detect-node: 2.0.4 + htmlparser2: 8.0.2 + lodash: 4.17.21 + dev: false + + /mjml-preset-core@4.14.1: + resolution: {integrity: sha512-uUCqK9Z9d39rwB/+JDV2KWSZGB46W7rPQpc9Xnw1DRP7wD7qAfJwK6AZFCwfTgWdSxw0PwquVNcrUS9yBa9uhw==} + dependencies: + '@babel/runtime': 7.22.6 + mjml-accordion: 4.14.1 + mjml-body: 4.14.1 + mjml-button: 4.14.1 + mjml-carousel: 4.14.1 + mjml-column: 4.14.1 + mjml-divider: 4.14.1 + mjml-group: 4.14.1 + mjml-head: 4.14.1 + mjml-head-attributes: 4.14.1 + mjml-head-breakpoint: 4.14.1 + mjml-head-font: 4.14.1 + mjml-head-html-attributes: 4.14.1 + mjml-head-preview: 4.14.1 + mjml-head-style: 4.14.1 + mjml-head-title: 4.14.1 + mjml-hero: 4.14.1 + mjml-image: 4.14.1 + mjml-navbar: 4.14.1 + mjml-raw: 4.14.1 + mjml-section: 4.14.1 + mjml-social: 4.14.1 + mjml-spacer: 4.14.1 + mjml-table: 4.14.1 + mjml-text: 4.14.1 + mjml-wrapper: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-raw@4.14.1: + resolution: {integrity: sha512-9+4wzoXnCtfV6QPmjfJkZ50hxFB4Z8QZnl2Ac0D1Cn3dUF46UkmO5NLMu7UDIlm5DdFyycZrMOwvZS4wv9ksPw==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-section@4.14.1: + resolution: {integrity: sha512-Ik5pTUhpT3DOfB3hEmAWp8rZ0ilWtIivnL8XdUJRfgYE9D+MCRn+reIO+DAoJHxiQoI6gyeKkIP4B9OrQ7cHQw==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-social@4.14.1: + resolution: {integrity: sha512-G44aOZXgZHukirjkeQWTTV36UywtE2YvSwWGNfo/8d+k5JdJJhCIrlwaahyKEAyH63G1B0Zt8b2lEWx0jigYUw==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-spacer@4.14.1: + resolution: {integrity: sha512-5SfQCXTd3JBgRH1pUy6NVZ0lXBiRqFJPVHBdtC3OFvUS3q1w16eaAXlIUWMKTfy8CKhQrCiE6m65kc662ZpYxA==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-table@4.14.1: + resolution: {integrity: sha512-aVBdX3WpyKVGh/PZNn2KgRem+PQhWlvnD00DKxDejRBsBSKYSwZ0t3EfFvZOoJ9DzfHsN0dHuwd6Z18Ps44NFQ==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-text@4.14.1: + resolution: {integrity: sha512-yZuvf5z6qUxEo5CqOhCUltJlR6oySKVcQNHwoV5sneMaKdmBiaU4VDnlYFera9gMD9o3KBHIX6kUg7EHnCwBRQ==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml-validator@4.13.0: + resolution: {integrity: sha512-uURYfyQYtHJ6Qz/1A7/+E9ezfcoISoLZhYK3olsxKRViwaA2Mm8gy/J3yggZXnsUXWUns7Qymycm5LglLEIiQg==} + dependencies: + '@babel/runtime': 7.22.6 + dev: false + + /mjml-wrapper@4.14.1: + resolution: {integrity: sha512-aA5Xlq6d0hZ5LY+RvSaBqmVcLkvPvdhyAv3vQf3G41Gfhel4oIPmkLnVpHselWhV14A0KwIOIAKVxHtSAxyOTQ==} + dependencies: + '@babel/runtime': 7.22.6 + lodash: 4.17.21 + mjml-core: 4.14.1 + mjml-section: 4.14.1 + transitivePeerDependencies: + - encoding + dev: false + + /mjml@4.14.1: + resolution: {integrity: sha512-f/wnWWIVbeb/ge3ff7c/KYYizI13QbGIp03odwwkCThsJsacw4gpZZAU7V4gXY3HxSXP2/q3jxOfaHVbkfNpOQ==} + hasBin: true + dependencies: + '@babel/runtime': 7.22.6 + mjml-cli: 4.14.1 + mjml-core: 4.14.1 + mjml-migrate: 4.14.1 + mjml-preset-core: 4.14.1 + mjml-validator: 4.13.0 + transitivePeerDependencies: + - encoding + dev: false + /mkdirp@0.5.6: resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} hasBin: true @@ -2551,7 +3198,6 @@ packages: /neo-async@2.6.2: resolution: {integrity: sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==} - dev: true /next@13.4.13(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-A3YVbVDNeXLhWsZ8Nf6IkxmNlmTNz0yVg186NJ97tGZqPDdPzTrHotJ+A1cuJm2XfuWPrKOUZILl5iBQkIf8Jw==} @@ -2597,7 +3243,18 @@ packages: resolution: {integrity: sha512-rmTZ9kz+f3rCvK2TD1Ue/oZlns7OGoIWP4fc3llxxRXlOkHKoWPPWJOfFYpITabSow43QJbRIoHQXtt10VldyQ==} dependencies: lower-case: 1.1.4 - dev: true + + /node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + dependencies: + whatwg-url: 5.0.0 + dev: false /node-plop@0.26.3: resolution: {integrity: sha512-Cov028YhBZ5aB7MdMWJEmwyBig43aGL5WT4vdoB28Oitau1zZAcHUn8Sgfk9HM33TqhtLJ9PlM/O0Mv+QpV/4Q==} @@ -2608,7 +3265,7 @@ packages: change-case: 3.1.0 del: 5.1.0 globby: 10.0.2 - handlebars: 4.7.7 + handlebars: 4.7.8 inquirer: 7.3.3 isbinaryfile: 4.0.10 lodash.get: 4.4.2 @@ -2620,6 +3277,14 @@ packages: resolution: {integrity: sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==} dev: false + /nopt@6.0.0: + resolution: {integrity: sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==} + engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} + hasBin: true + dependencies: + abbrev: 1.1.1 + dev: false + /normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -2644,6 +3309,12 @@ packages: path-key: 4.0.0 dev: false + /nth-check@2.1.1: + resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} + dependencies: + boolbase: 1.0.0 + dev: false + /object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -2800,7 +3471,6 @@ packages: resolution: {integrity: sha512-eQE845L6ot89sk2N8liD8HAuH4ca6Vvr7VWAWwt7+kvvG5aBcPmmphQ68JsEG2qa9n1TykS2DLeMt363AAH8/w==} dependencies: no-case: 2.3.2 - dev: true /parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} @@ -2808,6 +3478,19 @@ packages: dependencies: callsites: 3.1.0 + /parse5-htmlparser2-tree-adapter@7.0.0: + resolution: {integrity: sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==} + dependencies: + domhandler: 5.0.3 + parse5: 7.1.2 + dev: false + + /parse5@7.1.2: + resolution: {integrity: sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==} + dependencies: + entities: 4.5.0 + dev: false + /pascal-case@2.0.1: resolution: {integrity: sha512-qjS4s8rBOJa2Xm0jmxXiyh1+OFf6ekCWOvUaRgAQSktzlTbMotS0nmG9gyYAybCWBcuP4fsBeRCKNwGBnMe2OQ==} dependencies: @@ -2966,6 +3649,10 @@ packages: react-is: 16.13.1 dev: false + /proto-list@1.2.4: + resolution: {integrity: sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==} + dev: false + /punycode@2.3.0: resolution: {integrity: sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==} engines: {node: '>=6'} @@ -3065,6 +3752,16 @@ packages: rc: 1.2.8 dev: true + /relateurl@0.2.7: + resolution: {integrity: sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==} + engines: {node: '>= 0.10'} + dev: false + + /require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + dev: false + /require-from-string@2.0.2: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} @@ -3242,6 +3939,10 @@ packages: is-fullwidth-code-point: 3.0.0 dev: true + /slick@1.12.2: + resolution: {integrity: sha512-4qdtOGcBjral6YIBCWJ0ljFSKNLz9KkhbWtuGvUyRowl1kxfuE1x/Z/aJcaiilpb3do9bl5K7/1h9XC5wWpY/A==} + dev: false + /snake-case@2.1.0: resolution: {integrity: sha512-FMR5YoPFwOLuh4rRz92dywJjyKYZNLpMn1R5ujVpIYkbA9p01fq8RMg0FkO4M+Yobt4MjHeLTJVm5xFFBHSV2Q==} dependencies: @@ -3266,7 +3967,6 @@ packages: /source-map@0.6.1: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} - dev: true /sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} @@ -3284,7 +3984,6 @@ packages: emoji-regex: 8.0.0 is-fullwidth-code-point: 3.0.0 strip-ansi: 6.0.1 - dev: true /string.prototype.matchall@4.0.8: resolution: {integrity: sha512-6zOCOcJ+RJAQshcTvXPHoxoQGONa3e/Lqx90wUA+wEzX78sg5Bo+1tQo4N0pohS0erG9qtCqJDjNCQBjeWVxyg==} @@ -3519,6 +4218,10 @@ packages: dependencies: is-number: 7.0.0 + /tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + dev: false + /ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} dev: false @@ -3703,8 +4406,6 @@ packages: engines: {node: '>=0.8.0'} hasBin: true requiresBuild: true - dev: true - optional: true /unbox-primitive@1.0.2: resolution: {integrity: sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==} @@ -3751,7 +4452,6 @@ packages: /upper-case@1.1.3: resolution: {integrity: sha512-WRbjgmYzgXkCV7zNVpy5YgrHgbBv126rMALQQMrmzOVC4GM2waQ9x7xtm8VU+1yF2kWyPzI9zbZ48n4vSxwfSA==} - dev: true /uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -3780,6 +4480,11 @@ packages: resolution: {integrity: sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==} dev: true + /valid-data-url@3.0.1: + resolution: {integrity: sha512-jOWVmzVceKlVVdwjNSenT4PbGghU0SBIizAev8ofZVgivk/TVHXSbNL8LP6M3spZvkR9/QolkyJavGSX5Cs0UA==} + engines: {node: '>=10'} + dev: false + /validate-npm-package-name@5.0.0: resolution: {integrity: sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -3801,6 +4506,31 @@ packages: defaults: 1.0.4 dev: true + /web-resource-inliner@6.0.1: + resolution: {integrity: sha512-kfqDxt5dTB1JhqsCUQVFDj0rmY+4HLwGQIsLPbyrsN9y9WV/1oFDSx3BQ4GfCv9X+jVeQ7rouTqwK53rA/7t8A==} + engines: {node: '>=10.0.0'} + dependencies: + ansi-colors: 4.1.3 + escape-goat: 3.0.0 + htmlparser2: 5.0.1 + mime: 2.6.0 + node-fetch: 2.7.0 + valid-data-url: 3.0.1 + transitivePeerDependencies: + - encoding + dev: false + + /webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + dev: false + + /whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + dev: false + /which-boxed-primitive@1.0.2: resolution: {integrity: sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==} dependencies: @@ -3831,7 +4561,6 @@ packages: /wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} - dev: true /wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} @@ -3840,11 +4569,15 @@ packages: ansi-styles: 4.3.0 string-width: 4.2.3 strip-ansi: 6.0.1 - dev: true /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + /y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + dev: false + /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} @@ -3853,6 +4586,24 @@ packages: engines: {node: '>= 14'} dev: false + /yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + dev: false + + /yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + dependencies: + cliui: 7.0.4 + escalade: 3.1.1 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + dev: false + /yn@3.1.1: resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} engines: {node: '>=6'}