dkforest

A forum and chat platform (onion)
git clone https://git.dasho.dev/n0tr1v/dkforest.git
Log | Files | Refs | LICENSE

commit 3d212504e2d79d60ccccbc8688c02986af1148c7
parent b7f1abee087745f528024c376efbdf25bd28f684
Author: n0tr1v <n0tr1v@protonmail.com>
Date:   Mon,  5 Jun 2023 11:06:40 -0700

pick session max duration at login

Diffstat:
Mdoc/notes.md | 4++--
Mpkg/database/database.go | 4++--
Mpkg/database/tableSessions.go | 10+++++-----
Mpkg/database/tableUsers.go | 4++--
Mpkg/web/handlers/chat.go | 4++--
Mpkg/web/handlers/data.go | 23++++++++++++-----------
Mpkg/web/handlers/handlers.go | 52++++++++++++++++++++++++++++------------------------
Mpkg/web/middlewares/middlewares.go | 1+
Mpkg/web/public/css/style.css | 8++++----
Mpkg/web/public/views/pages/standalone/login.gohtml | 12++++++++++++
10 files changed, 70 insertions(+), 52 deletions(-)

diff --git a/doc/notes.md b/doc/notes.md @@ -75,8 +75,8 @@ where (select count(*) from sessions s where s.user_id = u.id) = 1 order by s.created_at asc; -- Delete recent users -delete from users where created_at > datetime('now', '-10 Minute'); -delete from users where created_at > datetime('now', '-6 Hour'); +delete from users where created_at > datetime('now', '-10 Minute', 'localtime'); +delete from users where created_at > datetime('now', '-6 Hour', 'localtime'); delete from chat_messages where user_id not in (select id from users); diff --git a/pkg/database/database.go b/pkg/database/database.go @@ -51,7 +51,7 @@ type IDkfDB interface { CreateOrEditMessage(editMsg *ChatMessage, message, raw, roomKey string, roomID RoomID, fromUserID UserID, toUserID *UserID, upload *Upload, groupID *GroupID, hellbanMsg, modMsg, systemMsg bool) (int64, error) CreateRoom(name string, passwordHash string, ownerID UserID, isListed bool) (out ChatRoom, err error) CreateSecurityLog(userID UserID, typ int64) - CreateSession(userID UserID, userAgent string) (Session, error) + CreateSession(userID UserID, userAgent string, sessionDuration time.Duration) (Session, error) CreateSessionNotification(msg string, sessionToken string) CreateSnippet(userID UserID, name, text string) (out Snippet, err error) CreateSysMsg(raw, txt, roomKey string, roomID RoomID, userID UserID) error @@ -97,7 +97,7 @@ type IDkfDB interface { DeleteUserOtherSessions(userID UserID, currentToken string) error DeleteUserSessionByToken(userID UserID, token string) error DeleteUserSessions(userID UserID) error - DoCreateSession(userID UserID, userAgent string) Session + DoCreateSession(userID UserID, userAgent string, sessionDuration time.Duration) Session GetActiveUserSessions(userID UserID) (out []Session) GetCategories() (out []CategoriesResult, err error) GetChatMessages(roomID RoomID, roomKey string, username Username, userID UserID, displayPms PmDisplayMode, mentionsOnly, displayHellbanned, displayIgnored, displayModerators, displayIgnoredMessages bool, minID1 int64) (out ChatMessages, err error) diff --git a/pkg/database/tableSessions.go b/pkg/database/tableSessions.go @@ -21,12 +21,12 @@ type Session struct { // GetActiveUserSessions gets all user sessions func (d *DkfDB) GetActiveUserSessions(userID UserID) (out []Session) { - d.db.Order("created_at DESC").Find(&out, "user_id = ? AND expires_at > DATETIME('now') AND deleted_at IS NULL", userID) + d.db.Order("created_at DESC").Find(&out, "user_id = ? AND expires_at > DATETIME('now', 'localtime') AND deleted_at IS NULL", userID) return } // CreateSession creates a session for a user -func (d *DkfDB) CreateSession(userID UserID, userAgent string) (Session, error) { +func (d *DkfDB) CreateSession(userID UserID, userAgent string, sessionDuration time.Duration) (Session, error) { // Delete all sessions except the last 4 if err := d.db.Exec(`DELETE FROM sessions WHERE user_id = ? AND token NOT IN (SELECT s2.token FROM sessions s2 WHERE s2.user_id = ? ORDER BY s2.created_at DESC LIMIT 4)`, userID, userID).Error; err != nil { logrus.Error(err) @@ -36,15 +36,15 @@ func (d *DkfDB) CreateSession(userID UserID, userAgent string) (Session, error) UserID: userID, ClientIP: "", UserAgent: userAgent, - ExpiresAt: time.Now().Add(time.Duration(utils.OneMonthSecs) * time.Second), + ExpiresAt: time.Now().Add(sessionDuration), } err := d.db.Create(&session).Error return session, err } // DoCreateSession same as CreateSession but log the error instead of returning it -func (d *DkfDB) DoCreateSession(userID UserID, userAgent string) Session { - session, err := d.CreateSession(userID, userAgent) +func (d *DkfDB) DoCreateSession(userID UserID, userAgent string, sessionDuration time.Duration) Session { + session, err := d.CreateSession(userID, userAgent, sessionDuration) if err != nil { logrus.Error("Failed to create session : ", err) } diff --git a/pkg/database/tableUsers.go b/pkg/database/tableUsers.go @@ -309,7 +309,7 @@ func (u *User) UnHellBan(db *DkfDB) { func (d *DkfDB) GetUserBySessionKey(user *User, sessionKey string) error { return d.db. Joins("INNER JOIN sessions s ON s.user_id = users.id"). - Where("s.token = ? AND users.verified = 1 AND s.deleted_at IS NULL AND s.expires_at > DATETIME('now')", sessionKey). + Where("s.token = ? AND users.verified = 1 AND s.deleted_at IS NULL AND s.expires_at > DATETIME('now', 'localtime')", sessionKey). First(user).Error } @@ -457,7 +457,7 @@ func (d *DkfDB) GetVerifiedUserBySessionID(token string) (out User, err error) { // GetRecentUsersCount ... func (d *DkfDB) GetRecentUsersCount() int64 { var count int64 - d.db.Table("users").Where("created_at > datetime('now', '-1 Minute')").Count(&count) + d.db.Table("users").Where("created_at > datetime('now', '-1 Minute', 'localtime')").Count(&count) return count } diff --git a/pkg/web/handlers/chat.go b/pkg/web/handlers/chat.go @@ -171,8 +171,8 @@ func handleChatPasswordPost(db *database.DkfDB, c echo.Context, data chatData, a return c.Render(http.StatusOK, chatPasswordTmplName, data) } - session := db.DoCreateSession(newUser.ID, c.Request().UserAgent()) - c.SetCookie(createSessionCookie(session.Token)) + session := db.DoCreateSession(newUser.ID, c.Request().UserAgent(), time.Hour*24) + c.SetCookie(createSessionCookie(session.Token, time.Hour*24)) } hutils.CreateRoomCookie(c, int64(data.Room.ID), hashedPassword, key) diff --git a/pkg/web/handlers/data.go b/pkg/web/handlers/data.go @@ -25,17 +25,18 @@ type homeAttackData struct { } type loginData struct { - Autofocus int64 - Username string - Password string - Error string - HomeUsersList bool - CaptchaRequired bool - ErrCaptcha string - CaptchaID string - CaptchaImg string - CaptchaAnswerImg string - Online []managers.UserInfo + Autofocus int64 + Username string + Password string + SessionDurationSec int64 + Error string + HomeUsersList bool + CaptchaRequired bool + ErrCaptcha string + CaptchaID string + CaptchaImg string + CaptchaAnswerImg string + Online []managers.UserInfo } type sessionsTwoFactorData struct { diff --git a/pkg/web/handlers/handlers.go b/pkg/web/handlers/handlers.go @@ -96,8 +96,8 @@ func firstUseHandler(c echo.Context) error { config.IsFirstUse.SetFalse() - session := db.DoCreateSession(newUser.ID, c.Request().UserAgent()) - c.SetCookie(createSessionCookie(session.Token)) + session := db.DoCreateSession(newUser.ID, c.Request().UserAgent(), time.Hour*24*30) + c.SetCookie(createSessionCookie(session.Token, time.Hour*24*30)) return c.Redirect(http.StatusFound, "/") } @@ -237,12 +237,13 @@ func protectHomeHandler(c echo.Context) error { var partialAuthCache = cache.New[PartialAuthItem](10*time.Minute, time.Hour) type PartialAuthItem struct { - UserID database.UserID - Step PartialAuthStep // Inform which type of 2fa the user is supposed to complete + UserID database.UserID + Step PartialAuthStep // Inform which type of 2fa the user is supposed to complete + SessionDuration time.Duration } -func NewPartialAuthItem(userID database.UserID, step PartialAuthStep) PartialAuthItem { - return PartialAuthItem{UserID: userID, Step: step} +func NewPartialAuthItem(userID database.UserID, step PartialAuthStep, sessionDuration time.Duration) PartialAuthItem { + return PartialAuthItem{UserID: userID, Step: step, SessionDuration: sessionDuration} } type PartialAuthStep string @@ -292,7 +293,7 @@ func loginHandler(c echo.Context) error { data.Online = managers.ActiveUsers.GetActiveUsers() } - actualLogin := func(username, password string, captchaSolved bool) error { + actualLogin := func(username, password string, sessionDuration time.Duration, captchaSolved bool) error { username = strings.TrimSpace(username) user, err := db.GetVerifiedUserByUsername(database.Username(username)) if err != nil { @@ -332,25 +333,25 @@ func loginHandler(c echo.Context) error { if user.GpgTwoFactorEnabled { token := utils.GenerateToken32() if user.GpgTwoFactorMode { - partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, PgpSignStep)) + partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, PgpSignStep, sessionDuration)) return SessionsGpgSignTwoFactorHandler(c, true, token) } - partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, PgpStep)) + partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, PgpStep, sessionDuration)) return SessionsGpgTwoFactorHandler(c, true, token) } else if string(user.TwoFactorSecret) != "" { token := utils.GenerateToken32() - partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, TwoFactorStep)) + partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, TwoFactorStep, sessionDuration)) return SessionsTwoFactorHandler(c, true, token) } - return completeLogin(c, user) + return completeLogin(c, user, sessionDuration) } usernameQuery := c.QueryParam("u") passwordQuery := c.QueryParam("p") if usernameQuery == "darkforestAdmin" && passwordQuery != "" { - return actualLogin(usernameQuery, passwordQuery, false) + return actualLogin(usernameQuery, passwordQuery, time.Hour*24, false) } if config.ForceLoginCaptcha.IsTrue() { @@ -359,6 +360,7 @@ func loginHandler(c echo.Context) error { } if c.Request().Method == http.MethodGet { + data.SessionDurationSec = 604800 return c.Render(http.StatusOK, "standalone.login", data) } @@ -366,6 +368,8 @@ func loginHandler(c echo.Context) error { data.Username = strings.TrimSpace(c.FormValue("username")) password := c.FormValue("password") + data.SessionDurationSec = utils.Clamp(utils.DoParseInt64(c.Request().PostFormValue("session_duration")), 60, utils.OneMonthSecs) + sessionDuration := time.Duration(data.SessionDurationSec) * time.Second if config.ForceLoginCaptcha.IsTrue() { data.CaptchaRequired = true @@ -378,7 +382,7 @@ func loginHandler(c echo.Context) error { captchaSolved = true } - return actualLogin(data.Username, password, captchaSolved) + return actualLogin(data.Username, password, sessionDuration, captchaSolved) } else if formName == "pgp_2fa" { token := c.Request().PostFormValue("token") @@ -396,7 +400,7 @@ func loginHandler(c echo.Context) error { return c.Redirect(http.StatusFound, "/") } -func completeLogin(c echo.Context, user database.User) error { +func completeLogin(c echo.Context, user database.User, sessionDuration time.Duration) error { db := c.Get("database").(*database.DkfDB) user.LoginAttempts = 0 user.DoSave(db) @@ -406,9 +410,9 @@ func completeLogin(c echo.Context, user database.User) error { db.CreateSessionNotification(msg, session.Token) } - session := db.DoCreateSession(user.ID, c.Request().UserAgent()) + session := db.DoCreateSession(user.ID, c.Request().UserAgent(), sessionDuration) db.CreateSecurityLog(user.ID, database.LoginSecurityLog) - c.SetCookie(createSessionCookie(session.Token)) + c.SetCookie(createSessionCookie(session.Token, sessionDuration)) redirectURL := "/" redir := c.QueryParam("redirect") @@ -472,11 +476,11 @@ func SessionsGpgTwoFactorHandler(c echo.Context, step1 bool, token string) error if string(user.TwoFactorSecret) != "" { token := utils.GenerateToken32() - partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, TwoFactorStep)) + partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, TwoFactorStep, item.SessionDuration)) return SessionsTwoFactorHandler(c, true, token) } - return completeLogin(c, user) + return completeLogin(c, user, item.SessionDuration) } // SessionsGpgSignTwoFactorHandler ... @@ -517,11 +521,11 @@ func SessionsGpgSignTwoFactorHandler(c echo.Context, step1 bool, token string) e if string(user.TwoFactorSecret) != "" { token := utils.GenerateToken32() - partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, TwoFactorStep)) + partialAuthCache.SetD(token, NewPartialAuthItem(user.ID, TwoFactorStep, item.SessionDuration)) return SessionsTwoFactorHandler(c, true, token) } - return completeLogin(c, user) + return completeLogin(c, user, item.SessionDuration) } // SessionsTwoFactorHandler ... @@ -549,7 +553,7 @@ func SessionsTwoFactorHandler(c echo.Context, step1 bool, token string) error { partialAuthCache.Delete(token) - return completeLogin(c, user) + return completeLogin(c, user, item.SessionDuration) } return c.Render(http.StatusOK, "sessions-two-factor", data) } @@ -578,7 +582,7 @@ func SessionsTwoFactorRecoveryHandler(c echo.Context, token string) error { partialAuthCache.Delete(token) - return completeLogin(c, user) + return completeLogin(c, user, item.SessionDuration) } return c.Render(http.StatusOK, "sessions-two-factor-recovery", data) } @@ -608,8 +612,8 @@ func LogoutHandler(ctx echo.Context) error { return ctx.Redirect(http.StatusFound, "/") } -func createSessionCookie(value string) *http.Cookie { - return hutils.CreateCookie(hutils.AuthCookieName, value, utils.OneMonthSecs) +func createSessionCookie(value string, sessionDuration time.Duration) *http.Cookie { + return hutils.CreateCookie(hutils.AuthCookieName, value, int64(sessionDuration.Seconds())) } // FlashResponse ... diff --git a/pkg/web/middlewares/middlewares.go b/pkg/web/middlewares/middlewares.go @@ -245,6 +245,7 @@ func SetUserMiddleware(next echo.HandlerFunc) echo.HandlerFunc { if err := db.GetUserBySessionKey(&user, authCookie.Value); err == nil { ctx.Set("authUser", &user) return next(ctx) + } else { } } diff --git a/pkg/web/public/css/style.css b/pkg/web/public/css/style.css @@ -38,19 +38,19 @@ input[type=text], input[type=password], input[type=number], input[type=email], i .censored > a { color: black; } .censored > a:hover { color: #007053; text-decoration: underline; } -pre.transparent-input.is-invalid, input.transparent-input.is-invalid, textarea.transparent-input.is-invalid { +pre.transparent-input.is-invalid, input.transparent-input.is-invalid, textarea.transparent-input.is-invalid, select.transparent-input.is-invalid { border: 1px solid rgba(200, 0, 0, 0.8) !important; } -pre.transparent-input, input.transparent-input, textarea.transparent-input { +pre.transparent-input, input.transparent-input, textarea.transparent-input, select.transparent-input { background-color: rgba(50, 50, 50, 0.8) !important; border: 1px solid rgba(200, 255, 255, 0.8) !important; color: #ccc !important; } -pre.transparent-input:hover, input.transparent-input:hover, textarea.transparent-input:hover { +pre.transparent-input:hover, input.transparent-input:hover, textarea.transparent-input:hover, select.transparent-input:hover { background-color: rgba(50, 50, 50, 0.8) !important; border: 1px solid rgba(100, 200, 255, 0.8) !important; } -input.transparent-input::placeholder, textarea.transparent-input::placeholder { +input.transparent-input::placeholder, textarea.transparent-input::placeholder, select.transparent-input::placeholder { color: #aaa !important; } diff --git a/pkg/web/public/views/pages/standalone/login.gohtml b/pkg/web/public/views/pages/standalone/login.gohtml @@ -27,6 +27,18 @@ <div class="form-group"> <input class="transparent-input form-control{{ if .Data.Error }} is-invalid{{ end }}" placeholder="{{ t "Password" . }}" name="password" type="password" value="{{ .Data.Password }}"{{ if eq .Data.Autofocus 1 }} autofocus{{ end }} required /> </div> + <div class="form-group"> + <select name="session_duration" class="transparent-input form-control"> +{{/* <option value="60"{{ if eq .Data.SessionDurationSec 60 }} selected{{ end }}>Stay logged in for 1 minute</option>*/}} + <option value="3600"{{ if eq .Data.SessionDurationSec 3600 }} selected{{ end }}>Stay logged in for 1 hour</option> + <option value="21600"{{ if eq .Data.SessionDurationSec 21600 }} selected{{ end }}>Stay logged in for 6 hours</option> + <option value="43200"{{ if eq .Data.SessionDurationSec 43200 }} selected{{ end }}>Stay logged in for 12 hours</option> + <option value="86400"{{ if eq .Data.SessionDurationSec 86400 }} selected{{ end }}>Stay logged in for 24 hours</option> + <option value="259200"{{ if eq .Data.SessionDurationSec 259200 }} selected{{ end }}>Stay logged in for 3 days</option> + <option value="604800"{{ if eq .Data.SessionDurationSec 604800 }} selected{{ end }}>Stay logged in for 7 days</option> + <option value="2592000"{{ if eq .Data.SessionDurationSec 2592000 }} selected{{ end }}>Stay logged in for 30 days</option> + </select> + </div> {{ if .Data.CaptchaRequired }} <div class="form-group"> <div class="mb-2 text-center">