mirror of
https://github.com/PrivateCaptcha/PrivateCaptcha.git
synced 2026-02-09 07:19:08 -06:00
Remove /twofactor endpoint and unite all login forms
This is done to eliminate the redirect to /twofactor endpoint which potentially can land on another server in case load balancing is not sticky
This commit is contained in:
@@ -361,11 +361,20 @@ func (impl *BusinessStoreImpl) CacheUserSession(ctx context.Context, data *sessi
|
||||
return impl.cache.Set(ctx, SessionCacheKey(data.ID()), data)
|
||||
}
|
||||
|
||||
func (impl *BusinessStoreImpl) RetrieveUserSession(ctx context.Context, sid string) (*session.SessionData, error) {
|
||||
func (impl *BusinessStoreImpl) RetrieveUserSession(ctx context.Context, sid string, skipCache bool) (*session.SessionData, error) {
|
||||
if len(sid) == 0 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
|
||||
if skipCache {
|
||||
session, err := impl.doGetSessionbyID(ctx, sid)
|
||||
if err == nil {
|
||||
// yes, it's "skip READ from cache", not "skip cache ENTIRELY"
|
||||
impl.cache.Set(ctx, SessionCacheKey(sid), session)
|
||||
}
|
||||
return session, err
|
||||
}
|
||||
|
||||
reader := &StoreOneReader[string, session.SessionData]{
|
||||
CacheKey: SessionCacheKey(sid),
|
||||
Cache: impl.cache,
|
||||
|
||||
@@ -51,8 +51,8 @@ func (ss *SessionStore) Init(ctx context.Context, session *session.Session) erro
|
||||
return ss.store.Impl().CacheUserSession(ctx, session.Data())
|
||||
}
|
||||
|
||||
func (ss *SessionStore) Read(ctx context.Context, sid string) (*session.Session, error) {
|
||||
sd, err := ss.store.Impl().RetrieveUserSession(ctx, sid)
|
||||
func (ss *SessionStore) Read(ctx context.Context, sid string, skipCache bool) (*session.Session, error) {
|
||||
sd, err := ss.store.Impl().RetrieveUserSession(ctx, sid, skipCache)
|
||||
if err != nil {
|
||||
if (err == ErrNegativeCacheHit) || (err == ErrCacheMiss) {
|
||||
return nil, session.ErrSessionMissing
|
||||
|
||||
@@ -18,7 +18,12 @@ func (s *Server) CreateCsrfContext(user *dbgen.User) CsrfRenderContext {
|
||||
}
|
||||
|
||||
func (s *Server) csrfUserEmailKeyFunc(w http.ResponseWriter, r *http.Request) string {
|
||||
sess := s.Sessions.SessionStart(w, r)
|
||||
// we're using session Get (and not Start) because we don't save session anywhere
|
||||
sess, ok := s.Sessions.SessionGet(r)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
userEmail, ok := sess.Get(ctx, session.KeyUserEmail).(string)
|
||||
if !ok {
|
||||
@@ -29,7 +34,12 @@ func (s *Server) csrfUserEmailKeyFunc(w http.ResponseWriter, r *http.Request) st
|
||||
}
|
||||
|
||||
func (s *Server) csrfUserIDKeyFunc(w http.ResponseWriter, r *http.Request) string {
|
||||
sess := s.Sessions.SessionStart(w, r)
|
||||
// we're using session Get (and not Start) because we don't save session anywhere
|
||||
sess, ok := s.Sessions.SessionGet(r)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
userID, ok := sess.Get(ctx, session.KeyUserID).(int32)
|
||||
if !ok {
|
||||
@@ -62,10 +72,10 @@ func (s *Server) csrf(keyFunc CsrfKeyFunc) alice.Constructor {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
} else {
|
||||
slog.WarnContext(ctx, "Failed to verify CSRF token")
|
||||
slog.WarnContext(ctx, "Failed to verify CSRF token", "path", r.URL.Path, "method", r.Method, "userID", userID)
|
||||
}
|
||||
} else {
|
||||
slog.WarnContext(ctx, "CSRF token is missing")
|
||||
slog.WarnContext(ctx, "CSRF token is missing", "path", r.URL.Path, "method", r.Method)
|
||||
}
|
||||
|
||||
common.Redirect(s.RelURL(common.ExpiredEndpoint), http.StatusUnauthorized, w, r)
|
||||
|
||||
@@ -19,9 +19,10 @@ const (
|
||||
loginStepSignInVerify = 1
|
||||
loginStepSignUpVerify = 2
|
||||
loginStepCompleted = 3
|
||||
loginFormTemplate = "login/form.html"
|
||||
loginTemplate = "login/login.html"
|
||||
loginContentsTemplate = "login/login-contents.html"
|
||||
captchaVerificationFailed = "Captcha verification failed."
|
||||
twofactorContentsTemplate = "login/twofactor-contents.html"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -31,8 +32,12 @@ var (
|
||||
type loginRenderContext struct {
|
||||
CsrfRenderContext
|
||||
CaptchaRenderContext
|
||||
Email string
|
||||
EmailError string
|
||||
CodeError string
|
||||
NameError string
|
||||
CanRegister bool
|
||||
IsRegister bool
|
||||
}
|
||||
|
||||
type portalPropertyOwnerSource struct {
|
||||
@@ -84,14 +89,14 @@ func (s *Server) postLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if len(captchaSolution) == 0 {
|
||||
slog.WarnContext(ctx, "Captcha solution field is empty")
|
||||
data.CaptchaError = "You need to solve captcha to login."
|
||||
s.render(w, r, loginFormTemplate, data)
|
||||
s.render(w, r, loginContentsTemplate, data)
|
||||
return
|
||||
}
|
||||
|
||||
payload, err := s.PuzzleEngine.ParseSolutionPayload(ctx, []byte(captchaSolution))
|
||||
if err != nil {
|
||||
data.CaptchaError = captchaVerificationFailed
|
||||
s.render(w, r, loginFormTemplate, data)
|
||||
s.render(w, r, loginContentsTemplate, data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -100,7 +105,7 @@ func (s *Server) postLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil || !verifyResult.Success() {
|
||||
slog.ErrorContext(ctx, "Failed to verify captcha", "verify", verifyResult.Error.String(), common.ErrAttr(err))
|
||||
data.CaptchaError = captchaVerificationFailed
|
||||
s.render(w, r, loginFormTemplate, data)
|
||||
s.render(w, r, loginContentsTemplate, data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -108,7 +113,7 @@ func (s *Server) postLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if err = checkmail.ValidateFormat(email); err != nil {
|
||||
slog.WarnContext(ctx, "Failed to validate email format", common.ErrAttr(err))
|
||||
data.EmailError = "Email address is not valid."
|
||||
s.render(w, r, loginFormTemplate, data)
|
||||
s.render(w, r, loginContentsTemplate, data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -116,11 +121,11 @@ func (s *Server) postLogin(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil {
|
||||
slog.WarnContext(ctx, "Failed to find user by email", "email", email, common.ErrAttr(err))
|
||||
data.EmailError = "User with such email does not exist."
|
||||
s.render(w, r, loginFormTemplate, data)
|
||||
s.render(w, r, loginContentsTemplate, data)
|
||||
return
|
||||
}
|
||||
|
||||
sess := s.Sessions.SessionStart(w, r)
|
||||
sess, _ := s.Sessions.SessionStart(w, r)
|
||||
if step, ok := sess.Get(ctx, session.KeyLoginStep).(int); ok {
|
||||
if step == loginStepCompleted {
|
||||
slog.DebugContext(ctx, "User seem to be already logged in", "email", email)
|
||||
@@ -145,6 +150,13 @@ func (s *Server) postLogin(w http.ResponseWriter, r *http.Request) {
|
||||
_ = sess.Set(session.KeyUserName, user.Name)
|
||||
_ = sess.Set(session.KeyTwoFactorCode, code)
|
||||
_ = sess.Set(session.KeyUserID, user.ID)
|
||||
// this is needed in case we will be routed to another server that does not have our session in memory
|
||||
// (previously we persisted ONLY logged in sessions, but if we're rerouted during login, it will break)
|
||||
// this should be OK now because we verified that user is a registered user AND they solved captcha
|
||||
_ = sess.Set(session.KeyPersistent, true)
|
||||
|
||||
common.Redirect(s.RelURL(common.TwoFactorEndpoint), http.StatusOK, w, r)
|
||||
data.Token = s.XSRF.Token(email)
|
||||
data.Email = common.MaskEmail(email, '*')
|
||||
|
||||
s.render(w, r, twofactorContentsTemplate, data)
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ func TestPostLogin(t *testing.T) {
|
||||
rr = httptest.NewRecorder()
|
||||
server.postLogin(rr, req)
|
||||
|
||||
if rr.Code != http.StatusSeeOther {
|
||||
if rr.Code != http.StatusOK {
|
||||
t.Errorf("Unexpected post login code: %v", rr.Code)
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,11 @@ func (s *Server) createSystemNotificationContext(ctx context.Context, sess *sess
|
||||
|
||||
func (s *Server) dismissNotification(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
sess := s.Sessions.SessionStart(w, r)
|
||||
sess, found := s.Sessions.SessionGet(r)
|
||||
if !found {
|
||||
http.Error(w, "", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
id, value, err := common.IntPathArg(r, common.ParamID, s.IDHasher)
|
||||
if err == nil {
|
||||
|
||||
@@ -253,7 +253,7 @@ func (s *Server) createOrgDashboardContext(ctx context.Context, orgID int32, ses
|
||||
func (s *Server) getPortal(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
sess := s.Sessions.SessionStart(w, r)
|
||||
sess := s.Session(w, r)
|
||||
|
||||
orgID, _, err := common.IntPathArg(r, common.ParamOrg, s.IDHasher)
|
||||
if err != nil {
|
||||
|
||||
@@ -24,29 +24,22 @@ var (
|
||||
)
|
||||
|
||||
const (
|
||||
registerFormTemplate = "register/form.html"
|
||||
registerTemplate = "register/register.html"
|
||||
userNameErrorMessage = "Name contains invalid characters."
|
||||
registerContentsTemplate = "login/register-contents.html"
|
||||
userNameErrorMessage = "Name contains invalid characters."
|
||||
)
|
||||
|
||||
type registerRenderContext struct {
|
||||
CsrfRenderContext
|
||||
CaptchaRenderContext
|
||||
NameError string
|
||||
EmailError string
|
||||
}
|
||||
|
||||
func (s *Server) getRegister(w http.ResponseWriter, r *http.Request) (Model, string, error) {
|
||||
if !s.canRegister.Load() {
|
||||
return nil, "", errRegistrationDisabled
|
||||
}
|
||||
|
||||
return ®isterRenderContext{
|
||||
return &loginRenderContext{
|
||||
CsrfRenderContext: CsrfRenderContext{
|
||||
Token: s.XSRF.Token(""),
|
||||
},
|
||||
CaptchaRenderContext: s.CreateCaptchaRenderContext(db.PortalRegisterSitekey),
|
||||
}, registerTemplate, nil
|
||||
IsRegister: true,
|
||||
}, loginTemplate, nil
|
||||
}
|
||||
|
||||
func isUserNameValid(name string) bool {
|
||||
@@ -88,15 +81,16 @@ func (s *Server) postRegister(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
data := ®isterRenderContext{
|
||||
data := &loginRenderContext{
|
||||
CsrfRenderContext: CsrfRenderContext{
|
||||
Token: s.XSRF.Token(""),
|
||||
},
|
||||
CaptchaRenderContext: s.CreateCaptchaRenderContext(db.PortalRegisterSitekey),
|
||||
IsRegister: true,
|
||||
}
|
||||
|
||||
if _, termsAndConditions := r.Form[common.ParamTerms]; !termsAndConditions {
|
||||
// it's error because they are marked 'required' on the frontend, so something went terribly wrong
|
||||
// it's an error because they are marked 'required' on the frontend, so something went terribly wrong
|
||||
slog.ErrorContext(ctx, "Terms and conditions were not accepted")
|
||||
s.RedirectError(http.StatusBadRequest, w, r)
|
||||
return
|
||||
@@ -106,14 +100,14 @@ func (s *Server) postRegister(w http.ResponseWriter, r *http.Request) {
|
||||
if len(captchaSolution) == 0 {
|
||||
slog.WarnContext(ctx, "Captcha solution field is empty")
|
||||
data.CaptchaError = "You need to solve captcha to register."
|
||||
s.render(w, r, registerFormTemplate, data)
|
||||
s.render(w, r, registerContentsTemplate, data)
|
||||
return
|
||||
}
|
||||
|
||||
payload, err := s.PuzzleEngine.ParseSolutionPayload(ctx, []byte(captchaSolution))
|
||||
if err != nil {
|
||||
data.CaptchaError = captchaVerificationFailed
|
||||
s.render(w, r, registerFormTemplate, data)
|
||||
s.render(w, r, registerContentsTemplate, data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -122,20 +116,20 @@ func (s *Server) postRegister(w http.ResponseWriter, r *http.Request) {
|
||||
if err != nil || !verifyResult.Success() {
|
||||
slog.ErrorContext(ctx, "Failed to verify captcha", "errors", verifyResult.Error.String(), common.ErrAttr(err))
|
||||
data.CaptchaError = captchaVerificationFailed
|
||||
s.render(w, r, registerFormTemplate, data)
|
||||
s.render(w, r, registerContentsTemplate, data)
|
||||
return
|
||||
}
|
||||
|
||||
name := strings.TrimSpace(r.FormValue(common.ParamName))
|
||||
if len(name) < 3 {
|
||||
data.NameError = "Please use a longer name."
|
||||
s.render(w, r, registerFormTemplate, data)
|
||||
s.render(w, r, registerContentsTemplate, data)
|
||||
return
|
||||
}
|
||||
|
||||
if !isUserNameValid(name) {
|
||||
data.NameError = userNameErrorMessage
|
||||
s.render(w, r, registerFormTemplate, data)
|
||||
s.render(w, r, registerContentsTemplate, data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -143,14 +137,14 @@ func (s *Server) postRegister(w http.ResponseWriter, r *http.Request) {
|
||||
if err := checkmail.ValidateFormat(email); err != nil {
|
||||
slog.WarnContext(ctx, "Failed to validate email format", common.ErrAttr(err))
|
||||
data.EmailError = "Email address is not valid."
|
||||
s.render(w, r, registerFormTemplate, data)
|
||||
s.render(w, r, registerContentsTemplate, data)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := s.Store.Impl().FindUserByEmail(ctx, email); err == nil {
|
||||
slog.WarnContext(ctx, "User with such email already exists", "email", email)
|
||||
data.EmailError = "Such email is already registered. Login instead?"
|
||||
s.render(w, r, registerFormTemplate, data)
|
||||
s.render(w, r, registerContentsTemplate, data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -163,17 +157,22 @@ func (s *Server) postRegister(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
sess := s.Sessions.SessionStart(w, r)
|
||||
sess, _ := s.Sessions.SessionStart(w, r)
|
||||
ctx = context.WithValue(ctx, common.SessionIDContextKey, sess.ID())
|
||||
|
||||
_ = sess.Set(session.KeyLoginStep, loginStepSignUpVerify)
|
||||
_ = sess.Set(session.KeyUserEmail, email)
|
||||
_ = sess.Set(session.KeyUserName, name)
|
||||
_ = sess.Set(session.KeyTwoFactorCode, code)
|
||||
// see comment in postLogin() why we have to use persistent here (although "registered user" argument does not apply)
|
||||
_ = sess.Set(session.KeyPersistent, true)
|
||||
|
||||
data.Token = s.XSRF.Token(email)
|
||||
data.Email = common.MaskEmail(email, '*')
|
||||
|
||||
slog.DebugContext(ctx, "Started 2FA registration flow", "email", email)
|
||||
|
||||
common.Redirect(s.RelURL(common.TwoFactorEndpoint), http.StatusOK, w, r)
|
||||
s.render(w, r, twofactorContentsTemplate, data)
|
||||
}
|
||||
|
||||
func createInternalTrial(plan billing.Plan, status string) *dbgen.CreateSubscriptionParams {
|
||||
|
||||
@@ -153,9 +153,10 @@ func (s *Server) render(w http.ResponseWriter, r *http.Request, name string, dat
|
||||
CDN: s.CDNURL,
|
||||
}
|
||||
|
||||
sess := s.Sessions.SessionStart(w, r)
|
||||
if username, ok := sess.Get(ctx, session.KeyUserName).(string); ok {
|
||||
reqCtx.UserName = username
|
||||
if sess, found := s.Sessions.SessionGet(r); found {
|
||||
if username, ok := sess.Get(ctx, session.KeyUserName).(string); ok {
|
||||
reqCtx.UserName = username
|
||||
}
|
||||
}
|
||||
|
||||
out, err := s.RenderResponse(ctx, name, data, reqCtx)
|
||||
|
||||
@@ -78,14 +78,20 @@ func TestRenderHTML(t *testing.T) {
|
||||
model: &loginRenderContext{CsrfRenderContext: stubToken()},
|
||||
},
|
||||
{
|
||||
path: []string{common.TwoFactorEndpoint},
|
||||
template: twofactorTemplate,
|
||||
model: &twoFactorRenderContext{CsrfRenderContext: stubToken(), Email: "foo@bar.com"},
|
||||
path: []string{common.LoginEndpoint},
|
||||
template: twofactorContentsTemplate,
|
||||
model: &loginRenderContext{CsrfRenderContext: stubToken(), Email: "foo@bar.com"},
|
||||
},
|
||||
{
|
||||
path: []string{common.RegisterEndpoint},
|
||||
template: registerTemplate,
|
||||
model: ®isterRenderContext{CsrfRenderContext: stubToken()},
|
||||
template: loginTemplate,
|
||||
model: &loginRenderContext{CsrfRenderContext: stubToken(), IsRegister: true},
|
||||
},
|
||||
// technically this is not needed (copy of the above), but it's an insurance against typos in case IsRegister will change
|
||||
{
|
||||
path: []string{common.RegisterEndpoint},
|
||||
template: registerContentsTemplate,
|
||||
model: &loginRenderContext{CsrfRenderContext: stubToken(), IsRegister: true},
|
||||
},
|
||||
{
|
||||
path: []string{common.OrgEndpoint, common.NewEndpoint},
|
||||
|
||||
@@ -252,7 +252,6 @@ func (s *Server) setupWithPrefix(router *http.ServeMux, rg *RouteGenerator, secu
|
||||
openRead := public.Append(s.maintenance, publicTimeout)
|
||||
router.Handle(rg.Get(common.LoginEndpoint), openRead.Then(common.Cached(s.Handler(s.getLogin))))
|
||||
router.Handle(rg.Get(common.RegisterEndpoint), openRead.Then(common.Cached(s.Handler(s.getRegister))))
|
||||
router.Handle(rg.Get(common.TwoFactorEndpoint), openRead.ThenFunc(s.getTwoFactor))
|
||||
router.Handle(rg.Get(common.ErrorEndpoint, arg(common.ParamCode)), public.ThenFunc(s.error))
|
||||
router.Handle(rg.Get(common.ExpiredEndpoint), public.ThenFunc(s.expired))
|
||||
router.Handle(rg.Get(common.LogoutEndpoint), public.ThenFunc(s.logout))
|
||||
@@ -382,7 +381,7 @@ func (s *Server) private(next http.Handler) http.Handler {
|
||||
)
|
||||
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sess := s.Sessions.SessionStart(w, r)
|
||||
sess, _ := s.Sessions.SessionStart(w, r)
|
||||
|
||||
ctx := r.Context()
|
||||
ctx = context.WithValue(ctx, common.SessionIDContextKey, sess.ID())
|
||||
|
||||
@@ -17,7 +17,10 @@ func setupSessionSuite(ctx context.Context, manager *session.Manager, t *testing
|
||||
req := httptest.NewRequest("GET", "/settings", nil)
|
||||
w := httptest.NewRecorder()
|
||||
|
||||
sess := manager.SessionStart(w, req)
|
||||
sess, started := manager.SessionStart(w, req)
|
||||
if !started {
|
||||
t.Error("session was not started")
|
||||
}
|
||||
sess.Set(session.KeyUserName, t.Name())
|
||||
sess.Set(session.KeyPersistent, true)
|
||||
|
||||
@@ -77,7 +80,11 @@ func TestPersistentSession(t *testing.T) {
|
||||
req2.AddCookie(cookie)
|
||||
w2 := httptest.NewRecorder()
|
||||
|
||||
sess2 := manager.SessionStart(w2, req2)
|
||||
sess2, started := manager.SessionStart(w2, req2)
|
||||
|
||||
if started {
|
||||
t.Error("new session was started")
|
||||
}
|
||||
|
||||
if sess1.ID() != sess2.ID() {
|
||||
t.Errorf("New session ID (%v) is different from original (%v)", sess2.ID(), sess1.ID())
|
||||
@@ -115,7 +122,11 @@ func TestDeleteSession(t *testing.T) {
|
||||
req3 := httptest.NewRequest("GET", "/about", nil)
|
||||
req3.AddCookie(cookie)
|
||||
w3 := httptest.NewRecorder()
|
||||
sess2 := manager.SessionStart(w3, req3)
|
||||
sess2, started := manager.SessionStart(w3, req3)
|
||||
|
||||
if !started {
|
||||
t.Error("new session was not started")
|
||||
}
|
||||
|
||||
if sess1.ID() != sess2.ID() {
|
||||
t.Errorf("New session ID (%v) is different from original (%v)", sess2.ID(), sess1.ID())
|
||||
|
||||
@@ -151,6 +151,10 @@ func AuthenticateSuite(ctx context.Context, email string, srv *http.ServeMux, xs
|
||||
return nil, fmt.Errorf("unexpected post twofactor code: %v", w.Code)
|
||||
}
|
||||
|
||||
if location, _ := w.Result().Location(); location.String() != "/" {
|
||||
return nil, fmt.Errorf("unexpected redirect: %v", location)
|
||||
}
|
||||
|
||||
slog.Log(ctx, common.LevelTrace, "Looks like we are authenticated", "code", w.Code)
|
||||
|
||||
return cookie, nil
|
||||
|
||||
@@ -11,47 +11,10 @@ import (
|
||||
"github.com/PrivateCaptcha/PrivateCaptcha/pkg/session"
|
||||
)
|
||||
|
||||
const (
|
||||
twofactorTemplate = "twofactor/twofactor.html"
|
||||
)
|
||||
|
||||
var (
|
||||
renderContextNothing = struct{}{}
|
||||
)
|
||||
|
||||
type twoFactorRenderContext struct {
|
||||
CsrfRenderContext
|
||||
Email string
|
||||
Error string
|
||||
}
|
||||
|
||||
func (s *Server) getTwoFactor(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
sess := s.Sessions.SessionStart(w, r)
|
||||
if step, ok := sess.Get(ctx, session.KeyLoginStep).(int); !ok || ((step != loginStepSignInVerify) && (step != loginStepSignUpVerify)) {
|
||||
slog.WarnContext(ctx, "User session is not valid", "step", step, "found", ok)
|
||||
common.Redirect(s.RelURL(common.LoginEndpoint), http.StatusUnauthorized, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
email, ok := sess.Get(ctx, session.KeyUserEmail).(string)
|
||||
if !ok {
|
||||
slog.ErrorContext(ctx, "Failed to get email from session")
|
||||
common.Redirect(s.RelURL(common.LoginEndpoint), http.StatusUnauthorized, w, r)
|
||||
return
|
||||
}
|
||||
|
||||
data := &twoFactorRenderContext{
|
||||
CsrfRenderContext: CsrfRenderContext{
|
||||
Token: s.XSRF.Token(email),
|
||||
},
|
||||
Email: common.MaskEmail(email, '*'),
|
||||
}
|
||||
|
||||
s.render(w, r, twofactorTemplate, data)
|
||||
}
|
||||
|
||||
func (s *Server) postTwoFactor(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
@@ -62,9 +25,19 @@ func (s *Server) postTwoFactor(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
sess := s.Sessions.SessionStart(w, r)
|
||||
sess, started := s.Sessions.SessionStart(w, r)
|
||||
ctx = context.WithValue(ctx, common.SessionIDContextKey, sess.ID())
|
||||
|
||||
// we start session ONLY when session cookie is empty or when DB explicitly returned read error
|
||||
// so "random" POST request to /twofactor might mean we access it from another node without this session
|
||||
if started {
|
||||
slog.DebugContext(ctx, "Attempting to reread potential stale session from DB", "started", started)
|
||||
if dbSess, err := s.Sessions.RetrieveSession(ctx, sess.ID()); err == nil {
|
||||
slog.InfoContext(ctx, "Using DB session instead for two factor")
|
||||
sess.Merge(dbSess)
|
||||
}
|
||||
}
|
||||
|
||||
step, ok := sess.Get(ctx, session.KeyLoginStep).(int)
|
||||
if !ok || ((step != loginStepSignInVerify) && (step != loginStepSignUpVerify)) {
|
||||
slog.WarnContext(ctx, "User session is not valid", "step", step)
|
||||
@@ -86,7 +59,7 @@ func (s *Server) postTwoFactor(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
data := &twoFactorRenderContext{
|
||||
data := &loginRenderContext{
|
||||
CsrfRenderContext: CsrfRenderContext{
|
||||
Token: s.XSRF.Token(email),
|
||||
},
|
||||
@@ -95,9 +68,9 @@ func (s *Server) postTwoFactor(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
formCode := r.FormValue(common.ParamVerificationCode)
|
||||
if enteredCode, err := strconv.Atoi(formCode); (err != nil) || (enteredCode != sentCode) {
|
||||
data.Error = "Code is not valid."
|
||||
data.CodeError = "Code is not valid."
|
||||
slog.WarnContext(ctx, "Code verification failed", "actual", formCode, "expected", sentCode, common.ErrAttr(err))
|
||||
s.render(w, r, "twofactor/form.html", data)
|
||||
s.render(w, r, "login/twofactor-form.html", data)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -145,7 +118,7 @@ func (s *Server) postTwoFactor(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) resend2fa(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
sess := s.Sessions.SessionStart(w, r)
|
||||
sess, _ := s.Sessions.SessionStart(w, r)
|
||||
if step, ok := sess.Get(ctx, session.KeyLoginStep).(int); !ok || ((step != loginStepSignInVerify) && (step != loginStepSignUpVerify)) {
|
||||
slog.WarnContext(ctx, "User session is not valid", "step", step)
|
||||
common.Redirect(s.RelURL(common.LoginEndpoint), http.StatusUnauthorized, w, r)
|
||||
@@ -164,10 +137,10 @@ func (s *Server) resend2fa(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
if err := s.Mailer.SendTwoFactor(ctx, email, code, r.UserAgent(), location); err != nil {
|
||||
slog.ErrorContext(ctx, "Failed to send email message", common.ErrAttr(err))
|
||||
s.render(w, r, "twofactor/resend-error.html", renderContextNothing)
|
||||
s.render(w, r, "login/resend-error.html", renderContextNothing)
|
||||
return
|
||||
}
|
||||
|
||||
_ = sess.Set(session.KeyTwoFactorCode, code)
|
||||
s.render(w, r, "twofactor/resend.html", renderContextNothing)
|
||||
s.render(w, r, "login/resend.html", renderContextNothing)
|
||||
}
|
||||
|
||||
@@ -125,7 +125,12 @@ func (s *Server) Session(w http.ResponseWriter, r *http.Request) *session.Sessio
|
||||
sess, ok := ctx.Value(common.SessionContextKey).(*session.Session)
|
||||
if !ok || (sess == nil) {
|
||||
slog.ErrorContext(ctx, "Failed to get session from context")
|
||||
sess = s.Sessions.SessionStart(w, r)
|
||||
var found bool
|
||||
sess, found = s.Sessions.SessionGet(r)
|
||||
if !found || (sess == nil) {
|
||||
slog.ErrorContext(ctx, "Failed to get started session")
|
||||
sess, _ = s.Sessions.SessionStart(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
return sess
|
||||
|
||||
@@ -104,6 +104,26 @@ func (sd *SessionData) UnmarshalBinary(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sd *SessionData) Merge(from *SessionData) {
|
||||
// Acquire locks in consistent order to prevent deadlock
|
||||
first, second := sd, from
|
||||
if sd.sid > from.sid {
|
||||
first, second = from, sd
|
||||
}
|
||||
|
||||
first.lock.Lock()
|
||||
defer first.lock.Unlock()
|
||||
|
||||
second.lock.Lock()
|
||||
defer second.lock.Unlock()
|
||||
|
||||
for key, value := range from.values {
|
||||
if _, ok := sd.values[key]; !ok {
|
||||
sd.values[key] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (sd *SessionData) ID() string {
|
||||
return sd.sid
|
||||
}
|
||||
@@ -148,6 +168,10 @@ func NewSession(data *SessionData, store Store) *Session {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Session) Merge(from *Session) {
|
||||
s.data.Merge(from.data)
|
||||
}
|
||||
|
||||
func (s *Session) Data() *SessionData {
|
||||
return s.data
|
||||
}
|
||||
@@ -180,7 +204,7 @@ func (s *Session) Delete(key SessionKey) error {
|
||||
type Store interface {
|
||||
Start(ctx context.Context, interval time.Duration)
|
||||
Init(ctx context.Context, session *Session) error
|
||||
Read(ctx context.Context, sid string) (*Session, error)
|
||||
Read(ctx context.Context, sid string, skipCache bool) (*Session, error)
|
||||
Update(session *Session) error
|
||||
Destroy(ctx context.Context, sid string) error
|
||||
}
|
||||
|
||||
@@ -28,7 +28,32 @@ func (m *Manager) Init(svc string, path string, interval time.Duration) {
|
||||
m.Store.Start(context.WithValue(context.Background(), common.ServiceContextKey, svc), interval)
|
||||
}
|
||||
|
||||
func (m *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session *Session) {
|
||||
func (m *Manager) SessionGet(r *http.Request) (*Session, bool) {
|
||||
cookie, err := r.Cookie(m.CookieName)
|
||||
if err != nil || cookie.Value == "" {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
sid, _ := url.QueryUnescape(cookie.Value)
|
||||
sslog := slog.With(common.SessionIDAttr(sid))
|
||||
|
||||
ctx := r.Context()
|
||||
sslog.Log(ctx, common.LevelTrace, "Session cookie found in the request for start", "path", r.URL.Path, "method", r.Method)
|
||||
session, err := m.Store.Read(ctx, sid, false /*skip cache*/)
|
||||
if err != nil {
|
||||
level := slog.LevelWarn
|
||||
if err != ErrSessionMissing {
|
||||
level = slog.LevelError
|
||||
}
|
||||
sslog.Log(ctx, level, "Failed to read session from store", common.ErrAttr(err))
|
||||
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return session, true
|
||||
}
|
||||
|
||||
func (m *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session *Session, started bool) {
|
||||
cookie, err := r.Cookie(m.CookieName)
|
||||
ctx := r.Context()
|
||||
if err != nil || cookie.Value == "" {
|
||||
@@ -36,6 +61,7 @@ func (m *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session
|
||||
sid := m.sessionID()
|
||||
sslog := slog.With(common.SessionIDAttr(sid))
|
||||
session = NewSession(NewSessionData(sid), m.Store)
|
||||
started = true
|
||||
sslog.DebugContext(ctx, "Registering new session", "path", r.URL.Path, "method", r.Method)
|
||||
if err = m.Store.Init(ctx, session); err != nil {
|
||||
sslog.ErrorContext(ctx, "Failed to register session", common.SessionIDAttr(sid), common.ErrAttr(err))
|
||||
@@ -54,21 +80,28 @@ func (m *Manager) SessionStart(w http.ResponseWriter, r *http.Request) (session
|
||||
sid, _ := url.QueryUnescape(cookie.Value)
|
||||
sslog := slog.With(common.SessionIDAttr(sid))
|
||||
sslog.Log(ctx, common.LevelTrace, "Session cookie found in the request for start", "path", r.URL.Path, "method", r.Method)
|
||||
session, err = m.Store.Read(ctx, sid)
|
||||
if err == ErrSessionMissing {
|
||||
sslog.WarnContext(ctx, "Session from cookie is missing")
|
||||
session, err = m.Store.Read(ctx, sid, false /*skip cache*/)
|
||||
if err != nil {
|
||||
level := slog.LevelWarn
|
||||
if err != ErrSessionMissing {
|
||||
level = slog.LevelError
|
||||
}
|
||||
sslog.Log(ctx, level, "Failed to read session from store", common.ErrAttr(err))
|
||||
session = NewSession(NewSessionData(sid), m.Store)
|
||||
started = true
|
||||
sslog.DebugContext(ctx, "Registering new session", "path", r.URL.Path, "method", r.Method)
|
||||
if err = m.Store.Init(ctx, session); err != nil {
|
||||
sslog.ErrorContext(ctx, "Failed to register session with existing cookie", common.ErrAttr(err))
|
||||
}
|
||||
} else if err != nil {
|
||||
sslog.ErrorContext(ctx, "Failed to read session from store", common.ErrAttr(err))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (m *Manager) RetrieveSession(ctx context.Context, sid string) (*Session, error) {
|
||||
return m.Store.Read(ctx, sid, true /*skip cache*/)
|
||||
}
|
||||
|
||||
func (m *Manager) SessionDestroy(w http.ResponseWriter, r *http.Request) {
|
||||
cookie, err := r.Cookie(m.CookieName)
|
||||
if err != nil || cookie.Value == "" {
|
||||
|
||||
26
web/layouts/login/dashes.html
Normal file
26
web/layouts/login/dashes.html
Normal file
@@ -0,0 +1,26 @@
|
||||
<svg class="w-auto h-4 mx-auto mt-8 text-gray-300" viewBox="0 0 172 16" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 11 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 46 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 81 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 116 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 151 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 18 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 53 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 88 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 123 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 158 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 25 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 60 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 95 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 130 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 165 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 32 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 67 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 102 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 137 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 172 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 39 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 74 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 109 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 144 1)" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
10
web/layouts/login/login-contents.html
Normal file
10
web/layouts/login/login-contents.html
Normal file
@@ -0,0 +1,10 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="pc-form-caption">Sign in</h1>
|
||||
{{ if .Params.CanRegister }}<p class="pc-form-text">Don’t have an account? <a href="{{ relURL .Const.RegisterEndpoint }}" title="" class="pc-form-link">Join now</a></p>{{ end }}
|
||||
</div>
|
||||
|
||||
<form hx-post='{{ relURL .Const.LoginEndpoint }}' hx-indicator="#spinner" hx-disabled-elt="input, button" class="mt-12" hx-target="#login-container" hx-swap="innerHTML">
|
||||
{{template "login-form.html" .}}
|
||||
</form>
|
||||
|
||||
{{ template "dashes.html" . }}
|
||||
@@ -13,42 +13,12 @@
|
||||
<div class="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="relative max-w-md mx-auto lg:max-w-lg">
|
||||
<div class="relative overflow-hidden bg-white shadow-xl rounded-xl">
|
||||
<div class="px-4 py-6 sm:px-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="pc-form-caption">Sign in</h1>
|
||||
{{ if .Params.CanRegister }}<p class="pc-form-text">Don’t have an account? <a href="{{ relURL .Const.RegisterEndpoint }}" title="" class="pc-form-link">Join now</a></p>{{ end }}
|
||||
</div>
|
||||
|
||||
<form hx-post='{{ relURL .Const.LoginEndpoint }}' hx-indicator="#spinner" hx-disabled-elt="input, button" class="mt-12" hx-target="this" hx-swap="innerHTML" hx-on::after-swap="window.privateCaptcha.setup()">
|
||||
{{template "form.html" .}}
|
||||
</form>
|
||||
|
||||
<svg class="w-auto h-4 mx-auto mt-8 text-gray-300" viewBox="0 0 172 16" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 11 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 46 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 81 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 116 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 151 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 18 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 53 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 88 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 123 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 158 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 25 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 60 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 95 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 130 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 165 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 32 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 67 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 102 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 137 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 172 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 39 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 74 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 109 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 144 1)" />
|
||||
</svg>
|
||||
<div id="login-container" class="px-4 py-6 sm:px-8" hx-on::after-swap="window.privateCaptcha.setup()">
|
||||
{{ if .Params.IsRegister }}
|
||||
{{ template "register-contents.html" . }}
|
||||
{{ else }}
|
||||
{{ template "login-contents.html" . }}
|
||||
{{ end }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
11
web/layouts/login/register-contents.html
Normal file
11
web/layouts/login/register-contents.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="pc-form-caption">Sign up</h1>
|
||||
|
||||
<p class="pc-form-text">Already joined? <a href="{{ relURL .Const.LoginEndpoint }}" title="" class="pc-form-link">Login now</a></p>
|
||||
</div>
|
||||
|
||||
<form hx-post='{{ relURL .Const.RegisterEndpoint }}' hx-indicator="#spinner" hx-disabled-elt="input, button" hx-target="#login-container" hx-swap="innerHTML" class="mt-12">
|
||||
{{template "register-form.html" .}}
|
||||
</form>
|
||||
|
||||
{{ template "dashes.html" . }}
|
||||
@@ -52,7 +52,7 @@
|
||||
</div>
|
||||
<input type="hidden" name="{{ .Const.Token }}" value="{{ .Params.Token }}" />
|
||||
|
||||
<button id="registerSubmit" type="submit" class="pc-form-button" disabled >
|
||||
<button id="loginSubmit" type="submit" class="pc-form-button" disabled >
|
||||
<svg id="spinner" class="htmx-indicator animate-spin -ml-1 mr-3 h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
@@ -8,5 +8,21 @@
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function resendTimer() {
|
||||
return {
|
||||
countdown: 25,
|
||||
canResend: false,
|
||||
init() {
|
||||
const interval = setInterval(() => {
|
||||
this.countdown--;
|
||||
if (this.countdown <= 0) {
|
||||
clearInterval(interval);
|
||||
this.canResend = true;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
|
||||
11
web/layouts/login/twofactor-contents.html
Normal file
11
web/layouts/login/twofactor-contents.html
Normal file
@@ -0,0 +1,11 @@
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="pc-form-caption">Verify your account</h1>
|
||||
|
||||
<p class="pc-form-text"><a href='{{ relURL .Const.LoginEndpoint }}' title="" class="pc-form-link">Back to Login</a></p>
|
||||
</div>
|
||||
|
||||
<form hx-post='{{ relURL .Const.TwoFactorEndpoint }}' class="mt-12" hx-target="this" hx-swap="innerHTML" {{ if .Params.Token }}hx-headers='{"{{ .Const.HeaderCSRFToken }}": "{{ .Params.Token }}"}'{{ end }}>
|
||||
{{template "twofactor-form.html" .}}
|
||||
</form>
|
||||
|
||||
{{ template "dashes.html" . }}
|
||||
@@ -2,16 +2,16 @@
|
||||
<div>
|
||||
<label for="{{ .Const.VerificationCode }}" class="pc-form-label"> Enter the code sent to <span class="italic">{{ .Params.Email }}</span> </label>
|
||||
<div class="mt-2.5 relative">
|
||||
{{- if .Params.Error -}}
|
||||
{{- if .Params.CodeError -}}
|
||||
{{template "info-icon-red.html" .}}
|
||||
{{- end -}}
|
||||
<input type="text" name="{{ .Const.VerificationCode }}" placeholder="XXXXXX" class="pc-form-input-base {{ if .Params.Error }}pc-form-input-error{{ else }}pc-form-input-normal{{ end }}" pattern="[0-9]{6}" required />
|
||||
<input type="text" name="{{ .Const.VerificationCode }}" placeholder="XXXXXX" class="pc-form-input-base {{ if .Params.CodeError }}pc-form-input-error{{ else }}pc-form-input-normal{{ end }}" pattern="[0-9]{6}" required />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative flex items-center mt-4" x-data="resendTimer()">
|
||||
<div class="text-base" hx-target="this" hx-swap="innerHTML">
|
||||
<p class="pc-form-text">{{ if .Params.Error }}<span class="pc-form-text-error">{{.Params.Error}}</span>{{ else }}Did not receive the code?{{ end }} <a hx-post='{{ relURL .Const.ResendEndpoint }}' href="#" title="" class="pc-form-link" :class="{ 'opacity-50 pointer-events-none': !canResend }" x-text="canResend ? 'Resend' : `Resend (${countdown}s)`">Resend</a></p>
|
||||
<p class="pc-form-text">{{ if .Params.CodeError }}<span class="pc-form-text-error">{{.Params.CodeError}}</span>{{ else }}Did not receive the code?{{ end }} <a hx-post='{{ relURL .Const.ResendEndpoint }}' href="#" title="" class="pc-form-link" :class="{ 'opacity-50 pointer-events-none': !canResend }" x-text="canResend ? 'Resend' : `Resend (${countdown}s)`">Resend</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,59 +0,0 @@
|
||||
{{template "base.html" .}}
|
||||
|
||||
{{define "title"}}Sign up{{end}}
|
||||
|
||||
{{define "header"}}{{template "header-signed-out" .}}{{end}}
|
||||
{{define "footer"}}{{template "footer-signed-out" .}}{{end}}
|
||||
|
||||
{{define "body_class"}}pc-vertical-stretch{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
<div class="flex flex-1 items-center bg-pcpalegreen">
|
||||
<section class="-mt-20 flex-1">
|
||||
<div class="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="relative max-w-md mx-auto lg:max-w-lg">
|
||||
<div class="relative overflow-hidden bg-white shadow-xl rounded-xl">
|
||||
<div class="px-4 py-6 sm:px-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="pc-form-caption">Sign up</h1>
|
||||
|
||||
<p class="pc-form-text">Already joined? <a href="{{ relURL .Const.LoginEndpoint }}" title="" class="pc-form-link">Login now</a></p>
|
||||
</div>
|
||||
|
||||
<form hx-post='{{ relURL .Const.RegisterEndpoint }}' hx-indicator="#spinner" hx-disabled-elt="input, button" hx-target="this" hx-swap="innerHTML" class="mt-12" hx-on::after-swap="window.privateCaptcha.setup()">
|
||||
{{template "form.html" .}}
|
||||
</form>
|
||||
|
||||
<svg class="w-auto h-4 mx-auto mt-8 text-gray-300" viewBox="0 0 172 16" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 11 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 46 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 81 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 116 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 151 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 18 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 53 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 88 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 123 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 158 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 25 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 60 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 95 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 130 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 165 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 32 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 67 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 102 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 137 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 172 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 39 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 74 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 109 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 144 1)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{{end}}
|
||||
@@ -1,12 +0,0 @@
|
||||
{{define "scripts"}}
|
||||
{{template "default-scripts.html" .}}
|
||||
<script defer src="{{$.Ctx.CDN}}/widget/js/privatecaptcha.js" type="text/javascript" charset="utf-8"></script>
|
||||
<script>
|
||||
function onCaptchaSolved() {
|
||||
var submitButton = document.querySelector('#registerSubmit');
|
||||
if (submitButton) {
|
||||
submitButton.disabled = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -1,21 +0,0 @@
|
||||
{{define "scripts"}}
|
||||
{{template "default-scripts.html" .}}
|
||||
<script>
|
||||
function resendTimer() {
|
||||
return {
|
||||
countdown: 25,
|
||||
canResend: false,
|
||||
|
||||
init() {
|
||||
const interval = setInterval(() => {
|
||||
this.countdown--;
|
||||
if (this.countdown <= 0) {
|
||||
clearInterval(interval);
|
||||
this.canResend = true;
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{{end}}
|
||||
@@ -1,60 +0,0 @@
|
||||
{{template "base.html" .}}
|
||||
|
||||
{{define "title"}}Verify{{end}}
|
||||
|
||||
{{define "header"}}{{template "header-signed-out" .}}{{end}}
|
||||
{{define "footer"}}{{template "footer-signed-out" .}}{{end}}
|
||||
|
||||
{{define "body_class"}}pc-vertical-stretch{{end}}
|
||||
|
||||
{{define "main"}}
|
||||
<div class="flex flex-1 items-center bg-pcpalegreen">
|
||||
<section class="-mt-20 flex-1">
|
||||
<div class="px-4 mx-auto max-w-7xl sm:px-6 lg:px-8">
|
||||
<div class="relative max-w-md mx-auto lg:max-w-lg">
|
||||
<div class="relative overflow-hidden bg-white shadow-xl rounded-xl">
|
||||
<div class="px-4 py-6 sm:px-8">
|
||||
<div class="flex items-center justify-between">
|
||||
<h1 class="pc-form-caption">Verify your account</h1>
|
||||
|
||||
<p class="pc-form-text"><a href='{{ relURL .Const.LoginEndpoint }}' title="" class="pc-form-link">Back to Login</a></p>
|
||||
</div>
|
||||
|
||||
<form hx-post='{{ relURL .Const.TwoFactorEndpoint }}' class="mt-12" hx-target="this" hx-swap="innerHTML">
|
||||
{{template "form.html" .}}
|
||||
</form>
|
||||
|
||||
<svg class="w-auto h-4 mx-auto mt-8 text-gray-300" viewBox="0 0 172 16" fill="none" stroke="currentColor" xmlns="http://www.w3.org/2000/svg">
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 11 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 46 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 81 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 116 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 151 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 18 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 53 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 88 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 123 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 158 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 25 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 60 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 95 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 130 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 165 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 32 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 67 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 102 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 137 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 172 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 39 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 74 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 109 1)" />
|
||||
<line y1="-0.5" x2="18.0278" y2="-0.5" transform="matrix(-0.5547 0.83205 0.83205 0.5547 144 1)" />
|
||||
</svg>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
{{end}}
|
||||
Reference in New Issue
Block a user