diff --git a/polyculeconnect/controller/ui/approval.go b/polyculeconnect/controller/ui/approval.go new file mode 100644 index 0000000..1853306 --- /dev/null +++ b/polyculeconnect/controller/ui/approval.go @@ -0,0 +1,67 @@ +package ui + +import ( + "net/http" + + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/services/approval" + "github.com/sirupsen/logrus" +) + +const ( + ApprovalRoute = "/approval" + rememberKey = "remember" +) + +type ApprovalController struct { + l *logrus.Logger + downstreamController http.Handler + srv approval.ApprovalService +} + +func NewApprovalController(l *logrus.Logger, downstream http.Handler, srv approval.ApprovalService) *ApprovalController { + return &ApprovalController{ + l: l, + downstreamController: downstream, + srv: srv, + } +} + +func (ac *ApprovalController) handleGetApproval(r *http.Request) { + ac.l.Debug("Checking if approval is remembered") + + remembered, err := ac.srv.IsRemembered(r.Context(), approval.Claim{Email: "kilgore@kilgore.trout", ClientID: "9854d42da9cd91369a293758d514178c73d2b9774971d8965945ab2b81e83e69"}) + if err != nil { + ac.l.Errorf("Failed to check if approval is remembered: %s", err.Error()) + return + } + + if remembered { + ac.l.Info("Approval is remembered, skipping approval page") + return + } else { + ac.l.Info("Approval is not remembered, continuing") + } +} + +func (ac *ApprovalController) handlePostApproval(r *http.Request) { + ac.l.Debug("Handling POST approval request") + if err := r.ParseForm(); err != nil { + ac.l.Errorf("Failed to parse request form: %s", err.Error()) + return + } + + if remember := r.Form.Get(rememberKey); remember == "on" { + if err := ac.srv.Remember(r.Context(), approval.Claim{Email: "kilgore@kilgore.trout", ClientID: "9854d42da9cd91369a293758d514178c73d2b9774971d8965945ab2b81e83e69"}); err != nil { + ac.l.Errorf("Failed to remember approval request: %s", err.Error()) + } + } +} + +func (ac *ApprovalController) ServeHTTP(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPost { + ac.handlePostApproval(r) + } else if r.Method == http.MethodGet { + ac.handleGetApproval(r) + } + ac.downstreamController.ServeHTTP(w, r) +} diff --git a/polyculeconnect/server/server.go b/polyculeconnect/server/server.go index f225e4b..7e3c887 100644 --- a/polyculeconnect/server/server.go +++ b/polyculeconnect/server/server.go @@ -11,6 +11,7 @@ import ( "git.faercol.me/faercol/polyculeconnect/polyculeconnect/config" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/controller/ui" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/middlewares" + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/services/approval" dex_server "github.com/dexidp/dex/server" "github.com/sirupsen/logrus" ) @@ -63,8 +64,11 @@ func New(appConf *config.AppConfig, dexSrv *dex_server.Server, logger *logrus.Lo panic(fmt.Errorf("unexpected listening mode %v", appConf.ServerMode)) } + approvalSrv := approval.New(appConf) + controllers := map[string]http.Handler{ ui.StaticRoute: middlewares.WithLogger(&ui.StaticController{}, logger), + "/approval": middlewares.WithLogger(ui.NewApprovalController(logger, dexSrv, approvalSrv), logger), "/": middlewares.WithLogger(ui.NewIndexController(logger, dexSrv), logger), } diff --git a/polyculeconnect/services/approval/approval.go b/polyculeconnect/services/approval/approval.go new file mode 100644 index 0000000..5d1cee1 --- /dev/null +++ b/polyculeconnect/services/approval/approval.go @@ -0,0 +1,81 @@ +package approval + +import ( + "context" + "database/sql" + "errors" + "fmt" + "time" + + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/config" + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/services/db" +) + +const ( + insertRememberApproval = `INSERT INTO stored_approvals (email, client_id, expiry) VALUES (?, ?, ?)` + getApproval = `SELECT expiry FROM stored_approvalts WHERE email = ? AND client_id = ?` + dbTimeout = 30 * time.Second + approvalExpiration = 30 * 24 * time.Hour +) + +type Claim struct { + Email string + ClientID string +} + +type ApprovalService interface { + Remember(ctx context.Context, claim Claim) error + IsRemembered(ctx context.Context, claim Claim) (bool, error) +} + +type concreteApprovalService struct { + conf *config.AppConfig +} + +func (cas *concreteApprovalService) Remember(ctx context.Context, claim Claim) error { + db, err := db.Connect(cas.conf) + if err != nil { + return fmt.Errorf("failed to connect to db: %w", err) + } + + queryCtx, cancel := context.WithTimeout(ctx, dbTimeout) + defer cancel() + + tx, err := db.BeginTx(queryCtx, nil) + if err != nil { + return fmt.Errorf("failed to begin transaction: %w", err) + } + + expiry := time.Now().UTC().Add(approvalExpiration) + _, err = tx.Exec(insertRememberApproval, claim.Email, claim.ClientID, expiry) + if err != nil { + return fmt.Errorf("failed to insert approval in DB: %w", err) + } + + if err := tx.Commit(); err != nil { + return fmt.Errorf("failed to commit transaction: %w", err) + } + return nil +} + +func (cas *concreteApprovalService) IsRemembered(ctx context.Context, claim Claim) (bool, error) { + db, err := db.Connect(cas.conf) + if err != nil { + return false, fmt.Errorf("failed to connect to db: %w", err) + } + + row := db.QueryRow(getApproval, claim.Email, claim.ClientID) + var expiry time.Time + if err := row.Scan(&expiry); err != nil { + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + return false, fmt.Errorf("failed to run query: %w", err) + } + + return time.Now().UTC().Before(expiry), nil +} + +func New(conf *config.AppConfig) ApprovalService { + return &concreteApprovalService{conf: conf} +} diff --git a/polyculeconnect/services/db/db.go b/polyculeconnect/services/db/db.go new file mode 100644 index 0000000..8d82b32 --- /dev/null +++ b/polyculeconnect/services/db/db.go @@ -0,0 +1,27 @@ +package db + +import ( + "database/sql" + "errors" + "fmt" + + "git.faercol.me/faercol/polyculeconnect/polyculeconnect/config" +) + +type concreteDBService struct { +} + +func connectSQLite(path string) (*sql.DB, error) { + return sql.Open("sqlite3", path) +} + +func Connect(conf *config.AppConfig) (*sql.DB, error) { + switch conf.StorageType { + case string(config.Memory): + return nil, errors.New("no db for memory mode") + case string(config.SQLite): + return connectSQLite(conf.StorageConfig.File) + default: + return nil, fmt.Errorf("unsupported storage mode %q", conf.StorageType) + } +} diff --git a/polyculeconnect/static/scripts/approval.js b/polyculeconnect/static/scripts/approval.js index e87367e..62a6fd2 100644 --- a/polyculeconnect/static/scripts/approval.js +++ b/polyculeconnect/static/scripts/approval.js @@ -1,5 +1,10 @@ -let approvalForm = document.getElementById("approvalform"); +function submitApproval(approve) { + if (approve) { + document.getElementById("approval").value = "approve"; + } else { + document.getElementById("approval").value = "rejected"; + } -approvalForm.addEventListener("submit", (e) => { handleSuccess(); -}); + document.getElementById("approvalform").submit(); +} \ No newline at end of file diff --git a/polyculeconnect/templates/approval.html b/polyculeconnect/templates/approval.html index f9080d8..6e41267 100644 --- a/polyculeconnect/templates/approval.html +++ b/polyculeconnect/templates/approval.html @@ -16,18 +16,19 @@ {{ end }} -