pcmt/modules/db/db.go
surtur 6b45213649
All checks were successful
continuous-integration/drone/push Build is passing
go: add user onboarding, HIBP search functionality
* add user onboarding workflow
* fix user editing (no edits of passwords of regular users after
  onboarding)
* refresh HIBP breach cache in DB on app start-up
* display HIBP breach details
* fix request scheduling to prevent panics (this still needs some love..)
* fix middleware auth
* add TODOs
* update head.tmpl
* reword some error messages
2023-08-24 18:43:24 +02:00

184 lines
4.4 KiB
Go

// Copyright 2023 wanderer <a_mirre at utb dot cz>
// SPDX-License-Identifier: AGPL-3.0-only
package db
import (
"context"
"fmt"
"os"
"git.dotya.ml/mirre-mt/pcmt/ent"
"git.dotya.ml/mirre-mt/pcmt/ent/migrate"
moduser "git.dotya.ml/mirre-mt/pcmt/modules/user"
"git.dotya.ml/mirre-mt/pcmt/slogging"
"golang.org/x/exp/slog"
)
// CtxKey serves as a key to context values for this package.
type CtxKey struct{}
// migrateRecreateColIdx deletes and re-creates db schema.
func migrateRecreateColIdx(ctx context.Context, client *ent.Client) error { //nolint: unused
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
log := *slogger
log.Logger = log.Logger.With(
slog.Group("pcmt extra", slog.String("module", "modules/db")),
)
// deleting data is very drastic and should be done by the user.
// if setup is already found, simply bail.
// _, err := client.Setup.Delete().Exec(ctx)
// if err != nil {
// log.Errorf("failed to delete setup table: %v", err)
// return err
// }
//
// _, err = client.User.Delete().Exec(ctx)
// if err != nil {
// log.Errorf("failed to delete user table: %v", err)
// return err
// }
err := client.Schema.
Create(
ctx,
migrate.WithDropIndex(true),
migrate.WithDropColumn(true),
)
if err != nil {
log.Errorf("failed to recreate schema resources: %v", err)
return err
}
return err
}
// IsSetUp checks if the database has previously been set up.
func IsSetUp(ctx context.Context, client *ent.Client) (bool, error) {
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
log := *slogger
log.Logger = log.Logger.With(
slog.Group("pcmt extra", slog.String("module", "modules/db")),
)
is, err := client.Setup.
Query().
Only(ctx)
if is != nil {
log.Debug("apparently the db has already been set up")
} else {
log.Debug("apparently the db was not yet set up")
}
switch {
case ent.IsNotFound(err) && is == nil:
return false, nil
case ent.IsNotSingular(err):
return true, err
case err != nil:
return false, err
case err == nil && is != nil:
return true, nil
}
return false, nil
}
// PrintMigration prints the upcoming migration to stdout.
func PrintMigration(ctx context.Context, client *ent.Client) error {
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
log := *slogger
log.Logger = log.With(
slog.Group("pcmt extra", slog.String("module", "modules/db")),
)
log.Info("printing the upcoming migration to stdout")
if err := client.Schema.WriteTo(ctx, os.Stdout); err != nil {
log.Errorf("failed to print schema changes: %v", err)
return err
}
return nil
}
// SetUp attempts to automatically migrate DB schema and creates the set-up
// record indicating that the DB has been set up. Optionally and only if the DB
// has not been set up prior, it creates and admin user with the initPasswd and
// the default username and email (see ../user/const.go).
func SetUp(ctx context.Context, client *ent.Client, createAdmin bool, initPasswd string) error {
// TODO: https://entgo.io/blog/2022/05/09/versioned-migrations-sum-file/
if err := client.Schema.Create(ctx); err != nil {
return fmt.Errorf("failed to create schema resources: %v", err)
}
isSetup, err := IsSetUp(ctx, client)
switch {
case err == nil && isSetup:
return nil
case err != nil && isSetup:
return err
case err == nil && !isSetup:
// run the setup in a transaction.
tx, err := client.Tx(ctx)
if err != nil {
return err
}
txClient := tx.Client()
usrCtx := context.WithValue(context.Background(),
moduser.CtxKey{}, ctx.Value(CtxKey{}).(*slogging.Slogger),
)
// use the "doSetUp" below, but give it the transactional client; no
// code changes to "doSetUp".
if err := doSetUp(usrCtx, txClient, createAdmin, initPasswd); err != nil {
return rollback(tx, err)
}
return tx.Commit()
}
// the remaining scenario (not set up and err) simply returns the err.
return err
}
func doSetUp(ctx context.Context, client *ent.Client, createAdmin bool, initPasswd string) error {
if _, err := client.Setup.
Create().
Save(ctx); err != nil {
return err
}
if createAdmin {
if err := moduser.CreateFirst(ctx, client, moduser.AdminUname, moduser.AdminEmail, initPasswd); err != nil {
return err
}
}
return nil
}
// rollback calls to tx.Rollback and wraps the given error with the rollback
// error if occurred.
func rollback(tx *ent.Tx, err error) error {
if rerr := tx.Rollback(); rerr != nil {
err = fmt.Errorf("%w: %v", err, rerr)
}
return err
}