Compare commits

...

12 Commits

Author SHA1 Message Date
Thomas Miceli
59440faedb Specify log path 2025-06-05 11:18:03 +02:00
Thomas Miceli
3c0115d829 Fix Markdown preview links (#475) 2025-05-15 15:16:40 +02:00
Thomas Miceli
d796895b75 Fix filename unescape (#474) 2025-05-14 11:51:42 +02:00
Andy Piper
5542497622 Add Proxmox VE Helper-Script (#473) 2025-05-14 10:49:27 +02:00
Thomas Miceli
546f1968e0 Fix helm ci 2025-05-09 20:16:57 +02:00
Thomas Miceli
75e71fd042 Use Helm deployment.env[] values (#471) 2025-05-09 20:08:25 +02:00
Thomas Miceli
897dc43790 Add LDAP authentication (#470)
* Introduce basic LDAP authentication.

* Reformat LDAP code; use ldap in Git HTTP

* lint

---------

Co-authored-by: Santhosh Raju <santhosh.raju@gmail.com>
2025-05-09 19:32:22 +02:00
Johannes Kirchner
72e02700ec fix: Correct German spelling, use consistent wording (#468) 2025-05-05 15:04:28 +02:00
Thomas Miceli
dc43fccc04 Style preference tab for user (#467) 2025-05-05 01:31:42 +02:00
Sergey Ryazanov
0e9b778b45 Fix Gitlab avatar (#461)
* Fix GitLab user avatar method

* Fix size of Gitlab avatar
2025-05-05 00:46:29 +02:00
Johannes Kirchner
3c940cd81f feat: read psql sslmode from db uri (#462) 2025-05-05 00:29:13 +02:00
Thomas Miceli
de144d09d3 Update README.md 2025-04-09 15:45:38 +02:00
45 changed files with 1029 additions and 410 deletions

View File

@@ -26,6 +26,7 @@ jobs:
helm package ./opengist
# First time, create the index
wget -q https://helm.opengist.io/index.yaml
if [ ! -f index.yaml ]; then
helm repo index --url https://helm.opengist.io .
else

4
.gitignore vendored
View File

@@ -1,6 +1,7 @@
node_modules/
gist.db
.idea/
.vscode/
.DS_Store
/**/.DS_Store
public/assets/*
@@ -10,4 +11,5 @@ opengist
build/
docs/.vitepress/dist/
docs/.vitepress/cache/
helm/opengist/charts/
helm/opengist/charts/
vendor/

View File

@@ -28,7 +28,7 @@ It is similar to [GitHub Gist](https://gist.github.com/), but open-source and co
* Download raw files or as a ZIP archive
* OAuth2 login with GitHub, GitLab, Gitea, and OpenID Connect
* Restrict or unrestrict snippets visibility to anonymous users
* Docker support
* Docker support / Helm Chart
* [More...](/docs/introduction.md#features)
## Quick start

View File

@@ -8,6 +8,9 @@ log-level: warn
# Set the log output to one or more of the following: `stdout`, `file`. Default: stdout,file
log-output: stdout,file
# Set the path to the log file.
log-path: /tmp/logaaa
# Public URL to access to Opengist
external-url:
@@ -111,6 +114,18 @@ oidc.group-claim-name:
# The name of the group that should receive admin rights
oidc.admin-group:
# LDAP authentication configuration
# URL of the LDAP instance e.g: ldap://ldap.example.com:389 ; if not set, LDAP authentication is disabled
ldap.url:
# Bind DN to authenticate against the LDAP e.g: cn=read-only-admin,dc=example,dc=com
ldap.bind-dn:
# The password for the Bind DN.
ldap.bind-credentials:
# The Base DN to start search from e.g: ou=People,dc=example,dc=com
ldap.search-base:
# The filter to search against (the format string %s will be replaced with the username) e.g: (uid=%s)
ldap.search-filter:
# Instance name
# Set your own custom name to be displayed instead of 'Opengist'
custom.name:

View File

@@ -36,10 +36,15 @@ aside: false
| gitea.secret | OG_GITEA_SECRET | none | The secret for the Gitea OAuth application. |
| gitea.url | OG_GITEA_URL | `https://gitea.com/` | The URL of the Gitea instance. |
| gitea.name | OG_GITEA_NAME | `Gitea` | The name of the Gitea instance. It is displayed in the OAuth login button. |
| oidc.provider-name | OG_OIDC_PROVIDER_NAME | none | The name of the OIDC provider |
| oidc.provider-name | OG_OIDC_PROVIDER_NAME | none | The name of the OIDC provider |
| oidc.client-key | OG_OIDC_CLIENT_KEY | none | The client key for the OpenID application. |
| oidc.secret | OG_OIDC_SECRET | none | The secret for the OpenID application. |
| oidc.discovery-url | OG_OIDC_DISCOVERY_URL | none | Discovery endpoint of the OpenID provider. |
| ldap.url | OG_LDAP_URL | none | URL of the LDAP instance; if not set, LDAP authentication is disabled |
| ldap.bind-dn | OG_LDAP_BIND_DN | none | Bind DN to authenticate against the LDAP. e.g: cn=read-only-admin,dc=example,dc=com |
| ldap.bind-credentials | OG_LDAP_BIND_CREDENTIALS | none | The password for the Bind DN. |
| ldap.search-base | OG_LDAP_SEARCH_BASE | none | The Base DN to start search from. e.g: ou=People,dc=example,dc=com |
| ldap.search-filter | OG_LDAP_SEARCH_FILTER | none | The filter to search against (the format string %s will be replaced with the username). e.g: (uid=%s) |
| custom.name | OG_CUSTOM_NAME | none | The name of your instance, to be displayed in the tab title |
| custom.logo | OG_CUSTOM_LOGO | none | Path to an image, relative to $opengist-home/custom. |
| custom.favicon | OG_CUSTOM_FAVICON | none | Path to an image, relative to $opengist-home/custom. |

View File

@@ -4,3 +4,4 @@ The following is a list of resources made by happy users of Opengist. Feel free
- [Aetherinox/opengist-debian](https://github.com/Aetherinox/opengist-debian) - A Debian package for Opengist
- [How to Install Opengist on Your Synology NAS](https://mariushosting.com/how-to-install-opengist-on-your-synology-nas/) - A guide to install Opengist on a Synology NAS
- [Proxmox VE Helper-Script](https://community-scripts.github.io/ProxmoxVE/scripts?id=opengist) - A script to install Opengist on Proxmox VE

3
go.mod
View File

@@ -8,6 +8,7 @@ require (
github.com/blevesearch/bleve/v2 v2.5.0
github.com/dustin/go-humanize v1.0.1
github.com/glebarez/sqlite v1.11.0
github.com/go-ldap/ldap/v3 v3.4.8
github.com/go-playground/validator/v10 v10.26.0
github.com/go-webauthn/webauthn v0.12.3
github.com/google/uuid v1.6.0
@@ -37,6 +38,7 @@ require (
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
github.com/RoaringBitmap/roaring/v2 v2.4.5 // indirect
github.com/andybalholm/brotli v1.1.1 // indirect
github.com/beorn7/perks v1.0.1 // indirect
@@ -66,6 +68,7 @@ require (
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/glebarez/go-sqlite v1.22.0 // indirect
github.com/go-asn1-ber/asn1-ber v1.5.5 // indirect
github.com/go-chi/chi/v5 v5.2.1 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect

73
go.sum
View File

@@ -1,5 +1,7 @@
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/Kunde21/markdownfmt/v3 v3.1.0 h1:KiZu9LKs+wFFBQKhrZJrFZwtLnCCWJahL+S+E/3VnM0=
github.com/Kunde21/markdownfmt/v3 v3.1.0/go.mod h1:tPXN1RTyOzJwhfHoon9wUr4HGYmWgVxSQN6VBJDkrVc=
github.com/RoaringBitmap/roaring/v2 v2.4.5 h1:uGrrMreGjvAtTBobc0g5IrW1D5ldxDQYe2JW2gggRdg=
@@ -12,6 +14,8 @@ github.com/alecthomas/chroma/v2 v2.16.0/go.mod h1:RVX6AvYm4VfYe/zsk7mjHueLDZor3a
github.com/alecthomas/repr v0.0.0-20220113201626-b1b626ac65ae/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc=
github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA=
github.com/andybalholm/brotli v1.1.1/go.mod h1:05ib4cKhjx3OQYUY22hTVd34Bc8upXjOLL2rKwwZBoA=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -87,8 +91,12 @@ github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec
github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
github.com/go-asn1-ber/asn1-ber v1.5.5 h1:MNHlNMBDgEKD4TcKr36vQN68BA00aDfjIt3/bD50WnA=
github.com/go-asn1-ber/asn1-ber v1.5.5/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-chi/chi/v5 v5.2.1 h1:KOIHODQj58PmL80G2Eak4WdvUzjSJSm0vG72crDCqb8=
github.com/go-chi/chi/v5 v5.2.1/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops=
github.com/go-ldap/ldap/v3 v3.4.8 h1:loKJyspcRezt2Q3ZRMq2p/0v8iOurlmeXDPw6fikSvQ=
github.com/go-ldap/ldap/v3 v3.4.8/go.mod h1:qS3Sjlu76eHfHGpUdWkAXQTw4beih+cHsco2jXlIXrk=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
@@ -137,10 +145,15 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/schema v1.4.1 h1:jUg5hUjCSDZpNGLuXQOgIWGdlgrIdYvgQ0wZtdK1M3E=
github.com/gorilla/schema v1.4.1/go.mod h1:Dg5SSm5PV60mhF2NFaTV1xuYYj8tV8NOPRo4FggUMnM=
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM=
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8=
github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=
github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
@@ -151,6 +164,18 @@ github.com/jackc/pgx/v5 v5.7.4 h1:9wKznZrhWa2QiHL+NjTSPP6yjl3451BX3imWDnokYlg=
github.com/jackc/pgx/v5 v5.7.4/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8=
github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs=
github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo=
github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM=
github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg=
github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo=
github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o=
github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg=
github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8=
github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs=
github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY=
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
@@ -232,9 +257,11 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
@@ -250,6 +277,7 @@ github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGC
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.4.15/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
@@ -262,37 +290,82 @@ go.abhg.dev/goldmark/mermaid v0.5.0 h1:mDkykpSPJ+5wCQ8bSXgzJ2KQskjXkI5Ndxz7JYDHW
go.abhg.dev/goldmark/mermaid v0.5.0/go.mod h1:OCyk2o85TX2drWHH+HRy6bih2yZlUwbbv/R1MMh1YLs=
go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk=
go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
golang.org/x/oauth2 v0.29.0 h1:WdYw2tdTK1S8olAzWHdgeqfy+Mtm9XNhv/xJsY65d98=
golang.org/x/oauth2 v0.29.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -2,7 +2,7 @@ apiVersion: v2
name: opengist
description: Opengist Helm chart for Kubernetes
type: application
version: 0.1.0
version: 0.2.0
appVersion: 1.10.0
home: https://opengist.io
icon: https://raw.githubusercontent.com/thomiceli/opengist/master/public/opengist.svg

View File

@@ -1,6 +1,6 @@
# Opengist Helm Chart
![Version: 0.1.0](https://img.shields.io/badge/Version-0.1.0-informational?style=flat-square) ![AppVersion: 1.10.0](https://img.shields.io/badge/AppVersion-1.10.0-informational?style=flat-square)
![Version: 0.2.0](https://img.shields.io/badge/Version-0.2.0-informational?style=flat-square) ![AppVersion: 1.10.0](https://img.shields.io/badge/AppVersion-1.10.0-informational?style=flat-square)
Opengist Helm chart for Kubernetes.

View File

@@ -32,6 +32,9 @@ spec:
{{- toYaml . | nindent 8 }}
{{- end }}
spec:
{{- if .Values.deployment.terminationGracePeriodSeconds }}
terminationGracePeriodSeconds: {{ .Values.deployment.terminationGracePeriodSeconds }}
{{- end }}
{{- with .Values.imagePullSecrets }}
imagePullSecrets:
{{- toYaml . | nindent 8 }}
@@ -49,6 +52,10 @@ spec:
mountPath: /init/config
- name: config-volume
mountPath: /config-volume
{{- if .Values.deployment.env }}
env:
{{- toYaml .Values.deployment.env | nindent 12 }}
{{- end }}
containers:
- name: {{ .Chart.Name }}
securityContext:

View File

@@ -0,0 +1,64 @@
package ldap
import (
"fmt"
"github.com/go-ldap/ldap/v3"
"github.com/thomiceli/opengist/internal/config"
)
func Enabled() bool {
return config.C.LDAPUrl != ""
}
// Authenticate attempts to authenticate a user against the configured LDAP instance.
func Authenticate(username, password string) (bool, error) {
l, err := ldap.DialURL(config.C.LDAPUrl)
if err != nil {
return false, fmt.Errorf("unable to connect to URI: %v", config.C.LDAPUrl)
}
defer func(l *ldap.Conn) {
_ = l.Close()
}(l)
// First bind with a read only user
err = l.Bind(config.C.LDAPBindDn, config.C.LDAPBindCredentials)
if err != nil {
return false, err
}
searchFilter := fmt.Sprintf(config.C.LDAPSearchFilter, username)
searchRequest := ldap.NewSearchRequest(
config.C.LDAPSearchBase,
ldap.ScopeWholeSubtree,
ldap.NeverDerefAliases,
0,
0,
false,
searchFilter,
[]string{"dn"},
nil,
)
sr, err := l.Search(searchRequest)
if err != nil {
return false, err
}
if len(sr.Entries) != 1 {
return false, nil
}
// Bind as the user to verify their password
err = l.Bind(sr.Entries[0].DN, password)
if err != nil {
return false, nil
}
// Rebind as the read only user for any further queries
err = l.Bind(config.C.LDAPBindDn, config.C.LDAPBindCredentials)
if err != nil {
return false, err
}
return true, nil
}

View File

@@ -2,13 +2,17 @@ package oauth
import (
gocontext "context"
gojson "encoding/json"
"io"
"net/http"
"github.com/markbates/goth"
"github.com/markbates/goth/gothic"
"github.com/markbates/goth/providers/gitlab"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/config"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/web/context"
"net/http"
)
type GitLabProvider struct {
@@ -77,7 +81,34 @@ func (p *GitLabCallbackProvider) GetProviderUserSSHKeys() ([]string, error) {
func (p *GitLabCallbackProvider) UpdateUserDB(user *db.User) {
user.GitlabID = p.User.UserID
user.AvatarURL = urlJoin(config.C.GitlabUrl, "/uploads/-/system/user/avatar/", p.User.UserID, "/avatar.png") + "?width=400"
resp, err := http.Get(urlJoin(config.C.GitlabUrl, "/api/v4/avatar?size=400&email=", p.User.Email))
if err != nil {
log.Error().Err(err).Msg("Cannot get user avatar from GitLab")
return
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msg("Cannot read Gitlab response body")
return
}
var result map[string]interface{}
err = gojson.Unmarshal(body, &result)
if err != nil {
log.Error().Err(err).Msg("Cannot unmarshal Gitlab response body")
return
}
field, ok := result["avatar_url"]
if !ok {
log.Error().Msg("Field 'avatar_url' not found in Gitlab JSON response")
return
}
user.AvatarURL = field.(string)
}
func NewGitLabCallbackProvider(user *goth.User) CallbackProvider {

View File

@@ -31,6 +31,7 @@ type config struct {
LogLevel string `yaml:"log-level" env:"OG_LOG_LEVEL"`
LogOutput string `yaml:"log-output" env:"OG_LOG_OUTPUT"`
LogPath string `yaml:"log-path" env:"OG_LOG_PATH"`
ExternalUrl string `yaml:"external-url" env:"OG_EXTERNAL_URL"`
OpengistHome string `yaml:"opengist-home" env:"OG_OPENGIST_HOME"`
@@ -79,6 +80,12 @@ type config struct {
MetricsEnabled bool `yaml:"metrics.enabled" env:"OG_METRICS_ENABLED"`
LDAPUrl string `yaml:"ldap.url" env:"OG_LDAP_URL"`
LDAPBindDn string `yaml:"ldap.bind-dn" env:"OG_LDAP_BIND_DN"`
LDAPBindCredentials string `yaml:"ldap.bind-credentials" env:"OG_LDAP_BIND_CREDENTIALS"`
LDAPSearchBase string `yaml:"ldap.search-base" env:"OG_LDAP_SEARCH_BASE"`
LDAPSearchFilter string `yaml:"ldap.search-filter" env:"OG_LDAP_SEARCH_FILTER"`
CustomName string `yaml:"custom.name" env:"OG_CUSTOM_NAME"`
CustomLogo string `yaml:"custom.logo" env:"OG_CUSTOM_LOGO"`
CustomFavicon string `yaml:"custom.favicon" env:"OG_CUSTOM_FAVICON"`
@@ -146,6 +153,10 @@ func InitConfig(configPath string, out io.Writer) error {
c.OpengistHome = filepath.Join(homeDir, ".opengist")
}
if c.LogPath == "" {
c.LogPath = filepath.Join(GetHomeDir(), "log")
}
if err = checks(c); err != nil {
return err
}
@@ -163,7 +174,7 @@ func InitConfig(configPath string, out io.Writer) error {
}
func InitLog() {
if err := os.MkdirAll(filepath.Join(GetHomeDir(), "log"), 0755); err != nil {
if err := os.MkdirAll(C.LogPath, 0755); err != nil {
panic(err)
}
@@ -204,7 +215,7 @@ func InitLog() {
logWriters = append(logWriters, consoleWriter)
defer func() { log.Debug().Msg("Logging to stdout") }()
case "file":
file, err := os.OpenFile(filepath.Join(GetHomeDir(), "log", "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
file, err := os.OpenFile(filepath.Join(C.LogPath, "opengist.log"), os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
panic(err)
}

View File

@@ -39,6 +39,7 @@ type databaseInfo struct {
User string
Password string
Database string
SSLMode string
}
var DatabaseInfo *databaseInfo
@@ -46,6 +47,8 @@ var DatabaseInfo *databaseInfo
func parseDBURI(uri string) (*databaseInfo, error) {
info := &databaseInfo{}
info.SSLMode = "disable"
if uri == ":memory:" {
info.Type = SQLite
info.Database = uri
@@ -85,6 +88,13 @@ func parseDBURI(uri string) (*databaseInfo, error) {
info.Password, _ = u.User.Password()
}
if u.RawQuery != "" {
q, _ := url.ParseQuery(u.RawQuery)
if sslmode := q.Get("sslmode"); sslmode != "" && info.Type == PostgreSQL {
info.SSLMode = sslmode
}
}
switch info.Type {
case PostgreSQL, MySQL:
info.Database = strings.TrimPrefix(u.Path, "/")
@@ -222,7 +232,7 @@ func setupSQLite(dbInfo databaseInfo) error {
func setupPostgres(dbInfo databaseInfo) error {
var err error
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=disable", dbInfo.Host, dbInfo.Port, dbInfo.User, dbInfo.Password, dbInfo.Database)
dsn := fmt.Sprintf("host=%s port=%s user=%s password=%s dbname=%s sslmode=%s", dbInfo.Host, dbInfo.Port, dbInfo.User, dbInfo.Password, dbInfo.Database, dbInfo.SSLMode)
db, err = gorm.Open(postgres.Open(dsn), &gorm.Config{
Logger: logger.Default.LogMode(logger.Silent),

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

@@ -672,7 +672,7 @@ func convertUTF8ToOctal(name string) string {
}
func convertURLToOctal(name string) string {
decoded, err := url.QueryUnescape(name)
decoded, err := url.PathUnescape(name)
if err != nil {
return name
}

View File

@@ -21,7 +21,7 @@ gist.header.embed: 'Einbetten'
gist.header.embed-help: 'Bette diese Gist in deine Webseite ein.'
gist.header.download-zip: 'ZIP Herunterladen'
gist.raw: 'Orginalformat'
gist.raw: 'Originalformat'
gist.file-truncated: 'Diese Datei wurde abgeschnitten.'
gist.watch-full-file: 'Die gesamte Datei anzeigen.'
gist.file-not-valid: 'Diese Datei ist keine korrekte CSV Datei.'
@@ -37,7 +37,7 @@ gist.new.indent-mode-space: 'Leerzeichen'
gist.new.indent-mode-tab: 'Tab'
gist.new.indent-size: 'Einrückungs Größe'
gist.new.wrap-mode: 'Textumbruch Modus'
gist.new.wrap-mode-no: 'kein Textumruch'
gist.new.wrap-mode-no: 'kein Textumbruch'
gist.new.wrap-mode-soft: 'weicher Zeilenumbruch'
gist.new.add-file: 'Datei hinzufügen'
gist.new.create-public-button: 'Öffentliche Gist erstellen'
@@ -53,7 +53,7 @@ gist.edit.delete: 'Löschen'
gist.edit.cancel: 'Abbrechen'
gist.edit.save: 'Speichern'
gist.list.joined: 'Gemeinsam'
gist.list.joined: 'Beigetreten'
gist.list.all: 'Alle Gists'
gist.list.search-results: 'Suchergebnisse'
gist.list.sort: 'Sortieren'
@@ -61,17 +61,17 @@ gist.list.sort-by-created: 'erstellt'
gist.list.sort-by-updated: 'bearbeitet'
gist.list.order-by-asc: 'Älteste'
gist.list.order-by-desc: 'Neueste'
gist.list.select-tab: 'Tab Auswählen'
gist.list.select-tab: 'Tab auswählen'
gist.list.liked: 'Favorisiert'
gist.list.likes: 'Favoriten'
gist.list.forked: 'Forked'
gist.list.forked-from: 'Forked von'
gist.list.forked: 'Geforkt'
gist.list.forked-from: 'Geforkt von'
gist.list.forks: 'Forks'
gist.list.files: 'Dateien'
gist.list.last-active: 'Zuletzt aktiv'
gist.list.no-gists: 'Keine Gists'
gist.list.all-liked-by: 'Alle Gists favorisiert von %s'
gist.list.all-forked-by: 'Alle Gists geforked von %s'
gist.list.all-forked-by: 'Alle Gists geforkt von %s'
gist.list.all-from: 'Alle Gists von %s'
gist.search.found: 'Gists gefunden'
@@ -89,7 +89,7 @@ gist.forks.for: 'Fork für %s'
gist.likes: 'Favoriten'
gist.likes.no: 'Keine Favorisierungen'
gist.likes.for: 'Favortitisiert für %s'
gist.likes.for: 'Favorisiert für %s'
gist.revisions: 'Revisionen'
gist.revision.revised: 'hat die Gist bearbeitet'
@@ -112,7 +112,7 @@ settings.link-accounts: 'Accounts verlinken'
settings.link-github-account: 'GitHub-Account verlinken'
settings.link-gitlab-account: 'GitLab-Account verlinken'
settings.link-gitea-account: 'Gitea-Account verlinken'
settings.unlink-github-account: 'Github-Account Verlinkung aufheben'
settings.unlink-github-account: 'GitHub-Account Verlinkung aufheben'
settings.unlink-gitlab-account: 'GitLab-Account Verlinkung aufheben'
settings.unlink-gitea-account: 'Gitea-Account Verlinkung aufheben'
settings.delete-account: 'Account löschen'

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

@@ -12,15 +12,18 @@ import (
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/util"
"go.abhg.dev/goldmark/mermaid"
"regexp"
)
func MarkdownGistPreview(gist *db.Gist) (RenderedGist, error) {
var buf bytes.Buffer
err := newMarkdown().Convert([]byte(gist.Preview), &buf)
// remove links in Markdown Preview, quick fix for now
re := regexp.MustCompile(`<a\b[^>]*>(.*?)</a>`)
return RenderedGist{
Gist: gist,
HTML: buf.String(),
HTML: re.ReplaceAllString(buf.String(), `$1`),
}, err
}

View File

@@ -3,6 +3,7 @@ package auth
import (
"errors"
"github.com/rs/zerolog/log"
"github.com/thomiceli/opengist/internal/auth/ldap"
passwordpkg "github.com/thomiceli/opengist/internal/auth/password"
"github.com/thomiceli/opengist/internal/db"
"github.com/thomiceli/opengist/internal/i18n"
@@ -114,6 +115,7 @@ func ProcessLogin(ctx *context.Context) error {
return ctx.ErrorRes(403, ctx.Tr("error.login-disabled-form"), nil)
}
var user *db.User
var err error
sess := ctx.GetSession()
@@ -121,26 +123,16 @@ func ProcessLogin(ctx *context.Context) error {
if err = ctx.Bind(dto); err != nil {
return ctx.ErrorRes(400, ctx.Tr("error.cannot-bind-data"), err)
}
password := dto.Password
var user *db.User
if user, err = db.GetUserByUsername(dto.Username); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return ctx.ErrorRes(500, "Cannot get user", err)
if ldap.Enabled() {
if user, err = tryLdapLogin(ctx, dto.Username, dto.Password); err != nil {
return err
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error")
return ctx.RedirectTo("/login")
}
if ok, err := passwordpkg.VerifyPassword(password, user.Password); !ok {
if err != nil {
return ctx.ErrorRes(500, "Cannot check for password", err)
if user == nil {
if user, err = tryDbLogin(ctx, dto.Username, dto.Password); user == nil {
return err
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error")
return ctx.RedirectTo("/login")
}
// handle MFA
@@ -168,3 +160,59 @@ func Logout(ctx *context.Context) error {
ctx.DeleteCsrfCookie()
return ctx.RedirectTo("/all")
}
func tryDbLogin(ctx *context.Context, username, password string) (user *db.User, err error) {
if user, err = db.GetUserByUsername(username); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ctx.ErrorRes(500, "Cannot get user", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error")
return nil, ctx.RedirectTo("/login")
}
if ok, err := passwordpkg.VerifyPassword(password, user.Password); !ok {
if err != nil {
return nil, ctx.ErrorRes(500, "Cannot check for password", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
ctx.AddFlash(ctx.Tr("flash.auth.invalid-credentials"), "error")
return nil, ctx.RedirectTo("/login")
}
return user, nil
}
func tryLdapLogin(ctx *context.Context, username, password string) (user *db.User, err error) {
ok, err := ldap.Authenticate(username, password)
if err != nil {
log.Info().Err(err).Msgf("LDAP authentication error")
return nil, ctx.ErrorRes(500, "Cannot get user", err)
}
if !ok {
log.Warn().Msg("Invalid LDAP authentication attempt from " + ctx.RealIP())
return nil, nil
}
if user, err = db.GetUserByUsername(username); err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, ctx.ErrorRes(500, "Cannot get user", err)
}
}
if errors.Is(err, gorm.ErrRecordNotFound) {
user = &db.User{
Username: username,
}
if err = user.Create(); err != nil {
log.Warn().Err(err).Msg("Cannot create user after LDAP authentication")
return nil, ctx.ErrorRes(500, "Cannot create user", err)
}
return user, nil
}
return user, nil
}

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

@@ -52,7 +52,7 @@ func ProcessCreate(ctx *context.Context) error {
name = "gistfile" + strconv.Itoa(fileCounter) + ".txt"
}
escapedValue, err := url.QueryUnescape(content)
escapedValue, err := url.PathUnescape(content)
if err != nil {
return ctx.ErrorRes(400, ctx.Tr("error.invalid-character-unescaped"), err)
}

View File

@@ -6,6 +6,7 @@ import (
"encoding/base64"
"errors"
"fmt"
"github.com/thomiceli/opengist/internal/auth/ldap"
"github.com/thomiceli/opengist/internal/auth/password"
"github.com/thomiceli/opengist/internal/web/context"
"github.com/thomiceli/opengist/internal/web/handlers"
@@ -112,12 +113,28 @@ func GitHttp(ctx *context.Context) error {
userToCheckPermissions = &gist.User
}
if ok, err := password.VerifyPassword(authPassword, userToCheckPermissions.Password); !ok {
if err != nil {
return ctx.ErrorRes(500, "Cannot verify password", err)
// ldap
ldapSuccess := false
if ldap.Enabled() {
if ok, err := ldap.Authenticate(userToCheckPermissions.Username, authPassword); !ok {
if err != nil {
log.Warn().Err(err).Msg("LDAP authentication error")
}
log.Warn().Msg("Invalid LDAP authentication attempt from " + ctx.RealIP())
} else {
ldapSuccess = true
}
}
// password
if !ldapSuccess {
if ok, err := password.VerifyPassword(authPassword, userToCheckPermissions.Password); !ok {
if err != nil {
return ctx.ErrorRes(500, "Cannot verify password", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
return ctx.PlainText(404, "Check your credentials or make sure you have access to the Gist")
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
return ctx.PlainText(404, "Check your credentials or make sure you have access to the Gist")
}
} else {
var user *db.User
@@ -128,13 +145,25 @@ func GitHttp(ctx *context.Context) error {
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
return ctx.ErrorRes(401, "Invalid credentials", nil)
}
if ok, err := password.VerifyPassword(authPassword, user.Password); !ok {
if err != nil {
return ctx.ErrorRes(500, "Cannot check for password", err)
ldapSuccess := false
if ldap.Enabled() {
if ok, err := ldap.Authenticate(user.Username, authPassword); !ok {
if err != nil {
log.Warn().Err(err).Msg("LDAP authentication error")
}
log.Warn().Msg("Invalid LDAP authentication attempt from " + ctx.RealIP())
} else {
ldapSuccess = true
}
}
if !ldapSuccess {
if ok, err := password.VerifyPassword(authPassword, user.Password); !ok {
if err != nil {
return ctx.ErrorRes(500, "Cannot check for password", err)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
return ctx.ErrorRes(401, "Invalid credentials", nil)
}
log.Warn().Msg("Invalid HTTP authentication attempt from " + ctx.RealIP())
return ctx.ErrorRes(401, "Invalid credentials", nil)
}
if isInit {

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

@@ -71,6 +71,19 @@
<dt>OIDC Discovery URL</dt><dd>{{ if .c.OIDCDiscoveryUrl }}&#60;defined&#62;{{ end }}</dd>
<dt>OIDC Group Claim Name</dt><dd>{{ .c.OIDCGroupClaimName }}</dd>
<dt>OIDC Admin Group</dt><dd>{{ .c.OIDCAdminGroup }}</dd>
<div class="relative col-span-3 mt-4">
<div class="absolute inset-0 flex items-center" aria-hidden="true">
<div class="w-full border-t border-gray-300"></div>
</div>
<div class="relative flex justify-center">
<span class="bg-gray-50 dark:bg-gray-800 px-2 text-sm text-slate-700 dark:text-slate-300 font-bold">LDAP</span>
</div>
</div>
<dt>LDAP URL</dt><dd>{{ .c.LDAPUrl }}</dd>
<dt>LDAP Bind DN</dt><dd>{{ .c.LDAPBindDn }}</dd>
<dt>LDAP Bind Credentials</dt><dd>{{ if .c.LDAPBindCredentials }}&#60;defined&#62;{{ end }}</dd>
<dt>LDAP Search Base</dt><dd>{{ .c.LDAPSearchBase }}</dd>
<dt>LDAP Search Filter</dt><dd>{{ .c.LDAPSearchFilter }}</dd>
</dl>
</div>
<div>

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