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:
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">