From dc43fccc04982b1a3c3508d44adf6b08bd1d8041 Mon Sep 17 00:00:00 2001
From: Thomas Miceli <27960254+thomiceli@users.noreply.github.com>
Date: Mon, 5 May 2025 01:31:42 +0200
Subject: [PATCH] Style preference tab for user (#467)
---
internal/db/user.go | 50 +++-
internal/i18n/locales/en-US.yml | 11 +
internal/web/handlers/auth/totp.go | 8 +-
internal/web/handlers/settings/settings.go | 62 +++-
internal/web/handlers/settings/sshkey.go | 14 +-
internal/web/server/middlewares.go | 1 +
internal/web/server/renderer.go | 4 +
internal/web/server/router.go | 6 +-
public/style.css | 13 +-
public/style_preferences.ts | 45 +++
public/vite.config.js | 3 +-
templates/base/base_header.html | 10 +
templates/base/settings_footer.html | 8 +
templates/base/settings_header.html | 28 ++
templates/pages/gist.html | 4 +-
templates/pages/revisions.html | 4 +-
templates/pages/settings.html | 316 ---------------------
templates/pages/settings_account.html | 162 +++++++++++
templates/pages/settings_mfa.html | 91 ++++++
templates/pages/settings_ssh.html | 69 +++++
templates/pages/settings_style.html | 110 +++++++
templates/pages/totp.html | 2 +-
templates/partials/_gist_preview.html | 4 +-
23 files changed, 664 insertions(+), 361 deletions(-)
create mode 100644 public/style_preferences.ts
create mode 100644 templates/base/settings_footer.html
create mode 100644 templates/base/settings_header.html
delete mode 100644 templates/pages/settings.html
create mode 100644 templates/pages/settings_account.html
create mode 100644 templates/pages/settings_mfa.html
create mode 100644 templates/pages/settings_ssh.html
create mode 100644 templates/pages/settings_style.html
diff --git a/internal/db/user.go b/internal/db/user.go
index fc4a0e6..52cf314 100644
--- a/internal/db/user.go
+++ b/internal/db/user.go
@@ -1,23 +1,25 @@
package db
import (
+ "encoding/json"
"github.com/thomiceli/opengist/internal/git"
"gorm.io/gorm"
)
type User struct {
- ID uint `gorm:"primaryKey"`
- Username string `gorm:"uniqueIndex,size:191"`
- Password string
- IsAdmin bool
- CreatedAt int64
- Email string
- MD5Hash string // for gravatar, if no Email is specified, the value is random
- AvatarURL string
- GithubID string
- GitlabID string
- GiteaID string
- OIDCID string `gorm:"column:oidc_id"`
+ ID uint `gorm:"primaryKey"`
+ Username string `gorm:"uniqueIndex,size:191"`
+ Password string
+ IsAdmin bool
+ CreatedAt int64
+ Email string
+ MD5Hash string // for gravatar, if no Email is specified, the value is random
+ AvatarURL string
+ GithubID string
+ GitlabID string
+ GiteaID string
+ OIDCID string `gorm:"column:oidc_id"`
+ StylePreferences string
Gists []Gist `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
SSHKeys []SSHKey `gorm:"constraint:OnUpdate:CASCADE,OnDelete:CASCADE;foreignKey:UserID"`
@@ -234,6 +236,15 @@ func (user *User) HasMFA() (bool, bool, error) {
return webauthn, totp, err
}
+func (user *User) GetStyle() *UserStyleDTO {
+ style := new(UserStyleDTO)
+ err := json.Unmarshal([]byte(user.StylePreferences), style)
+ if err != nil {
+ return nil
+ }
+ return style
+}
+
// -- DTO -- //
type UserDTO struct {
@@ -251,3 +262,18 @@ func (dto *UserDTO) ToUser() *User {
type UserUsernameDTO struct {
Username string `form:"username" validate:"required,max=24,alphanumdash,notreserved"`
}
+
+type UserStyleDTO struct {
+ SoftWrap bool `form:"softwrap" json:"soft_wrap"`
+ RemovedLineColor string `form:"removedlinecolor" json:"removed_line_color" validate:"min=0,max=7"`
+ AddedLineColor string `form:"addedlinecolor" json:"added_line_color" validate:"min=0,max=7"`
+ GitLineColor string `form:"gitlinecolor" json:"git_line_color" validate:"min=0,max=7"`
+}
+
+func (dto *UserStyleDTO) ToJson() string {
+ data, err := json.Marshal(dto)
+ if err != nil {
+ return "{}"
+ }
+ return string(data)
+}
diff --git a/internal/i18n/locales/en-US.yml b/internal/i18n/locales/en-US.yml
index cad30cf..30801fb 100644
--- a/internal/i18n/locales/en-US.yml
+++ b/internal/i18n/locales/en-US.yml
@@ -148,6 +148,17 @@ settings.create-password-help: Create your password to login to Opengist via HTT
settings.change-password: Change password
settings.change-password-help: Change your password to login to Opengist via HTTP
settings.password-label-title: Password
+settings.header.account: Account
+settings.header.mfa: MFA
+settings.header.ssh: SSH
+settings.header.style: Style
+settings.style.gist-code: Gist code
+settings.style.no-soft-wrap: No Soft Wrap
+settings.style.soft-wrap: Soft Wrap
+settings.style.removed-lines-color: Removed lines color
+settings.style.added-lines-color: Added lines color
+settings.style.git-lines-color: Git lines color
+settings.style.save-style: Save style
auth.signup-disabled: Administrator has disabled signing up
auth.login: Login
diff --git a/internal/web/handlers/auth/totp.go b/internal/web/handlers/auth/totp.go
index 8be704c..c594c15 100644
--- a/internal/web/handlers/auth/totp.go
+++ b/internal/web/handlers/auth/totp.go
@@ -14,7 +14,7 @@ func BeginTotp(ctx *context.Context) error {
return ctx.ErrorRes(500, "Cannot check for user MFA", err)
} else if hasTotp {
ctx.AddFlash(ctx.Tr("auth.totp.already-enabled"), "error")
- return ctx.RedirectTo("/settings")
+ return ctx.RedirectTo("/settings/mfa")
}
ogUrl, err := url.Parse(ctx.GetData("baseHttpUrl").(string))
@@ -47,7 +47,7 @@ func FinishTotp(ctx *context.Context) error {
return ctx.ErrorRes(500, "Cannot check for user MFA", err)
} else if hasTotp {
ctx.AddFlash(ctx.Tr("auth.totp.already-enabled"), "error")
- return ctx.RedirectTo("/settings")
+ return ctx.RedirectTo("/settings/mfa")
}
dto := &db.TOTPDTO{}
@@ -134,7 +134,7 @@ func AssertTotp(ctx *context.Context) error {
}
ctx.AddFlash(ctx.Tr("auth.totp.code-used", dto.Code), "warning")
- redirectUrl = "/settings"
+ redirectUrl = "/settings/mfa"
}
sess.Values["user"] = userId
@@ -157,7 +157,7 @@ func DisableTotp(ctx *context.Context) error {
}
ctx.AddFlash(ctx.Tr("auth.totp.disabled"), "success")
- return ctx.RedirectTo("/settings")
+ return ctx.RedirectTo("/settings/mfa")
}
func RegenerateTotpRecoveryCodes(ctx *context.Context) error {
diff --git a/internal/web/handlers/settings/settings.go b/internal/web/handlers/settings/settings.go
index 06b3d4c..918e481 100644
--- a/internal/web/handlers/settings/settings.go
+++ b/internal/web/handlers/settings/settings.go
@@ -5,13 +5,19 @@ import (
"github.com/thomiceli/opengist/internal/web/context"
)
-func UserSettings(ctx *context.Context) error {
+func UserAccount(ctx *context.Context) error {
user := ctx.User
- keys, err := db.GetSSHKeysByUserID(user.ID)
- if err != nil {
- return ctx.ErrorRes(500, "Cannot get SSH keys", err)
- }
+ ctx.SetData("email", user.Email)
+ ctx.SetData("hasPassword", user.Password != "")
+ ctx.SetData("disableForm", ctx.GetData("DisableLoginForm"))
+ ctx.SetData("settingsHeaderPage", "account")
+ ctx.SetData("htmlTitle", ctx.TrH("settings"))
+ return ctx.Html("settings_account.html")
+}
+
+func UserMFA(ctx *context.Context) error {
+ user := ctx.User
passkeys, err := db.GetAllCredentialsForUser(user.ID)
if err != nil {
@@ -23,12 +29,48 @@ func UserSettings(ctx *context.Context) error {
return ctx.ErrorRes(500, "Cannot get MFA status", err)
}
- ctx.SetData("email", user.Email)
- ctx.SetData("sshKeys", keys)
ctx.SetData("passkeys", passkeys)
ctx.SetData("hasTotp", hasTotp)
- ctx.SetData("hasPassword", user.Password != "")
- ctx.SetData("disableForm", ctx.GetData("DisableLoginForm"))
+ ctx.SetData("settingsHeaderPage", "mfa")
ctx.SetData("htmlTitle", ctx.TrH("settings"))
- return ctx.Html("settings.html")
+ return ctx.Html("settings_mfa.html")
+}
+
+func UserSSHKeys(ctx *context.Context) error {
+ user := ctx.User
+
+ keys, err := db.GetSSHKeysByUserID(user.ID)
+ if err != nil {
+ return ctx.ErrorRes(500, "Cannot get SSH keys", err)
+ }
+
+ ctx.SetData("sshKeys", keys)
+ ctx.SetData("settingsHeaderPage", "ssh")
+ ctx.SetData("htmlTitle", ctx.TrH("settings"))
+ return ctx.Html("settings_ssh.html")
+}
+
+func UserStyle(ctx *context.Context) error {
+ ctx.SetData("settingsHeaderPage", "style")
+ ctx.SetData("htmlTitle", ctx.TrH("settings"))
+ return ctx.Html("settings_style.html")
+}
+
+func ProcessUserStyle(ctx *context.Context) error {
+ styleDto := new(db.UserStyleDTO)
+ if err := ctx.Bind(styleDto); err != nil {
+ return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
+ }
+
+ if err := ctx.Validate(styleDto); err != nil {
+ return ctx.ErrorRes(400, "Invalid data", err)
+ }
+ user := ctx.User
+ user.StylePreferences = styleDto.ToJson()
+ if err := user.Update(); err != nil {
+ return ctx.ErrorRes(500, "Cannot update user styles", err)
+ }
+
+ ctx.AddFlash("Updated style", "success")
+ return ctx.RedirectTo("/settings/style")
}
diff --git a/internal/web/handlers/settings/sshkey.go b/internal/web/handlers/settings/sshkey.go
index db8bdc3..fd0eee7 100644
--- a/internal/web/handlers/settings/sshkey.go
+++ b/internal/web/handlers/settings/sshkey.go
@@ -20,7 +20,7 @@ func SshKeysProcess(ctx *context.Context) error {
if err := ctx.Validate(dto); err != nil {
ctx.AddFlash(validator.ValidationMessages(&err, ctx.GetData("locale").(*i18n.Locale)), "error")
- return ctx.RedirectTo("/settings")
+ return ctx.RedirectTo("/settings/ssh")
}
key := dto.ToSSHKey()
@@ -29,7 +29,7 @@ func SshKeysProcess(ctx *context.Context) error {
pubKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(key.Content))
if err != nil {
ctx.AddFlash(ctx.Tr("flash.user.invalid-ssh-key"), "error")
- return ctx.RedirectTo("/settings")
+ return ctx.RedirectTo("/settings/ssh")
}
key.Content = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(pubKey)))
@@ -38,7 +38,7 @@ func SshKeysProcess(ctx *context.Context) error {
return ctx.ErrorRes(500, "Cannot check if SSH key exists", err)
}
ctx.AddFlash(ctx.Tr("settings.ssh-key-exists"), "error")
- return ctx.RedirectTo("/settings")
+ return ctx.RedirectTo("/settings/ssh")
}
if err := key.Create(); err != nil {
@@ -46,20 +46,20 @@ func SshKeysProcess(ctx *context.Context) error {
}
ctx.AddFlash(ctx.Tr("flash.user.ssh-key-added"), "success")
- return ctx.RedirectTo("/settings")
+ return ctx.RedirectTo("/settings/ssh")
}
func SshKeysDelete(ctx *context.Context) error {
user := ctx.User
keyId, err := strconv.Atoi(ctx.Param("id"))
if err != nil {
- return ctx.RedirectTo("/settings")
+ return ctx.RedirectTo("/settings/ssh")
}
key, err := db.GetSSHKeyByID(uint(keyId))
if err != nil || key.UserID != user.ID {
- return ctx.RedirectTo("/settings")
+ return ctx.RedirectTo("/settings/ssh")
}
if err := key.Delete(); err != nil {
@@ -67,5 +67,5 @@ func SshKeysDelete(ctx *context.Context) error {
}
ctx.AddFlash(ctx.Tr("flash.user.ssh-key-deleted"), "success")
- return ctx.RedirectTo("/settings")
+ return ctx.RedirectTo("/settings/ssh")
}
diff --git a/internal/web/server/middlewares.go b/internal/web/server/middlewares.go
index e5b1cc9..54e7946 100644
--- a/internal/web/server/middlewares.go
+++ b/internal/web/server/middlewares.go
@@ -275,6 +275,7 @@ func sessionInit(next Handler) Handler {
if user != nil {
ctx.User = user
ctx.SetData("userLogged", user)
+ ctx.SetData("currentStyle", user.GetStyle())
}
return next(ctx)
}
diff --git a/internal/web/server/renderer.go b/internal/web/server/renderer.go
index 3c8e4de..daab68f 100644
--- a/internal/web/server/renderer.go
+++ b/internal/web/server/renderer.go
@@ -186,6 +186,10 @@ func (s *Server) setFuncMap() {
}
return str
},
+ "hexToRgb": func(hex string) string {
+ h, _ := strconv.ParseUint(strings.TrimPrefix(hex, "#"), 16, 32)
+ return fmt.Sprintf("%d, %d, %d,", (h>>16)&0xFF, (h>>8)&0xFF, h&0xFF)
+ },
}
t := template.Must(template.New("t").Funcs(fm).ParseFS(templates.Files, "*/*.html"))
diff --git a/internal/web/server/router.go b/internal/web/server/router.go
index 05267d2..9bd513f 100644
--- a/internal/web/server/router.go
+++ b/internal/web/server/router.go
@@ -56,7 +56,11 @@ func (s *Server) registerRoutes() {
sA := r.SubGroup("/settings")
{
sA.Use(logged)
- sA.GET("", settings.UserSettings)
+ sA.GET("", settings.UserAccount)
+ sA.GET("/mfa", settings.UserMFA)
+ sA.GET("/ssh", settings.UserSSHKeys)
+ sA.GET("/style", settings.UserStyle)
+ sA.POST("/style", settings.ProcessUserStyle)
sA.POST("/email", settings.EmailProcess)
sA.DELETE("/account", settings.AccountDeleteProcess)
sA.POST("/ssh-keys", settings.SshKeysProcess)
diff --git a/public/style.css b/public/style.css
index 4659e5c..37ef9b4 100644
--- a/public/style.css
+++ b/public/style.css
@@ -10,6 +10,12 @@
}
}
+:root {
+ --red-diff: rgba(255, 0, 0, .1);
+ --green-diff: rgba(0, 255, 128, .1);
+ --git-diff: rgba(143, 143, 143, 0.38);
+}
+
html {
@apply bg-gray-50 dark:bg-gray-800;
}
@@ -41,18 +47,19 @@ pre {
.code .line-num {
width: 4%;
text-align: right;
+ vertical-align: top;
}
.red-diff {
- background-color: rgba(255, 0, 0, .1);
+ background-color: var(--red-diff);
}
.green-diff {
- background-color: rgba(0, 255, 128, .1);
+ background-color: var(--green-diff);
}
.gray-diff {
- background-color: rgba(143, 143, 143, 0.38);
+ background-color: var(--git-diff);
@apply py-4 !important
}
diff --git a/public/style_preferences.ts b/public/style_preferences.ts
new file mode 100644
index 0000000..b99583a
--- /dev/null
+++ b/public/style_preferences.ts
@@ -0,0 +1,45 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const noSoftWrapRadio = document.getElementById('no-soft-wrap');
+ const softWrapRadio = document.getElementById('soft-wrap');
+
+ function updateRootClass() {
+ const table = document.querySelector("table");
+
+ if (softWrapRadio.checked) {
+ table.classList.remove('whitespace-pre');
+ table.classList.add('whitespace-pre-wrap');
+ } else {
+ table.classList.remove('whitespace-pre-wrap');
+ table.classList.add('whitespace-pre');
+ }
+ }
+
+ noSoftWrapRadio.addEventListener('change', updateRootClass);
+ softWrapRadio.addEventListener('change', updateRootClass);
+
+
+ document.getElementById('removedlinecolor').addEventListener('change', function(event) {
+ const color = hexToRgba(event.target.value, 0.1);
+ document.documentElement.style.setProperty('--red-diff', color);
+ });
+
+ document.getElementById('addedlinecolor').addEventListener('change', function(event) {
+ const color = hexToRgba(event.target.value, 0.1);
+ document.documentElement.style.setProperty('--green-diff', color);
+ });
+
+ document.getElementById('gitlinecolor').addEventListener('change', function(event) {
+ const color = hexToRgba(event.target.value, 0.38);
+ document.documentElement.style.setProperty('--git-diff', color);
+ });
+});
+
+function hexToRgba(hex, opacity) {
+ hex = hex.replace('#', '');
+
+ const r = parseInt(hex.substring(0, 2), 16);
+ const g = parseInt(hex.substring(2, 4), 16);
+ const b = parseInt(hex.substring(4, 6), 16);
+
+ return `rgba(${r}, ${g}, ${b}, ${opacity})`;
+}
\ No newline at end of file
diff --git a/public/vite.config.js b/public/vite.config.js
index 3fb9176..455c055 100644
--- a/public/vite.config.js
+++ b/public/vite.config.js
@@ -15,7 +15,8 @@ export default defineConfig({
'./public/admin.ts',
'./public/gist.ts',
'./public/embed.ts',
- './public/webauthn.ts'
+ './public/webauthn.ts',
+ './public/style_preferences.ts'
]
},
assetsInlineLimit: 0,
diff --git a/templates/base/base_header.html b/templates/base/base_header.html
index b36c325..695f22d 100644
--- a/templates/base/base_header.html
+++ b/templates/base/base_header.html
@@ -50,6 +50,16 @@
{{ else }}
{{ if $.c.CustomName }}{{ $.c.CustomName }}{{ else }}Opengist{{ end }}
{{ end }}
+
+ {{ if .currentStyle }}
+
+ {{ end }}
diff --git a/templates/base/settings_footer.html b/templates/base/settings_footer.html
new file mode 100644
index 0000000..6277c3f
--- /dev/null
+++ b/templates/base/settings_footer.html
@@ -0,0 +1,8 @@
+{{ if false }}{{/* prevent IDE errors */}}
+
+{{ end }}
+
+{{ define "settings_footer" }}
+
+
+{{ end }}
diff --git a/templates/base/settings_header.html b/templates/base/settings_header.html
new file mode 100644
index 0000000..e056bdb
--- /dev/null
+++ b/templates/base/settings_header.html
@@ -0,0 +1,28 @@
+{{ define "settings_header" }}
+
+
+
+
+{{ end }}
+
+{{ if false }}
+{{/* prevent IDE errors */}}
+
+{{ end }}
diff --git a/templates/pages/gist.html b/templates/pages/gist.html
index f5a6b3c..55a58c0 100644
--- a/templates/pages/gist.html
+++ b/templates/pages/gist.html
@@ -74,11 +74,11 @@
{{ $fileslug := slug $file.Filename }}
{{ if ne $file.Content "" }}
-
+
{{ $ii := "1" }}
{{ $i := toInt $ii }}
- {{ range $line := $file.Lines }}{{$i}} | {{ $line | safe }} |
{{ $i = inc $i }}{{ end }}
+ {{ range $line := $file.Lines }}{{$i}} | {{ $line | safe }} |
{{ $i = inc $i }}{{ end }}
{{ end }}
diff --git a/templates/pages/revisions.html b/templates/pages/revisions.html
index 9846c8f..28ee83d 100644
--- a/templates/pages/revisions.html
+++ b/templates/pages/revisions.html
@@ -54,7 +54,7 @@
{{ else if eq $file.Content "" }}
{{ $.locale.Tr "gist.revision.empty-file" }}
{{ else }}
-
+
{{ $left := 0 }}
{{ $right := 0 }}
@@ -83,7 +83,7 @@
{{ $right = inc $right }}
{{ end }}
{{ end }}
- {{ if ne (index $line 0) 64 }}{{ slice $line 0 1 }}{{ end }} |
+ {{ if ne (index $line 0) 64 }}{{ slice $line 0 1 }}{{ end }} |
{{ if ne (index $line 0) 64 }}{{ slice $line 1 }}{{ else }}{{ $line }}{{ end }} |
{{end}}
diff --git a/templates/pages/settings.html b/templates/pages/settings.html
deleted file mode 100644
index fc86543..0000000
--- a/templates/pages/settings.html
+++ /dev/null
@@ -1,316 +0,0 @@
-{{ template "header" .}}
-
-
-
-
-
-
-
-
- {{ .locale.Tr "settings.email" }}
-
-
- {{ .locale.Tr "settings.email-help" }}
-
-
-
-
- {{ if or .githubOauth .gitlabOauth .giteaOauth .oidcOauth }}
-
-
-
- {{ .locale.Tr "settings.link-accounts" }}
-
-
-
-
- {{ end }}
-
-
-
-
- {{ .locale.Tr "auth.totp" }}
-
-
- {{ .locale.Tr "auth.totp.help" }}
-
- {{ if .hasTotp }}
-
-
-
-
- {{ else }}
-
{{ .locale.Tr "auth.totp.use" }}
- {{ end }}
-
-
-
-
-
-
-
- {{ .locale.Tr "auth.mfa.passkeys" }}
-
-
- {{ .locale.Tr "auth.mfa.passkeys-help" }}
-
-
-
-
{{ .locale.Tr "auth.mfa.waiting-for-passkey-input" }}
-
-
-
-
-
-
- {{ if .passkeys }}
- {{ range $passkey := .passkeys }}
- -
-
-
-
-
{{ .Name }}
-
{{ $.locale.Tr "auth.mfa.passkey-added-at" }} {{ .CreatedAt }}
- {{ if eq .LastUsedAt 0 }}
-
{{ $.locale.Tr "auth.mfa.passkey-never-used" }}
- {{ else }}
-
{{ $.locale.Tr "auth.mfa.passkey-last-used" }} {{ .LastUsedAt }}
- {{ end }}
-
-
-
-
- {{ end }}
- {{ end }}
-
-
-
-
-
-
-
-
-
- {{ .locale.Tr "settings.add-ssh-key" }}
-
-
- {{ .locale.Tr "settings.add-ssh-key-help" }}
-
-
-
-
-
-
-
- {{ if .sshKeys }}
- {{ range $key := .sshKeys }}
- -
-
-
-
-
{{ .Title }}
-
SHA256:{{.SHA}}
-
{{ $.locale.Tr "settings.ssh-key-added-at" }} {{ .CreatedAt }}
- {{ if eq .LastUsedAt 0 }}
-
{{ $.locale.Tr "settings.ssh-key-never-used" }}
- {{ else }}
-
{{ $.locale.Tr "settings.ssh-key-last-used" }} {{ .LastUsedAt }}
- {{ end }}
-
-
-
-
- {{ end }}
- {{ end }}
-
-
-
-
-
-
-
- {{ .locale.Tr "settings.delete-account" }}
-
-
-
-
-
-
-
-
-
-
-
-{{ template "footer" .}}
diff --git a/templates/pages/settings_account.html b/templates/pages/settings_account.html
new file mode 100644
index 0000000..9ace800
--- /dev/null
+++ b/templates/pages/settings_account.html
@@ -0,0 +1,162 @@
+{{ template "header" .}}
+{{ template "settings_header" .}}
+
+
+
+
+
+ {{ .locale.Tr "settings.email" }}
+
+
+ {{ .locale.Tr "settings.email-help" }}
+
+
+
+
+ {{ if or .githubOauth .gitlabOauth .giteaOauth .oidcOauth }}
+
+
+
+ {{ .locale.Tr "settings.link-accounts" }}
+
+
+
+
+ {{ end }}
+
+
+
+
+
+ {{ .locale.Tr "settings.delete-account" }}
+
+
+
+
+
+
+{{ template "settings_footer" .}}
+{{ template "footer" .}}
diff --git a/templates/pages/settings_mfa.html b/templates/pages/settings_mfa.html
new file mode 100644
index 0000000..16ec2c1
--- /dev/null
+++ b/templates/pages/settings_mfa.html
@@ -0,0 +1,91 @@
+{{ template "header" .}}
+{{ template "settings_header" .}}
+
+
+
+
+ {{ .locale.Tr "auth.totp" }}
+
+
+ {{ .locale.Tr "auth.totp.help" }}
+
+ {{ if .hasTotp }}
+
+
+
+
+ {{ else }}
+
{{ .locale.Tr "auth.totp.use" }}
+ {{ end }}
+
+
+
+
+
+
+
+ {{ .locale.Tr "auth.mfa.passkeys" }}
+
+
+ {{ .locale.Tr "auth.mfa.passkeys-help" }}
+
+
+
+
{{ .locale.Tr "auth.mfa.waiting-for-passkey-input" }}
+
+
+
+
+
+
+ {{ if .passkeys }}
+ {{ range $passkey := .passkeys }}
+ -
+
+
+
+
{{ .Name }}
+
{{ $.locale.Tr "auth.mfa.passkey-added-at" }} {{ .CreatedAt }}
+ {{ if eq .LastUsedAt 0 }}
+
{{ $.locale.Tr "auth.mfa.passkey-never-used" }}
+ {{ else }}
+
{{ $.locale.Tr "auth.mfa.passkey-last-used" }} {{ .LastUsedAt }}
+ {{ end }}
+
+
+
+
+ {{ end }}
+ {{ end }}
+
+
+
+
+
+
+
+
+{{ template "settings_footer" .}}
+{{ template "footer" .}}
diff --git a/templates/pages/settings_ssh.html b/templates/pages/settings_ssh.html
new file mode 100644
index 0000000..18cf773
--- /dev/null
+++ b/templates/pages/settings_ssh.html
@@ -0,0 +1,69 @@
+{{ template "header" .}}
+{{ template "settings_header" .}}
+
+
+
+
+
+ {{ .locale.Tr "settings.add-ssh-key" }}
+
+
+ {{ .locale.Tr "settings.add-ssh-key-help" }}
+
+
+
+
+
+
+
+ {{ if .sshKeys }}
+ {{ range $key := .sshKeys }}
+ -
+
+
+
+
{{ .Title }}
+
SHA256:{{.SHA}}
+
{{ $.locale.Tr "settings.ssh-key-added-at" }} {{ .CreatedAt }}
+ {{ if eq .LastUsedAt 0 }}
+
{{ $.locale.Tr "settings.ssh-key-never-used" }}
+ {{ else }}
+
{{ $.locale.Tr "settings.ssh-key-last-used" }} {{ .LastUsedAt }}
+ {{ end }}
+
+
+
+
+ {{ end }}
+ {{ end }}
+
+
+
+
+
+
+{{ template "settings_footer" .}}
+{{ template "footer" .}}
diff --git a/templates/pages/settings_style.html b/templates/pages/settings_style.html
new file mode 100644
index 0000000..7a6b77d
--- /dev/null
+++ b/templates/pages/settings_style.html
@@ -0,0 +1,110 @@
+{{ template "header" .}}
+{{ template "settings_header" .}}
+
+
+
+
+ {{ .locale.Tr "settings.style.gist-code" }}
+
+
+
+
+
+
+
+ file.txt
+
+ · 95 B · Text
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1 | This is a string |
+
+
+ 2 | This is a really really really really really really really really long string |
+
+
+ 3 | |
+
+
+ 4 | - code removed |
+
+
+ 5 | - another pretty pretty pretty pretty pretty pretty long code removed |
+
+
+ 6 | + code added |
+
+
+ 7 | + added a line which help to demonstrate the difference between enabling and disabling soft wrap |
+
+
+ 8 | |
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+{{ template "settings_footer" .}}
+{{ template "footer" .}}
diff --git a/templates/pages/totp.html b/templates/pages/totp.html
index 738393f..a27b0aa 100644
--- a/templates/pages/totp.html
+++ b/templates/pages/totp.html
@@ -23,7 +23,7 @@
{{ else }}
diff --git a/templates/partials/_gist_preview.html b/templates/partials/_gist_preview.html
index dc57511..fe83eeb 100644
--- a/templates/partials/_gist_preview.html
+++ b/templates/partials/_gist_preview.html
@@ -63,7 +63,7 @@
{{ if isMarkdown .gist.PreviewFilename }}
{{ .gist.HTML | safe }}
{{ else }}
-
+
{{ $ii := "1" }}
{{ $i := toInt $ii }}
@@ -71,7 +71,7 @@
{{$i}} |
- {{ $line | safe }} |
+ {{ $line | safe }} |
{{ $i = inc $i }}
{{ end }}