Style preference tab for user (#467)

This commit is contained in:
Thomas Miceli
2025-05-05 01:31:42 +02:00
committed by GitHub
parent 0e9b778b45
commit dc43fccc04
23 changed files with 664 additions and 361 deletions

View File

@ -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)
}

View File

@ -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

View File

@ -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 {

View File

@ -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")
}

View File

@ -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")
}

View File

@ -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)
}

View File

@ -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"))

View File

@ -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)

13
public/style.css vendored
View File

@ -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
}

View File

@ -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})`;
}

View File

@ -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,

View File

@ -50,6 +50,16 @@
{{ else }}
<title>{{ if $.c.CustomName }}{{ $.c.CustomName }}{{ else }}Opengist{{ end }}</title>
{{ end }}
{{ if .currentStyle }}
<style>
:root {
--red-diff: rgba({{ hexToRgb .currentStyle.RemovedLineColor }} 0.1);
--green-diff: rgba({{ hexToRgb .currentStyle.AddedLineColor }} 0.1);
--git-diff: rgba({{ hexToRgb .currentStyle.GitLineColor }} 0.38);
}
</style>
{{ end }}
</head>
<body class="h-full">
<div id="app" class="text-gray-700 dark:text-white min-h-full bg-white dark:bg-gray-900">

8
templates/base/settings_footer.html vendored Normal file
View File

@ -0,0 +1,8 @@
{{ if false }}{{/* prevent IDE errors */}}
<div><main>
{{ end }}
{{ define "settings_footer" }}
</main>
</div>
{{ end }}

28
templates/base/settings_header.html vendored Normal file
View File

@ -0,0 +1,28 @@
{{ define "settings_header" }}
<div class="py-10">
<header class="pb-4">
<div>
<h1 class="text-2xl font-bold leading-tight">{{ .locale.Tr "settings" }}</h1>
</div>
</header>
<main>
<div class="mb-4">
<div class="">
<nav class="flex space-x-4" aria-label="Tabs">
<a href="{{ $.c.ExternalUrl }}/settings" class="{{ if eq .settingsHeaderPage "account" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}">{{ .locale.Tr "settings.header.account" }} </a>
<a href="{{ $.c.ExternalUrl }}/settings/mfa" class="{{ if eq .settingsHeaderPage "mfa" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "settings.header.mfa" }}</a>
<a href="{{ $.c.ExternalUrl }}/settings/ssh" class="{{ if eq .settingsHeaderPage "ssh" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "settings.header.ssh" }}</a>
<a href="{{ $.c.ExternalUrl }}/settings/style" class="{{ if eq .settingsHeaderPage "style" }}bg-gray-100 dark:bg-gray-700 text-slate-700 dark:text-slate-300 px-3 py-2 font-medium text-sm rounded-md
{{ else }} text-gray-600 dark:text-gray-400 hover:text-gray-400 dark:hover:text-slate-300 px-3 py-2 font-medium text-sm rounded-md {{ end }}" aria-current="page">{{ .locale.Tr "settings.header.style" }}</a>
</nav>
</div>
</div>
{{ end }}
{{ if false }}
{{/* prevent IDE errors */}}
</main></div>
{{ end }}

View File

@ -74,11 +74,11 @@
<div class="code">
{{ $fileslug := slug $file.Filename }}
{{ if ne $file.Content "" }}
<table class="chroma table-code w-full whitespace-pre" data-filename-slug="{{ $fileslug }}" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
<table class="chroma table-code w-full {{ if $.currentStyle }}{{ if $.currentStyle.SoftWrap }}whitespace-pre-wrap{{ else }}whitespace-pre{{ end }}{{ else }}whitespace-pre{{ end }}" data-filename-slug="{{ $fileslug }}" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
<tbody>
{{ $ii := "1" }}
{{ $i := toInt $ii }}
{{ range $line := $file.Lines }}<tr><td id="file-{{ $fileslug }}-{{$i}}" class="select-none line-num px-4">{{$i}}</td><td class="line-code">{{ $line | safe }}</td></tr>{{ $i = inc $i }}{{ end }}
{{ range $line := $file.Lines }}<tr><td id="file-{{ $fileslug }}-{{$i}}" class="select-none line-num px-4">{{$i}}</td><td class="line-code break-all">{{ $line | safe }}</td></tr>{{ $i = inc $i }}{{ end }}
</tbody>
</table>
{{ end }}

View File

@ -54,7 +54,7 @@
{{ else if eq $file.Content "" }}
<p class="m-2 ml-4 text-sm">{{ $.locale.Tr "gist.revision.empty-file" }}</p>
{{ else }}
<table class="code chroma table-code w-full whitespace-pre" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0">
<table class="code chroma table-code w-full {{ if $.currentStyle }}{{ if $.currentStyle.SoftWrap }}whitespace-pre-wrap{{ else }}whitespace-pre{{ end }}{{ else }}whitespace-pre{{ end }}" data-filename="{{ $file.Filename }}" style="font-size: 0.8em; border-spacing: 0">
<tbody>
{{ $left := 0 }}
{{ $right := 0 }}
@ -83,7 +83,7 @@
{{ $right = inc $right }}
{{ end }}
{{ end }}
<td class="select-none" style="width: 2%;">{{ if ne (index $line 0) 64 }}{{ slice $line 0 1 }}{{ end }}</td>
<td class="select-none" style="width: 2%; vertical-align: top;">{{ if ne (index $line 0) 64 }}{{ slice $line 0 1 }}{{ end }}</td>
<td>{{ if ne (index $line 0) 64 }}{{ slice $line 1 }}{{ else }}{{ $line }}{{ end }}</td>
</tr>
{{end}}

View File

@ -1,316 +0,0 @@
{{ template "header" .}}
<div class="py-10">
<header class="pb-4">
<div>
<h1 class="text-2xl font-bold leading-tight">{{ .locale.Tr "settings" }}</h1>
</div>
</header>
<main>
<div class="relative mx-auto max-w-[40rem] space-y-8">
<div class="sm:grid {{ if not .disableForm }}grid-cols-2{{ else }}grid-cols-1{{ end }} gap-x-4 md:gap-x-8 space-y-8 md:space-y-0">
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10 h-full">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "settings.change-username" }}
</h2>
<form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/username" method="post">
<div>
<div class="mt-1">
<input id="username-change" name="username" type="text" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
</div>
</div>
<input type="hidden" name="_method" value="PUT">
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
{{ .locale.Tr "settings.change-username" }}
</button>
{{ .csrfHtml }}
</form>
</div>
</div>
{{ if not .disableForm }}
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{if .hasPassword}}
{{ .locale.Tr "settings.change-password" }}
{{else}}
{{ .locale.Tr "settings.create-password" }}
{{end}}
</h2>
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
{{if .hasPassword}}
{{ .locale.Tr "settings.change-password-help" }}
{{else}}
{{ .locale.Tr "settings.create-password-help" }}
{{end}}
</h3>
<form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/password" method="post">
<div>
<label for="password-change" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "settings.password-label-title" }} </label>
<div class="mt-1">
<input id="password-change" name="password" type="password" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
</div>
</div>
<input type="hidden" name="_method" value="PUT">
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
{{if .hasPassword}}
{{ .locale.Tr "settings.change-password" }}
{{else}}
{{ .locale.Tr "settings.create-password" }}
{{end}}
</button>
{{ .csrfHtml }}
</form>
</div>
</div>
{{ end }}
</div>
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "settings.email" }}
</h2>
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
{{ .locale.Tr "settings.email-help" }}
</h3>
<form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/email" method="post">
<div>
<div class="mt-1">
<input id="email" name="email" value="{{ .userLogged.Email }}" type="email" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
</div>
</div>
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "settings.email-set" }}</button>
{{ .csrfHtml }}
</form>
</div>
</div>
{{ if or .githubOauth .gitlabOauth .giteaOauth .oidcOauth }}
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300 mb-2">
{{ .locale.Tr "settings.link-accounts" }}
</h2>
<div class="gap-y-2">
{{ if .githubOauth }}
{{ if .userLogged.GithubID }}
<a href="{{ $.c.ExternalUrl }}/oauth/github/unlink" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your GitHub account? You may lose access to Opengist if it\'s your only way to log in.')">
{{ .locale.Tr "settings.unlink-github-account" }}
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/github" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "settings.link-github-account" }}
</a>
{{ end }}
{{ end }}
{{ if .gitlabOauth }}
{{ if .userLogged.GitlabID }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitlab/unlink" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your GitLab account? You may lose access to Opengist if it\'s your only way to log in.')">
{{ .locale.Tr "settings.unlink-gitlab-account" }}
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitlab" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "settings.link-gitlab-account" }}
</a>
{{ end }}
{{ end }}
{{ if .giteaOauth }}
{{ if .userLogged.GiteaID }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitea/unlink" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your Gitea account? You may lose access to Opengist if it\'s your only way to log in.')">
{{ .locale.Tr "settings.unlink-gitea-account" }}
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitea" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "settings.link-gitea-account" }}
</a>
{{ end }}
{{ end }}
{{ if .oidcOauth }}
{{ if .userLogged.OIDCID }}
<a href="{{ $.c.ExternalUrl }}/oauth/openid-connect/unlink" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your OpenID account? You may lose access to Opengist if it\'s your only way to log in.')">
Unlink OpenID account
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/openid-connect" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
Link OpenID account
</a>
{{ end }}
{{ end }}
</div>
</div>
</div>
{{ end }}
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "auth.totp" }}
</h2>
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
{{ .locale.Tr "auth.totp.help" }}
</h3>
{{ if .hasTotp }}
<div class="flex">
<form method="post" action="{{ $.c.ExternalUrl }}/settings/totp" onconfirm="" class="mr-2">
<input type="hidden" name="_method" value="DELETE" />
{{ .csrfHtml }}
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500">{{ .locale.Tr "auth.totp.disable" }}</button>
</form>
<form method="post" action="{{ $.c.ExternalUrl }}/settings/totp/regenerate" onconfirm="">
{{ .csrfHtml }}
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "auth.totp.regenerate-recovery-codes" }}</button>
</form>
</div>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/settings/totp/generate" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "auth.totp.use" }}</a>
{{ end }}
</div>
</div>
<div class="sm:grid grid-cols-2 gap-x-4 md:gap-x-8">
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "auth.mfa.passkeys" }}
</h2>
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
{{ .locale.Tr "auth.mfa.passkeys-help" }}
</h3>
<form class="space-y-6" id="webauthn">
<div>
<label for="passkeyname" class="block text-sm font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "auth.mfa.passkey-name" }}</label>
<div class="mt-1">
<input id="passkeyname" name="passkeyname" type="text" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm" />
</div>
</div>
{{ .csrfHtml }}
<button id="bind-passkey-button" type="button" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "auth.mfa.bind-passkey" }}</button>
</form>
<div class="flex items-center justify-center mt-4">
<p id="login-passkey-wait" class="hidden text-sm font-medium items-center text-slate-700 dark:text-slate-300">{{ .locale.Tr "auth.mfa.waiting-for-passkey-input" }}</p>
</div>
</div>
</div>
<div>
<div class="mt-6 flow-root">
<ul role="list" class="-my-5 divide-y divide-gray-300 dark:divide-gray-700 list-none">
{{ if .passkeys }}
{{ range $passkey := .passkeys }}
<li class="py-5">
<div class="inline-flex">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-12 h-12 mr-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
<div>
<h3 class="text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .Name }}</h3>
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "auth.mfa.passkey-added-at" }} <span class="moment-timestamp-date">{{ .CreatedAt }}</span></p>
{{ if eq .LastUsedAt 0 }}
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "auth.mfa.passkey-never-used" }}</p>
{{ else }}
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "auth.mfa.passkey-last-used" }} <span class="moment-timestamp">{{ .LastUsedAt }}</span></p>
{{ end }}
</div>
<form action="{{ $.c.ExternalUrl }}/settings/passkeys/{{.ID}}" method="post" class="inline-block">
<input type="hidden" name="_method" value="DELETE">
{{ $.csrfHtml }}
<button type="submit" onclick="return confirm('{{ $.locale.Tr "auth.mfa.delete-passkey-confirm" }}');" class="align-middle items-center leading-2 ml-2 px-3 py-1 border border-transparent border-gray-200 dark:border-gray-700 text-xs font-medium rounded-md shadow-sm text-white dark:text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500">{{ $.locale.Tr "auth.mfa.delete-passkey" }}</button>
</form>
</div>
</li>
{{ end }}
{{ end }}
</ul>
</div>
</div>
</div>
<div class="sm:grid grid-cols-2 gap-x-4 md:gap-x-8">
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "settings.add-ssh-key" }}
</h2>
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
{{ .locale.Tr "settings.add-ssh-key-help" }}
</h3>
<form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/ssh-keys" method="post">
<div>
<label for="title" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "settings.add-ssh-key-title" }} </label>
<div class="mt-1">
<input id="title" name="title" type="text" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
</div>
</div>
<div class="mt-8">
<label for="sshkey" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "settings.add-ssh-key-content" }} </label>
<div class="mt-1">
<textarea id="sshkey" required autocomplete="off" name="content" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"></textarea>
</div>
</div>
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "settings.add-ssh-key" }}</button>
{{ .csrfHtml }}
</form>
</div>
</div>
<div>
<div class="mt-6 flow-root">
<ul role="list" class="-my-5 divide-y divide-gray-300 dark:divide-gray-700 list-none">
{{ if .sshKeys }}
{{ range $key := .sshKeys }}
<li class="py-5">
<div class="inline-flex">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-12 h-12 mr-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
<div>
<h3 class="text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .Title }}</h3>
<p class="mt-1 text-xs text-slate-600 dark:text-slate-400 line-clamp-2 code" style="overflow-wrap: anywhere">SHA256:{{.SHA}}</p>
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.ssh-key-added-at" }} <span class="moment-timestamp-date">{{ .CreatedAt }}</span></p>
{{ if eq .LastUsedAt 0 }}
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.ssh-key-never-used" }}</p>
{{ else }}
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.ssh-key-last-used" }} <span class="moment-timestamp">{{ .LastUsedAt }}</span></p>
{{ end }}
</div>
<form action="{{ $.c.ExternalUrl }}/settings/ssh-keys/{{.ID}}" method="post" class="inline-block">
<input type="hidden" name="_method" value="DELETE">
{{ $.csrfHtml }}
<button type="submit" onclick="return confirm('{{ $.locale.Tr "settings.delete-ssh-key-confirm" }}')" class="align-middle items-center leading-2 ml-2 px-3 py-1 border border-transparent border-gray-200 dark:border-gray-700 text-xs font-medium rounded-md shadow-sm text-white dark:text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500">{{ $.locale.Tr "settings.delete-ssh-key" }}</button>
</form>
</div>
</li>
{{ end }}
{{ end }}
</ul>
</div>
</div>
</div>
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "settings.delete-account" }}
</h2>
<form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/account" method="post">
<input type="hidden" name="_method" value="DELETE">
<button type="submit" onclick="return confirm('{{ .locale.Tr "settings.delete-account-confirm" }}')" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 mt-2">{{ .locale.Tr "settings.delete-account" }}</button>
{{ .csrfHtml }}
</form>
</div>
</div>
</div>
</main>
</div>
<script type="module" src="{{ asset "webauthn.ts" }}"></script>
{{ template "footer" .}}

162
templates/pages/settings_account.html vendored Normal file
View File

@ -0,0 +1,162 @@
{{ template "header" .}}
{{ template "settings_header" .}}
<div class="relative mx-auto max-w-[40rem] space-y-8">
<div class="sm:grid {{ if not .disableForm }}grid-cols-2{{ else }}grid-cols-1{{ end }} gap-x-4 md:gap-x-8 space-y-8 md:space-y-0">
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10 h-full">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "settings.change-username" }}
</h2>
<form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/username" method="post">
<div>
<div class="mt-1">
<input id="username-change" name="username" type="text" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
</div>
</div>
<input type="hidden" name="_method" value="PUT">
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
{{ .locale.Tr "settings.change-username" }}
</button>
{{ .csrfHtml }}
</form>
</div>
</div>
{{ if not .disableForm }}
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{if .hasPassword}}
{{ .locale.Tr "settings.change-password" }}
{{else}}
{{ .locale.Tr "settings.create-password" }}
{{end}}
</h2>
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
{{if .hasPassword}}
{{ .locale.Tr "settings.change-password-help" }}
{{else}}
{{ .locale.Tr "settings.create-password-help" }}
{{end}}
</h3>
<form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/password" method="post">
<div>
<label for="password-change" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "settings.password-label-title" }} </label>
<div class="mt-1">
<input id="password-change" name="password" type="password" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
</div>
</div>
<input type="hidden" name="_method" value="PUT">
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">
{{if .hasPassword}}
{{ .locale.Tr "settings.change-password" }}
{{else}}
{{ .locale.Tr "settings.create-password" }}
{{end}}
</button>
{{ .csrfHtml }}
</form>
</div>
</div>
{{ end }}
</div>
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "settings.email" }}
</h2>
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
{{ .locale.Tr "settings.email-help" }}
</h3>
<form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/email" method="post">
<div>
<div class="mt-1">
<input id="email" name="email" value="{{ .userLogged.Email }}" type="email" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
</div>
</div>
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "settings.email-set" }}</button>
{{ .csrfHtml }}
</form>
</div>
</div>
{{ if or .githubOauth .gitlabOauth .giteaOauth .oidcOauth }}
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300 mb-2">
{{ .locale.Tr "settings.link-accounts" }}
</h2>
<div class="gap-y-2">
{{ if .githubOauth }}
{{ if .userLogged.GithubID }}
<a href="{{ $.c.ExternalUrl }}/oauth/github/unlink" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your GitHub account? You may lose access to Opengist if it\'s your only way to log in.')">
{{ .locale.Tr "settings.unlink-github-account" }}
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/github" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "settings.link-github-account" }}
</a>
{{ end }}
{{ end }}
{{ if .gitlabOauth }}
{{ if .userLogged.GitlabID }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitlab/unlink" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your GitLab account? You may lose access to Opengist if it\'s your only way to log in.')">
{{ .locale.Tr "settings.unlink-gitlab-account" }}
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitlab" class="block w-full mb-2 text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-100 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "settings.link-gitlab-account" }}
</a>
{{ end }}
{{ end }}
{{ if .giteaOauth }}
{{ if .userLogged.GiteaID }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitea/unlink" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your Gitea account? You may lose access to Opengist if it\'s your only way to log in.')">
{{ .locale.Tr "settings.unlink-gitea-account" }}
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/gitea" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
{{ .locale.Tr "settings.link-gitea-account" }}
</a>
{{ end }}
{{ end }}
{{ if .oidcOauth }}
{{ if .userLogged.OIDCID }}
<a href="{{ $.c.ExternalUrl }}/oauth/openid-connect/unlink" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3"
onclick="return confirm('Are you sure you want to unlink your OpenID account? You may lose access to Opengist if it\'s your only way to log in.')">
Unlink OpenID account
</a>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/oauth/openid-connect" class="block w-full text-center whitespace-nowrap text-slate-700 dark:text-slate-300{{ if .syncReposFromFS }} text-slate-500 cursor-not-allowed {{ end }}rounded border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 px-2.5 py-2 text-xs font-medium text-gray-700 dark:text-white shadow-sm hover:bg-gray-200 dark:hover:bg-gray-700 hover:border-gray-500 hover:text-slate-700 dark:hover:text-slate-300 focus:outline-none focus:ring-1 focus:border-primary-500 focus:ring-primary-500 leading-3">
Link OpenID account
</a>
{{ end }}
{{ end }}
</div>
</div>
</div>
{{ end }}
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "settings.delete-account" }}
</h2>
<form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/account" method="post">
<input type="hidden" name="_method" value="DELETE">
<button type="submit" onclick="return confirm('{{ .locale.Tr "settings.delete-account-confirm" }}')" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500 mt-2">{{ .locale.Tr "settings.delete-account" }}</button>
{{ .csrfHtml }}
</form>
</div>
</div>
</div>
{{ template "settings_footer" .}}
{{ template "footer" .}}

91
templates/pages/settings_mfa.html vendored Normal file
View File

@ -0,0 +1,91 @@
{{ template "header" .}}
{{ template "settings_header" .}}
<div class="relative mx-auto max-w-[40rem] space-y-8">
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "auth.totp" }}
</h2>
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
{{ .locale.Tr "auth.totp.help" }}
</h3>
{{ if .hasTotp }}
<div class="flex">
<form method="post" action="{{ $.c.ExternalUrl }}/settings/totp" onconfirm="" class="mr-2">
<input type="hidden" name="_method" value="DELETE" />
{{ .csrfHtml }}
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500">{{ .locale.Tr "auth.totp.disable" }}</button>
</form>
<form method="post" action="{{ $.c.ExternalUrl }}/settings/totp/regenerate" onconfirm="">
{{ .csrfHtml }}
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "auth.totp.regenerate-recovery-codes" }}</button>
</form>
</div>
{{ else }}
<a href="{{ $.c.ExternalUrl }}/settings/totp/generate" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "auth.totp.use" }}</a>
{{ end }}
</div>
</div>
<div class="sm:grid grid-cols-2 gap-x-4 md:gap-x-8">
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "auth.mfa.passkeys" }}
</h2>
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
{{ .locale.Tr "auth.mfa.passkeys-help" }}
</h3>
<form class="space-y-6" id="webauthn">
<div>
<label for="passkeyname" class="block text-sm font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "auth.mfa.passkey-name" }}</label>
<div class="mt-1">
<input id="passkeyname" name="passkeyname" type="text" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm" />
</div>
</div>
{{ .csrfHtml }}
<button id="bind-passkey-button" type="button" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "auth.mfa.bind-passkey" }}</button>
</form>
<div class="flex items-center justify-center mt-4">
<p id="login-passkey-wait" class="hidden text-sm font-medium items-center text-slate-700 dark:text-slate-300">{{ .locale.Tr "auth.mfa.waiting-for-passkey-input" }}</p>
</div>
</div>
</div>
<div>
<div class="mt-6 flow-root">
<ul role="list" class="-my-5 divide-y divide-gray-300 dark:divide-gray-700 list-none">
{{ if .passkeys }}
{{ range $passkey := .passkeys }}
<li class="py-5">
<div class="inline-flex">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-12 h-12 mr-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
<div>
<h3 class="text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .Name }}</h3>
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "auth.mfa.passkey-added-at" }} <span class="moment-timestamp-date">{{ .CreatedAt }}</span></p>
{{ if eq .LastUsedAt 0 }}
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "auth.mfa.passkey-never-used" }}</p>
{{ else }}
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "auth.mfa.passkey-last-used" }} <span class="moment-timestamp">{{ .LastUsedAt }}</span></p>
{{ end }}
</div>
<form action="{{ $.c.ExternalUrl }}/settings/passkeys/{{.ID}}" method="post" class="inline-block">
<input type="hidden" name="_method" value="DELETE">
{{ $.csrfHtml }}
<button type="submit" onclick="return confirm('{{ $.locale.Tr "auth.mfa.delete-passkey-confirm" }}');" class="align-middle items-center leading-2 ml-2 px-3 py-1 border border-transparent border-gray-200 dark:border-gray-700 text-xs font-medium rounded-md shadow-sm text-white dark:text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500">{{ $.locale.Tr "auth.mfa.delete-passkey" }}</button>
</form>
</div>
</li>
{{ end }}
{{ end }}
</ul>
</div>
</div>
</div>
<script type="module" src="{{ asset "webauthn.ts" }}"></script>
</div>
{{ template "settings_footer" .}}
{{ template "footer" .}}

69
templates/pages/settings_ssh.html vendored Normal file
View File

@ -0,0 +1,69 @@
{{ template "header" .}}
{{ template "settings_header" .}}
<div class="relative mx-auto max-w-[40rem] space-y-8">
<div class="sm:grid grid-cols-2 gap-x-4 md:gap-x-8">
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "settings.add-ssh-key" }}
</h2>
<h3 class="text-sm text-gray-600 dark:text-gray-400 italic mb-4">
{{ .locale.Tr "settings.add-ssh-key-help" }}
</h3>
<form class="space-y-6" action="{{ $.c.ExternalUrl }}/settings/ssh-keys" method="post">
<div>
<label for="title" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "settings.add-ssh-key-title" }} </label>
<div class="mt-1">
<input id="title" name="title" type="text" required autocomplete="off" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm">
</div>
</div>
<div class="mt-8">
<label for="sshkey" class="block text-sm font-medium text-slate-700 dark:text-slate-300"> {{ .locale.Tr "settings.add-ssh-key-content" }} </label>
<div class="mt-1">
<textarea id="sshkey" required autocomplete="off" name="content" class="dark:bg-gray-800 appearance-none block w-full px-3 py-2 border border-gray-200 dark:border-gray-700 rounded-md shadow-sm placeholder-gray-600 dark:placeholder-gray-400 focus:outline-none focus:ring-primary-500 focus:border-primary-500 sm:text-sm"></textarea>
</div>
</div>
<button type="submit" class="inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "settings.add-ssh-key" }}</button>
{{ .csrfHtml }}
</form>
</div>
</div>
<div>
<div class="mt-6 flow-root">
<ul role="list" class="-my-5 divide-y divide-gray-300 dark:divide-gray-700 list-none">
{{ if .sshKeys }}
{{ range $key := .sshKeys }}
<li class="py-5">
<div class="inline-flex">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-12 h-12 mr-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25a3 3 0 013 3m3 0a6 6 0 01-7.029 5.912c-.563-.097-1.159.026-1.563.43L10.5 17.25H8.25v2.25H6v2.25H2.25v-2.818c0-.597.237-1.17.659-1.591l6.499-6.499c.404-.404.527-1 .43-1.563A6 6 0 1121.75 8.25z" />
</svg>
<div>
<h3 class="text-sm font-semibold text-slate-700 dark:text-slate-300">{{ .Title }}</h3>
<p class="mt-1 text-xs text-slate-600 dark:text-slate-400 line-clamp-2 code" style="overflow-wrap: anywhere">SHA256:{{.SHA}}</p>
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.ssh-key-added-at" }} <span class="moment-timestamp-date">{{ .CreatedAt }}</span></p>
{{ if eq .LastUsedAt 0 }}
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.ssh-key-never-used" }}</p>
{{ else }}
<p class="text-xs text-gray-500 line-clamp-2">{{ $.locale.Tr "settings.ssh-key-last-used" }} <span class="moment-timestamp">{{ .LastUsedAt }}</span></p>
{{ end }}
</div>
<form action="{{ $.c.ExternalUrl }}/settings/ssh-keys/{{.ID}}" method="post" class="inline-block">
<input type="hidden" name="_method" value="DELETE">
{{ $.csrfHtml }}
<button type="submit" onclick="return confirm('{{ $.locale.Tr "settings.delete-ssh-key-confirm" }}')" class="align-middle items-center leading-2 ml-2 px-3 py-1 border border-transparent border-gray-200 dark:border-gray-700 text-xs font-medium rounded-md shadow-sm text-white dark:text-white bg-rose-600 hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-rose-500">{{ $.locale.Tr "settings.delete-ssh-key" }}</button>
</form>
</div>
</li>
{{ end }}
{{ end }}
</ul>
</div>
</div>
</div>
</div>
{{ template "settings_footer" .}}
{{ template "footer" .}}

110
templates/pages/settings_style.html vendored Normal file
View File

@ -0,0 +1,110 @@
{{ template "header" .}}
{{ template "settings_header" .}}
<div class="relative mx-auto max-w-[40rem] space-y-8">
<div class="w-full">
<div class="bg-white dark:bg-gray-900 rounded-md border border-1 border-gray-200 dark:border-gray-700 py-8 px-4 shadow sm:rounded-lg sm:px-10">
<h2 class="text-md font-bold text-slate-700 dark:text-slate-300">
{{ .locale.Tr "settings.style.gist-code" }}
</h2>
<div class="rounded-md border border-1 border-gray-200 dark:border-gray-700 overflow-auto mt-4">
<div class="border-b-1 border-gray-200 dark:border-gray-700 bg-gray-50 dark:bg-gray-800 my-auto block">
<div class="ml-4 py-1.5 flex">
<span class="flex-auto inline-flex items-center text-sm text-slate-700 dark:text-slate-300 filename">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 text-slate-700 dark:text-slate-300" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M10 20l4-16m4 4l4 4-4 4M6 16l-4-4 4-4" />
</svg>
<a href="#" class="hover:text-primary-600 ml-2 mr-1">file.txt</a>
<span class="hidden sm:block">
<span class="text-gray-400"> · 95 B · Text</span>
</span>
</span>
<span class="isolate inline-flex rounded-md shadow-sm mr-2">
<button class="relative inline-flex items-center rounded-l-md bg-white text-gray-500 dark:text-slate-300 float-right px-2.5 py-1 leading-4 text-xs font-medium dark:bg-gray-600 border border-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 hover:text-slate-700 dark:hover:text-slate-300 select-none">
{{ $.locale.Tr "gist.raw" }}
</button>
<button type="button" class="relative -ml-px inline-flex items-center bg-white text-gray-500 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10 px-1 py-1 dark:text-slate-300 dark:bg-gray-600 dark:hover:bg-gray-700 copy-gist-btn">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 17.25v3.375c0 .621-.504 1.125-1.125 1.125h-9.75a1.125 1.125 0 01-1.125-1.125V7.875c0-.621.504-1.125 1.125-1.125H6.75a9.06 9.06 0 011.5.124m7.5 10.376h3.375c.621 0 1.125-.504 1.125-1.125V11.25c0-4.46-3.243-8.161-7.5-8.876a9.06 9.06 0 00-1.5-.124H9.375c-.621 0-1.125.504-1.125 1.125v3.5m7.5 10.375H9.375a1.125 1.125 0 01-1.125-1.125v-9.25m12 6.625v-1.875a3.375 3.375 0 00-3.375-3.375h-1.5a1.125 1.125 0 01-1.125-1.125v-1.5a3.375 3.375 0 00-3.375-3.375H9.75" />
</svg>
</button>
<button class="relative -ml-px inline-flex items-center rounded-r-md bg-white text-gray-500 ring-1 ring-inset ring-gray-300 hover:bg-gray-50 focus:z-10 px-1 py-1 dark:text-slate-300 dark:bg-gray-600 dark:hover:bg-gray-700">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
</svg>
</button>
</span>
</div>
</div>
<div class="overflow-auto">
<div class="code">
<table class="chroma table-code w-full {{ if .currentStyle }}{{ if .currentStyle.SoftWrap }}whitespace-pre-wrap{{ else }}whitespace-pre{{ end }}{{ else }}whitespace-pre{{ end }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
<tbody>
<tr>
<td class="select-none line-num px-4">1</td><td class="line-code break-all">This is a string</td>
</tr>
<tr>
<td class="select-none line-num px-4">2</td><td class="line-code break-all">This is a really really really really really really really really long string</td>
</tr>
<tr>
<td class="select-none line-num px-4">3</td><td class="line-code break-all"></td>
</tr>
<tr class="red-diff">
<td class="select-none line-num px-4">4</td><td class="line-code break-all">- code removed</td>
</tr>
<tr class="red-diff">
<td class="select-none line-num px-4">5</td><td class="line-code break-all">- another pretty pretty pretty pretty pretty pretty long code removed</td>
</tr>
<tr class="green-diff">
<td class="select-none line-num px-4">6</td><td class="line-code break-all">+ code added</td>
</tr>
<tr class="green-diff">
<td class="select-none line-num px-4">7</td><td class="line-code break-all">+ added a line which help to demonstrate the difference between enabling and disabling soft wrap</td>
</tr>
<tr>
<td class="select-none line-num px-4">8</td><td class="line-code break-all"></td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
<div class="flex">
<form method="post" action="{{ $.c.ExternalUrl }}/settings/style" class="mr-2">
<div class="mt-6 space-y-6 sm:flex sm:items-center sm:space-x-10 sm:space-y-0">
<div class="flex items-center">
<input id="no-soft-wrap" value="false" name="softwrap" type="radio" {{ if .currentStyle }}{{ if not .currentStyle.SoftWrap }}checked{{ end }}{{ else }}checked{{ end }} class="relative size-4 appearance-none rounded-full border border-gray-300 bg-white before:absolute before:inset-1 before:rounded-full before:bg-white checked:border-indigo-600 checked:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:before:bg-gray-400 forced-colors:appearance-auto forced-colors:before:hidden [&:not(:checked)]:before:hidden">
<label for="no-soft-wrap" class="ml-3 block text-sm font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "settings.style.no-soft-wrap" }}</label>
</div>
<div class="flex items-center">
<input id="soft-wrap" value="true" name="softwrap" type="radio" {{ if .currentStyle }}{{ if .currentStyle.SoftWrap }}checked{{ end }}{{ end }} class="relative size-4 appearance-none rounded-full border border-gray-300 bg-white before:absolute before:inset-1 before:rounded-full before:bg-white checked:border-indigo-600 checked:bg-indigo-600 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600 disabled:border-gray-300 disabled:bg-gray-100 disabled:before:bg-gray-400 forced-colors:appearance-auto forced-colors:before:hidden [&:not(:checked)]:before:hidden">
<label for="soft-wrap" class="ml-3 block text-sm font-medium text-slate-700 dark:text-slate-300">{{ .locale.Tr "settings.style.soft-wrap" }}</label>
</div>
</div>
<div class="mt-6 space-y-6 sm:flex sm:items-center sm:space-x-10 sm:space-y-0">
<div class="flex-2">
<label for="removedlinecolor" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">{{ .locale.Tr "settings.style.removed-lines-color" }}</label>
<input type="color" value="{{ if .currentStyle }}{{ .currentStyle.RemovedLineColor }}{{ else }}#ff0000{{ end }}" id="removedlinecolor" name="removedlinecolor">
</div>
<div class="flex-2">
<label for="addedlinecolor" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">{{ .locale.Tr "settings.style.added-lines-color" }}</label>
<input type="color" value="{{ if .currentStyle }}{{ .currentStyle.AddedLineColor }}{{ else }}#00ff80{{ end }}" id="addedlinecolor" name="addedlinecolor">
</div>
<div class="flex-2">
<label for="gitlinecolor" class="block text-sm font-medium text-slate-700 dark:text-slate-300 mb-1">{{ .locale.Tr "settings.style.git-lines-color" }}</label>
<input type="color" value="{{ if .currentStyle }}{{ .currentStyle.GitLineColor }}{{ else }}#8f8f8f{{ end }}" id="gitlinecolor" name="gitlinecolor">
</div>
</div>
{{ .csrfHtml }}
<button type="submit" class="mt-4 inline-flex items-center px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "settings.style.save-style" }}</button>
</form>
</div>
</div>
</div>
</div>
<script type="module" src="{{ asset "style_preferences.ts" }}"></script>
{{ template "settings_footer" .}}
{{ template "footer" .}}

View File

@ -23,7 +23,7 @@
</div>
<div class="mt-8 sm:w-full sm:max-w-md mx-auto flex flex-col items-center">
<a href="{{ $.c.ExternalUrl }}/settings" class="px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "auth.totp.proceed" }}</a>
<a href="{{ $.c.ExternalUrl }}/settings/mfa" class="px-4 py-2 border border-transparent border-gray-200 dark:border-gray-700 text-sm font-medium rounded-md shadow-sm text-white dark:text-white bg-primary-500 hover:bg-primary-600 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500">{{ .locale.Tr "auth.totp.proceed" }}</a>
</div>
{{ else }}

View File

@ -63,7 +63,7 @@
{{ if isMarkdown .gist.PreviewFilename }}
<div class="chroma preview markdown markdown-body p-8">{{ .gist.HTML | safe }}</div>
{{ else }}
<table class="chroma table-code w-full whitespace-pre" data-filename="{{ .gist.PreviewFilename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
<table class="chroma table-code w-full {{ if .currentStyle }}{{ if .currentStyle.SoftWrap }}whitespace-pre-wrap{{ else }}whitespace-pre{{ end }}{{ else }}whitespace-pre{{ end }}" data-filename="{{ .gist.PreviewFilename }}" style="font-size: 0.8em; border-spacing: 0; border-collapse: collapse;">
<tbody>
{{ $ii := "1" }}
{{ $i := toInt $ii }}
@ -71,7 +71,7 @@
<tr>
<td class="select-none line-num px-4">{{$i}}</td>
<td class="line-code">{{ $line | safe }}</td>
<td class="line-code break-all">{{ $line | safe }}</td>
</tr>
{{ $i = inc $i }}
{{ end }}