package user import ( "context" "errors" "git.golem15.com/golem15/sm-user-plugin/classes" "git.golem15.com/golem15/sm-user-plugin/models" "git.golem15.com/golem15/summercms/modules/cabana" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/phrasebook" "gorm.io/gorm" ) var _ pact.HasAccountContributions = (*Plugin)(nil) // backendLinkLang prefixes the phrase keys of the account link. const backendLinkLang = "golem15.user::lang.backend_link." // AccountContributions adds the "Frontend account" section to the admin's // My account page (D-04): its status names the linked frontend account, // "link" starts a link request and answers the site URL the administrator // opens to sign in there as the frontend user and confirm (D-05; the link is // written only by that confirmation), and "unlink" removes the link. func (p *Plugin) AccountContributions() []pact.AccountContribution { return []pact.AccountContribution{{ Name: "frontend_account", Label: backendLinkLang + "section", Description: backendLinkLang + "section_comment", Status: p.frontendAccountStatus, Actions: []pact.AccountAction{ { Name: "link", Label: backendLinkLang + "link", Applies: func(ctx context.Context, adminID uint) (bool, error) { link, err := p.backendLinks().LinkFor(ctx, adminID) return link == nil, err }, Run: p.startFrontendLink, }, { Name: "unlink", Label: backendLinkLang + "unlink", Confirm: backendLinkLang + "unlink_confirm", Danger: true, Applies: func(ctx context.Context, adminID uint) (bool, error) { link, err := p.backendLinks().LinkFor(ctx, adminID) return link != nil, err }, Run: p.unlinkOwnFrontendAccount, }, }, }} } // backendLinks returns the published link service, or one over the app. func (p *Plugin) backendLinks() *classes.BackendLinks { if p.app != nil { if links, ok := p.app.Lookup[*classes.BackendLinks](); ok && links != nil { return links } } return &classes.BackendLinks{App: p.app} } // frontendAccountStatus answers "Linked to " or "Not linked" in the // request locale for the signed-in administrator. func (p *Plugin) frontendAccountStatus(ctx context.Context, adminID uint) (string, error) { link, err := p.backendLinks().LinkFor(ctx, adminID) if err != nil { return "", err } if link == nil { return backendLinkLang + "status_unlinked", nil } email, err := p.frontendEmail(ctx, link.UserID) if err != nil { return "", err } const key = backendLinkLang + "status_linked" if p.app != nil { if tr, ok := p.app.Lookup[*phrasebook.Translator](); ok && tr != nil { if text := tr.Get(ctx, key, map[string]string{"email": email}); text != "" && text != key { return text, nil } } } return "Linked to " + email, nil } // frontendEmail reads the email of a frontend user, deactivated or not. func (p *Plugin) frontendEmail(ctx context.Context, userID uint) (string, error) { var db *gorm.DB if tx, ok := cabana.TxFromContext(ctx); ok { db = tx.Session(&gorm.Session{NewDB: true}) } else if p.app != nil { db, _ = p.app.Lookup[*gorm.DB]() } if db == nil { return "", errors.New("golem15.user: gorm handle is missing") } var emails []string if err := db.WithContext(ctx).Unscoped().Model(&models.User{}).Where("id = ?", userID).Pluck("email", &emails).Error; err != nil { return "", err } if len(emails) == 0 { return "", nil } return emails[0], nil } // startFrontendLink creates a link request for the signed-in administrator // and answers the site page that confirms it: // golem15.user.frontend_url (else app.url) + /link-admin?code=. func (p *Plugin) startFrontendLink(ctx context.Context, in pact.AccountActionInput) (pact.AccountActionResult, error) { plain, _, err := p.backendLinks().CreateLinkRequest(ctx, in.AdminID) if err != nil { return pact.AccountActionResult{}, err } return pact.AccountActionResult{ Message: backendLinkLang + "link_started", URL: classes.FrontendURL(p.app) + "/link-admin?code=" + plain, }, nil } // unlinkOwnFrontendAccount removes the signed-in administrator's link in // the action's transaction. func (p *Plugin) unlinkOwnFrontendAccount(ctx context.Context, in pact.AccountActionInput) (pact.AccountActionResult, error) { tx, ok := cabana.TxFromContext(ctx) if !ok { return pact.AccountActionResult{}, errors.New("golem15.user: unlinking needs the action's transaction") } links := p.backendLinks() link, err := links.LinkFor(ctx, in.AdminID) if err != nil { return pact.AccountActionResult{}, err } if err := links.RemoveLink(ctx, tx, link); err != nil { return pact.AccountActionResult{}, err } return pact.AccountActionResult{Message: backendLinkLang + "unlinked"}, nil }