From 6b89796f88c699c31ba1ebd4e25a9bb4b93ce044 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 3 Sep 2022 18:45:10 -0800 Subject: [PATCH 01/15] do end-to-end testing --- .github/workflows/frontend.yaml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml index 34abb7d..be042a2 100644 --- a/.github/workflows/frontend.yaml +++ b/.github/workflows/frontend.yaml @@ -24,6 +24,14 @@ jobs: with: version: 6.0.2 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.18 + + - name: Install Task + uses: arduino/setup-task@v1 + - name: Checkout uses: actions/checkout@v2 with: From 5bf7efe87e98a316b4212890ad3b18508fbbbc70 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 3 Sep 2022 18:47:53 -0800 Subject: [PATCH 02/15] set node version --- .github/workflows/frontend.yaml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/frontend.yaml b/.github/workflows/frontend.yaml index be042a2..db769ae 100644 --- a/.github/workflows/frontend.yaml +++ b/.github/workflows/frontend.yaml @@ -29,8 +29,13 @@ jobs: with: go-version: 1.18 - - name: Install Task - uses: arduino/setup-task@v1 + - uses: actions/setup-node@v3 + with: + node-version: 18 + + - uses: pnpm/action-setup@v2.2.2 + with: + version: 6.0.2 - name: Checkout uses: actions/checkout@v2 From 61483c3ea41f02691445028b7c2bbd394fb8ea97 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 3 Sep 2022 19:10:42 -0800 Subject: [PATCH 03/15] encapsulate notFoundHandler --- backend/app/api/routes.go | 81 ++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 49 deletions(-) diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index 46b8610..37cc2ea 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "io/fs" "mime" "net/http" "path" @@ -26,14 +25,11 @@ const prefix = "/api" // registerRoutes registers all the routes for the API func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux { + registerMimes() + r := chi.NewRouter() a.setGlobalMiddleware(r) - // ========================================================================= - // Base Routes - - DumpEmbedContents() - r.Get("/swagger/*", httpSwagger.Handler( httpSwagger.URL(fmt.Sprintf("%s://%s/swagger/doc.json", a.conf.Swagger.Scheme, a.conf.Swagger.Host)), )) @@ -78,8 +74,7 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux { }) } - r.NotFound(NotFoundHandler) - + r.NotFound(notFoundHandler()) return r } @@ -106,7 +101,7 @@ func (a *app) LogRoutes(r *chi.Mux) { var ErrDir = errors.New("path is dir") -func init() { +func registerMimes() { err := mime.AddExtensionType(".js", "application/javascript") if err != nil { panic(err) @@ -118,48 +113,36 @@ func init() { } } -func tryRead(fs embed.FS, prefix, requestedPath string, w http.ResponseWriter) error { - f, err := fs.Open(path.Join(prefix, requestedPath)) - if err != nil { - return err - } - defer f.Close() - - stat, _ := f.Stat() - if stat.IsDir() { - return ErrDir - } - - contentType := mime.TypeByExtension(filepath.Ext(requestedPath)) - w.Header().Set("Content-Type", contentType) - _, err = io.Copy(w, f) - return err -} - -func NotFoundHandler(w http.ResponseWriter, r *http.Request) { - err := tryRead(public, "public", r.URL.Path, w) - if err == nil { - return - } - log.Debug(). - Str("path", r.URL.Path). - Msg("served from embed not found - serving index.html") - err = tryRead(public, "public", "index.html", w) - if err != nil { - panic(err) - } -} - -func DumpEmbedContents() { - // recursively prints all contents in the embed.FS - err := fs.WalkDir(public, ".", func(path string, d fs.DirEntry, err error) error { +func notFoundHandler() http.HandlerFunc { + tryRead := func(fs embed.FS, prefix, requestedPath string, w http.ResponseWriter) error { + f, err := fs.Open(path.Join(prefix, requestedPath)) if err != nil { return err } - fmt.Println(path) - return nil - }) - if err != nil { - panic(err) + defer f.Close() + + stat, _ := f.Stat() + if stat.IsDir() { + return ErrDir + } + + contentType := mime.TypeByExtension(filepath.Ext(requestedPath)) + w.Header().Set("Content-Type", contentType) + _, err = io.Copy(w, f) + return err + } + + return func(w http.ResponseWriter, r *http.Request) { + err := tryRead(public, "public", r.URL.Path, w) + if err == nil { + return + } + log.Debug(). + Str("path", r.URL.Path). + Msg("served from embed not found - serving index.html") + err = tryRead(public, "public", "index.html", w) + if err != nil { + panic(err) + } } } From c2613a03ca84a983312626944915bb27cbd8fce2 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 3 Sep 2022 19:11:37 -0800 Subject: [PATCH 04/15] define constants at the top --- backend/app/api/routes.go | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index 37cc2ea..1abed0b 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -18,11 +18,15 @@ import ( httpSwagger "github.com/swaggo/http-swagger" // http-swagger middleware ) -//go:embed all:public/* -var public embed.FS - const prefix = "/api" +var ( + ErrDir = errors.New("path is dir") + + //go:embed all:public/* + public embed.FS +) + // registerRoutes registers all the routes for the API func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux { registerMimes() @@ -99,8 +103,6 @@ func (a *app) LogRoutes(r *chi.Mux) { } } -var ErrDir = errors.New("path is dir") - func registerMimes() { err := mime.AddExtensionType(".js", "application/javascript") if err != nil { From 194a90ccfb23c4ea08484b91967e5c32b564bdb4 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 3 Sep 2022 19:14:19 -0800 Subject: [PATCH 05/15] drop seeder and PSQL config --- backend/app/api/main.go | 2 - backend/app/api/seed.go | 96 ------------------- backend/config.template.yml | 14 +-- backend/internal/config/conf.go | 1 - backend/internal/config/conf_database.go | 10 +- backend/internal/config/conf_database_test.go | 10 -- backend/internal/config/conf_seed.go | 14 --- 7 files changed, 4 insertions(+), 143 deletions(-) delete mode 100644 backend/app/api/seed.go delete mode 100644 backend/internal/config/conf_seed.go diff --git a/backend/app/api/main.go b/backend/app/api/main.go index 99a668a..7519ee7 100644 --- a/backend/app/api/main.go +++ b/backend/app/api/main.go @@ -83,8 +83,6 @@ func run(cfg *config.Config) error { routes := app.newRouter(app.repos) app.LogRoutes(routes) - app.SeedDatabase(app.repos) - log.Info().Msgf("Starting HTTP Server on %s:%s", app.server.Host, app.server.Port) // ========================================================================= diff --git a/backend/app/api/seed.go b/backend/app/api/seed.go deleted file mode 100644 index 76d9daa..0000000 --- a/backend/app/api/seed.go +++ /dev/null @@ -1,96 +0,0 @@ -package main - -import ( - "context" - - "github.com/google/uuid" - "github.com/hay-kot/content/backend/ent" - "github.com/hay-kot/content/backend/internal/repo" - "github.com/hay-kot/content/backend/internal/types" - "github.com/hay-kot/content/backend/pkgs/hasher" - "github.com/rs/zerolog/log" -) - -const ( - DefaultGroup = "Default" - DefaultName = "Admin" - DefaultEmail = "admin@admin.com" - DefaultPassword = "admin" -) - -// EnsureAdministrator ensures that there is at least one superuser in the database -// if one isn't found a default is generate using the default credentials -func (a *app) EnsureAdministrator() { - superusers, err := a.repos.Users.GetSuperusers(context.Background()) - - if err != nil { - log.Fatal().Err(err).Msg("failed to get superusers") - } - if len(superusers) > 0 { - return - } - - pw, _ := hasher.HashPassword(DefaultPassword) - newSuperUser := types.UserCreate{ - Name: DefaultName, - Email: DefaultEmail, - IsSuperuser: true, - Password: pw, - } - - log.Info(). - Str("name", newSuperUser.Name). - Str("email", newSuperUser.Email). - Msg("no superusers found, creating default superuser") - - _, err = a.repos.Users.Create(context.Background(), newSuperUser) - - if err != nil { - log.Fatal().Err(err).Msg("failed to create default superuser") - } - -} - -func (a *app) SeedDatabase(repos *repo.AllRepos) { - if !a.conf.Seed.Enabled { - return - } - - group, err := repos.Groups.Create(context.Background(), DefaultGroup) - if err != nil { - log.Fatal().Err(err).Msg("failed to create default group") - } - - for _, seedUser := range a.conf.Seed.Users { - - // Check if User Exists - usr, err := repos.Users.GetOneEmail(context.Background(), seedUser.Email) - if err != nil && !ent.IsNotFound(err) { - log.Fatal().Err(err).Msg("failed to get user") - } - - if usr != nil && usr.ID != uuid.Nil { - log.Info().Str("email", seedUser.Email).Msg("user already exists, skipping") - continue - } - - hashedPw, err := hasher.HashPassword(seedUser.Password) - if err != nil { - log.Fatal().Err(err).Msg("failed to hash password") - } - - _, err = repos.Users.Create(context.Background(), types.UserCreate{ - Name: seedUser.Name, - Email: seedUser.Email, - IsSuperuser: seedUser.IsSuperuser, - Password: hashedPw, - GroupID: group.ID, - }) - - if err != nil { - log.Fatal().Err(err).Msg("failed to create user") - } - - log.Info().Str("email", seedUser.Email).Msg("created user") - } -} diff --git a/backend/config.template.yml b/backend/config.template.yml index 366e2a3..cb699c9 100644 --- a/backend/config.template.yml +++ b/backend/config.template.yml @@ -5,7 +5,7 @@ swagger: scheme: http web: port: 7745 - host: + host: database: driver: sqlite3 sqlite-url: ./ent.db?_fk=1 @@ -18,15 +18,3 @@ mailer: username: password: from: example@email.com -seed: - enabled: true - group: Default - users: - - name: Admin - email: admin@admin.com - password: admin - isSuperuser: true - - name: User - email: user@user.com - password: user - isSuperuser: false diff --git a/backend/internal/config/conf.go b/backend/internal/config/conf.go index 5f6c7e1..79edadd 100644 --- a/backend/internal/config/conf.go +++ b/backend/internal/config/conf.go @@ -22,7 +22,6 @@ type Config struct { Database Database `yaml:"database"` Log LoggerConf `yaml:"logger"` Mailer MailerConf `yaml:"mailer"` - Seed Seed `yaml:"seed"` Swagger SwaggerConf `yaml:"swagger"` } diff --git a/backend/internal/config/conf_database.go b/backend/internal/config/conf_database.go index d8a6c7b..7b396bd 100644 --- a/backend/internal/config/conf_database.go +++ b/backend/internal/config/conf_database.go @@ -1,14 +1,12 @@ package config const ( - DriverSqlite3 = "sqlite3" - DriverPostgres = "postgres" + DriverSqlite3 = "sqlite3" ) type Database struct { - Driver string `yaml:"driver" conf:"default:sqlite3"` - SqliteUrl string `yaml:"sqlite-url" conf:"default:file:ent?mode=memory&cache=shared&_fk=1"` - PostgresUrl string `yaml:"postgres-url" conf:""` + Driver string `yaml:"driver" conf:"default:sqlite3"` + SqliteUrl string `yaml:"sqlite-url" conf:"default:file:ent?mode=memory&cache=shared&_fk=1"` } func (d *Database) GetDriver() string { @@ -19,8 +17,6 @@ func (d *Database) GetUrl() string { switch d.Driver { case DriverSqlite3: return d.SqliteUrl - case DriverPostgres: - return d.PostgresUrl default: panic("unknown database driver") } diff --git a/backend/internal/config/conf_database_test.go b/backend/internal/config/conf_database_test.go index 4720a15..f11c21c 100644 --- a/backend/internal/config/conf_database_test.go +++ b/backend/internal/config/conf_database_test.go @@ -16,16 +16,6 @@ func Test_DatabaseConfig_Sqlite(t *testing.T) { assert.Equal(t, "file:ent?mode=memory&cache=shared&_fk=1", dbConf.GetUrl()) } -func Test_DatabaseConfig_Postgres(t *testing.T) { - dbConf := &Database{ - Driver: DriverPostgres, - PostgresUrl: "postgres://user:pass@host:port/dbname?sslmode=disable", - } - - assert.Equal(t, "postgres", dbConf.GetDriver()) - assert.Equal(t, "postgres://user:pass@host:port/dbname?sslmode=disable", dbConf.GetUrl()) -} - func Test_DatabaseConfig_Unknown(t *testing.T) { dbConf := &Database{ Driver: "null", diff --git a/backend/internal/config/conf_seed.go b/backend/internal/config/conf_seed.go deleted file mode 100644 index 67a409a..0000000 --- a/backend/internal/config/conf_seed.go +++ /dev/null @@ -1,14 +0,0 @@ -package config - -type SeedUser struct { - Name string `yaml:"name"` - Email string `yaml:"email"` - Password string `yaml:"password"` - IsSuperuser bool `yaml:"isSuperuser"` -} - -type Seed struct { - Enabled bool `yaml:"enabled" conf:"default:false"` - Users []SeedUser `yaml:"users"` - Group string `yaml:"group"` -} From e159087e5f738f5e07b89431bfc3607064a2edc3 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 3 Sep 2022 19:17:56 -0800 Subject: [PATCH 06/15] cleanup return --- backend/internal/repo/repo_users.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/backend/internal/repo/repo_users.go b/backend/internal/repo/repo_users.go index 8fc348e..66cd7ac 100644 --- a/backend/internal/repo/repo_users.go +++ b/backend/internal/repo/repo_users.go @@ -28,18 +28,11 @@ func (e *EntUserRepository) GetOneEmail(ctx context.Context, email string) (*ent } func (e *EntUserRepository) GetAll(ctx context.Context) ([]*ent.User, error) { - users, err := e.db.User.Query().WithGroup().All(ctx) - - if err != nil { - return nil, err - } - - return users, nil + return e.db.User.Query().WithGroup().All(ctx) } func (e *EntUserRepository) Create(ctx context.Context, usr types.UserCreate) (*ent.User, error) { err := usr.Validate() - if err != nil { return &ent.User{}, err } @@ -52,7 +45,6 @@ func (e *EntUserRepository) Create(ctx context.Context, usr types.UserCreate) (* SetIsSuperuser(usr.IsSuperuser). SetGroupID(usr.GroupID). Save(ctx) - if err != nil { return entUser, err } From 687282ca680229f7627159d3085f1492609ab090 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 3 Sep 2022 19:27:02 -0800 Subject: [PATCH 07/15] confirm casing --- backend/app/api/app.go | 4 ++-- backend/app/api/logger.go | 41 +++++++++++++++++++++++++++++++++++++++ backend/app/api/main.go | 20 +++++++------------ backend/app/api/routes.go | 6 ++++-- 4 files changed, 54 insertions(+), 17 deletions(-) create mode 100644 backend/app/api/logger.go diff --git a/backend/app/api/app.go b/backend/app/api/app.go index 8e1f742..7176758 100644 --- a/backend/app/api/app.go +++ b/backend/app/api/app.go @@ -20,7 +20,7 @@ type app struct { services *services.AllServices } -func NewApp(conf *config.Config) *app { +func new(conf *config.Config) *app { s := &app{ conf: conf, } @@ -36,7 +36,7 @@ func NewApp(conf *config.Config) *app { return s } -func (a *app) StartBgTask(t time.Duration, fn func()) { +func (a *app) startBgTask(t time.Duration, fn func()) { for { a.server.Background(fn) time.Sleep(t) diff --git a/backend/app/api/logger.go b/backend/app/api/logger.go new file mode 100644 index 0000000..b410cd1 --- /dev/null +++ b/backend/app/api/logger.go @@ -0,0 +1,41 @@ +package main + +import ( + "os" + "strings" + + "github.com/hay-kot/content/backend/internal/config" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" +) + +// setupLogger initializes the zerolog config +// for the shared logger. +func (a *app) setupLogger() { + // Logger Init + // zerolog.TimeFieldFormat = zerolog.TimeFormatUnix + if a.conf.Mode != config.ModeProduction { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + } + + log.Level(getLevel(a.conf.Log.Level)) +} + +func getLevel(l string) zerolog.Level { + switch strings.ToLower(l) { + case "debug": + return zerolog.DebugLevel + case "info": + return zerolog.InfoLevel + case "warn": + return zerolog.WarnLevel + case "error": + return zerolog.ErrorLevel + case "fatal": + return zerolog.FatalLevel + case "panic": + return zerolog.PanicLevel + default: + return zerolog.InfoLevel + } +} diff --git a/backend/app/api/main.go b/backend/app/api/main.go index 7519ee7..d06faa0 100644 --- a/backend/app/api/main.go +++ b/backend/app/api/main.go @@ -2,7 +2,6 @@ package main import ( "context" - "os" "time" "github.com/hay-kot/content/backend/app/api/docs" @@ -12,7 +11,6 @@ import ( "github.com/hay-kot/content/backend/internal/services" "github.com/hay-kot/content/backend/pkgs/server" _ "github.com/mattn/go-sqlite3" - "github.com/rs/zerolog" "github.com/rs/zerolog/log" ) @@ -27,10 +25,6 @@ import ( // @name Authorization // @description "Type 'Bearer TOKEN' to correctly set the API Key" func main() { - // Logger Init - // zerolog.TimeFieldFormat = zerolog.TimeFormatUnix - log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) - log.Level(zerolog.DebugLevel) cfgFile := "config.yml" @@ -47,8 +41,9 @@ func main() { } func run(cfg *config.Config) error { - app := NewApp(cfg) + app := new(cfg) + app.setupLogger() // ========================================================================= // Initialize Database & Repos @@ -75,20 +70,19 @@ func run(cfg *config.Config) error { // ========================================================================= // Start Server - - app.conf.Print() - app.server = server.NewServer(app.conf.Web.Host, app.conf.Web.Port) - routes := app.newRouter(app.repos) - app.LogRoutes(routes) + + if app.conf.Mode != config.ModeDevelopment { + app.logRoutes(routes) + } log.Info().Msgf("Starting HTTP Server on %s:%s", app.server.Host, app.server.Port) // ========================================================================= // Start Reoccurring Tasks - go app.StartBgTask(time.Duration(24)*time.Hour, func() { + go app.startBgTask(time.Duration(24)*time.Hour, func() { _, err := app.repos.AuthTokens.PurgeExpiredTokens(context.Background()) if err != nil { log.Error(). diff --git a/backend/app/api/routes.go b/backend/app/api/routes.go index 1abed0b..1ff79f0 100644 --- a/backend/app/api/routes.go +++ b/backend/app/api/routes.go @@ -82,9 +82,9 @@ func (a *app) newRouter(repos *repo.AllRepos) *chi.Mux { return r } -// LogRoutes logs the routes of the server that are registered within Server.registerRoutes(). This is useful for debugging. +// logRoutes logs the routes of the server that are registered within Server.registerRoutes(). This is useful for debugging. // See https://github.com/go-chi/chi/issues/332 for details and inspiration. -func (a *app) LogRoutes(r *chi.Mux) { +func (a *app) logRoutes(r *chi.Mux) { desiredSpaces := 10 walkFunc := func(method string, route string, handler http.Handler, middleware ...func(http.Handler) http.Handler) error { @@ -115,6 +115,8 @@ func registerMimes() { } } +// notFoundHandler perform the main logic around handling the internal SPA embed and ensuring that +// the client side routing is handled correctly. func notFoundHandler() http.HandlerFunc { tryRead := func(fs embed.FS, prefix, requestedPath string, w http.ResponseWriter) error { f, err := fs.Open(path.Join(prefix, requestedPath)) From efcfbf8c216b9e4e154c9d17a66d30d6b9d570be Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 3 Sep 2022 19:29:20 -0800 Subject: [PATCH 08/15] rename db --- .gitignore | 3 +-- backend/config.template.yml | 5 ++--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index ad0d618..096f82c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,6 @@ # Project Specific -api.log config.yml -ent.db +homebox.db .idea .vscode diff --git a/backend/config.template.yml b/backend/config.template.yml index cb699c9..7ee50a6 100644 --- a/backend/config.template.yml +++ b/backend/config.template.yml @@ -5,13 +5,12 @@ swagger: scheme: http web: port: 7745 - host: + host: "" database: driver: sqlite3 - sqlite-url: ./ent.db?_fk=1 + sqlite-url: ./homebox.db?_fk=1 logger: level: debug - file: api.log mailer: host: smtp.example.com port: 465 From 9b46ea78741ee9348bb96f06308955c88ca4ef28 Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 3 Sep 2022 19:31:36 -0800 Subject: [PATCH 09/15] spacing --- Dockerfile | 3 --- 1 file changed, 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 829d5f2..805420e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,8 +7,6 @@ RUN pnpm install --frozen-lockfile --shamefully-hoist COPY frontend . RUN pnpm build - - # Build API FROM golang:alpine AS builder RUN apk update @@ -20,7 +18,6 @@ COPY --from=frontend-builder app/.output go/src/app/app/api/public RUN go get -d -v ./... RUN CGO_ENABLED=1 GOOS=linux go build -o /go/bin/api -v ./app/api/*.go - # Production Stage FROM alpine:latest From 5f589f95b868c60bd9d10065a215d47d55c12b4c Mon Sep 17 00:00:00 2001 From: Hayden <64056131+hay-kot@users.noreply.github.com> Date: Sat, 3 Sep 2022 22:19:13 -0800 Subject: [PATCH 10/15] inject defaults + cleanup --- backend/app/api/middleware.go | 53 ++++++--- backend/internal/services/service_user.go | 23 +++- .../services/service_user_defaults.go | 55 ++++++++++ frontend/composables/use-api.ts | 28 +++-- frontend/lib/requests/requests.ts | 26 ++--- frontend/pages/home.vue | 2 +- frontend/pages/index.vue | 101 +++++++++--------- frontend/stores/auth.ts | 9 ++ 8 files changed, 212 insertions(+), 85 deletions(-) create mode 100644 backend/internal/services/service_user_defaults.go diff --git a/backend/app/api/middleware.go b/backend/app/api/middleware.go index c062ec2..aec4917 100644 --- a/backend/app/api/middleware.go +++ b/backend/app/api/middleware.go @@ -87,8 +87,21 @@ func mwStripTrailingSlash(next http.Handler) http.Handler { }) } +type StatusRecorder struct { + http.ResponseWriter + Status int +} + +func (r *StatusRecorder) WriteHeader(status int) { + r.Status = status + r.ResponseWriter.WriteHeader(status) +} + func (a *app) mwStructLogger(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + record := &StatusRecorder{ResponseWriter: w, Status: http.StatusOK} + next.ServeHTTP(record, r) + scheme := "http" if r.TLS != nil { scheme = "https" @@ -101,25 +114,35 @@ func (a *app) mwStructLogger(next http.Handler) http.Handler { Str("url", url). Str("method", r.Method). Str("remote_addr", r.RemoteAddr). - Msgf("[%s] %s", r.Method, url) - next.ServeHTTP(w, r) + Int("status", record.Status). + Msg(url) }) } func (a *app) mwSummaryLogger(next http.Handler) http.Handler { - bold := func(s string) string { - return "\033[1m" + s + "\033[0m" - } + bold := func(s string) string { return "\033[1m" + s + "\033[0m" } + orange := func(s string) string { return "\033[33m" + s + "\033[0m" } + aqua := func(s string) string { return "\033[36m" + s + "\033[0m" } + red := func(s string) string { return "\033[31m" + s + "\033[0m" } + green := func(s string) string { return "\033[32m" + s + "\033[0m" } - pink := func(s string) string { - return "\033[35m" + s + "\033[0m" - } - - aqua := func(s string) string { - return "\033[36m" + s + "\033[0m" + fmtCode := func(code int) string { + switch { + case code >= 500: + return red(fmt.Sprintf("%d", code)) + case code >= 400: + return orange(fmt.Sprintf("%d", code)) + case code >= 300: + return aqua(fmt.Sprintf("%d", code)) + default: + return green(fmt.Sprintf("%d", code)) + } } return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + record := &StatusRecorder{ResponseWriter: w, Status: http.StatusOK} + next.ServeHTTP(record, r) // Blocks until the next handler returns. + scheme := "http" if r.TLS != nil { scheme = "https" @@ -127,7 +150,11 @@ func (a *app) mwSummaryLogger(next http.Handler) http.Handler { url := fmt.Sprintf("%s://%s%s %s", scheme, r.Host, r.RequestURI, r.Proto) - log.Info().Msgf("%s %s", bold(pink("["+r.Method+"]")), aqua(url)) - next.ServeHTTP(w, r) + log.Info(). + Msgf("%s %s %s", + bold(orange(""+r.Method+"")), + aqua(url), + bold(fmtCode(record.Status)), + ) }) } diff --git a/backend/internal/services/service_user.go b/backend/internal/services/service_user.go index 80c7d10..b5ced21 100644 --- a/backend/internal/services/service_user.go +++ b/backend/internal/services/service_user.go @@ -23,6 +23,8 @@ type UserService struct { repos *repo.AllRepos } +// RegisterUser creates a new user and group in the data with the provided data. It also bootstraps the user's group +// with default Labels and Locations. func (svc *UserService) RegisterUser(ctx context.Context, data types.UserRegistration) (*types.UserOut, error) { group, err := svc.repos.Groups.Create(ctx, data.GroupName) if err != nil { @@ -38,7 +40,26 @@ func (svc *UserService) RegisterUser(ctx context.Context, data types.UserRegistr GroupID: group.ID, } - return mappers.ToOutUser(svc.repos.Users.Create(ctx, usrCreate)) + usr, err := svc.repos.Users.Create(ctx, usrCreate) + if err != nil { + return &types.UserOut{}, err + } + + for _, label := range defaultLabels() { + _, err := svc.repos.Labels.Create(ctx, group.ID, label) + if err != nil { + return &types.UserOut{}, err + } + } + + for _, location := range defaultLocations() { + _, err := svc.repos.Locations.Create(ctx, group.ID, location) + if err != nil { + return &types.UserOut{}, err + } + } + + return mappers.ToOutUser(usr, nil) } // GetSelf returns the user that is currently logged in based of the token provided within diff --git a/backend/internal/services/service_user_defaults.go b/backend/internal/services/service_user_defaults.go new file mode 100644 index 0000000..185c782 --- /dev/null +++ b/backend/internal/services/service_user_defaults.go @@ -0,0 +1,55 @@ +package services + +import "github.com/hay-kot/content/backend/internal/types" + +func defaultLocations() []types.LocationCreate { + return []types.LocationCreate{ + { + Name: "Living Room", + }, + { + Name: "Garage", + }, + { + Name: "Kitchen", + }, + { + Name: "Bedroom", + }, + { + Name: "Bathroom", + }, + { + Name: "Office", + }, + { + Name: "Attic", + }, + { + Name: "Basement", + }, + } +} + +func defaultLabels() []types.LabelCreate { + return []types.LabelCreate{ + { + Name: "Appliances", + }, + { + Name: "IOT", + }, + { + Name: "Electronics", + }, + { + Name: "Servers", + }, + { + Name: "General", + }, + { + Name: "Important", + }, + } +} diff --git a/frontend/composables/use-api.ts b/frontend/composables/use-api.ts index 3e63488..d80dec7 100644 --- a/frontend/composables/use-api.ts +++ b/frontend/composables/use-api.ts @@ -1,23 +1,31 @@ -import { PublicApi } from "~~/lib/api/public"; -import { UserApi } from "~~/lib/api/user"; -import { Requests } from "~~/lib/requests"; -import { useAuthStore } from "~~/stores/auth"; +import { PublicApi } from '~~/lib/api/public'; +import { UserApi } from '~~/lib/api/user'; +import { Requests } from '~~/lib/requests'; +import { useAuthStore } from '~~/stores/auth'; -async function ApiDebugger(r: Response) { +function ApiDebugger(r: Response) { console.table({ - "Request Url": r.url, - "Response Status": r.status, - "Response Status Text": r.statusText, + 'Request Url': r.url, + 'Response Status': r.status, + 'Response Status Text': r.statusText, }); } export function usePublicApi(): PublicApi { - const requests = new Requests("", "", {}, ApiDebugger); + const requests = new Requests('', '', {}); return new PublicApi(requests); } export function useUserApi(): UserApi { const authStore = useAuthStore(); - const requests = new Requests("", () => authStore.token, {}, ApiDebugger); + + const requests = new Requests('', () => authStore.token, {}); + requests.addResponseInterceptor(ApiDebugger); + requests.addResponseInterceptor(r => { + if (r.status === 401) { + authStore.clearSession(); + } + }); + return new UserApi(requests); } diff --git a/frontend/lib/requests/requests.ts b/frontend/lib/requests/requests.ts index 2d2d9ca..3627d93 100644 --- a/frontend/lib/requests/requests.ts +++ b/frontend/lib/requests/requests.ts @@ -5,6 +5,9 @@ export enum Method { DELETE = 'DELETE', } +export type RequestInterceptor = (r: Response) => void; +export type ResponseInterceptor = (r: Response) => void; + export interface TResponse { status: number; error: boolean; @@ -16,22 +19,24 @@ export class Requests { private baseUrl: string; private token: () => string; private headers: Record = {}; - private logger?: (response: Response) => void; + private responseInterceptors: ResponseInterceptor[] = []; + + addResponseInterceptor(interceptor: ResponseInterceptor) { + this.responseInterceptors.push(interceptor); + } + + private callResponseInterceptors(response: Response) { + this.responseInterceptors.forEach(i => i(response)); + } private url(rest: string): string { return this.baseUrl + rest; } - constructor( - baseUrl: string, - token: string | (() => string) = '', - headers: Record = {}, - logger?: (response: Response) => void - ) { + constructor(baseUrl: string, token: string | (() => string) = '', headers: Record = {}) { this.baseUrl = baseUrl; this.token = typeof token === 'string' ? () => token : token; this.headers = headers; - this.logger = logger; } public get(url: string): Promise> { @@ -73,10 +78,7 @@ export class Requests { } const response = await fetch(this.url(url), args); - - if (this.logger) { - this.logger(response); - } + this.callResponseInterceptors(response); const data: T = await (async () => { if (response.status === 204) { diff --git a/frontend/pages/home.vue b/frontend/pages/home.vue index 9673985..640249c 100644 --- a/frontend/pages/home.vue +++ b/frontend/pages/home.vue @@ -44,7 +44,7 @@