Compare commits
33 Commits
0.0.1-rc.1
...
developmen
Author | SHA1 | Date | |
---|---|---|---|
surtur | 4d10510f5b | ||
surtur | 882b7dfd28 | ||
surtur | 67165c82cc | ||
surtur | b97e47ed1b | ||
surtur | 35435da9a6 | ||
surtur | de9c6d0196 | ||
surtur | fc3dc01229 | ||
surtur | 1b457aa8c0 | ||
surtur | 96c0b53493 | ||
surtur | ff87c35dd1 | ||
surtur | 1d159e4f64 | ||
surtur | 73915fcd98 | ||
surtur | 83f0ec7e15 | ||
surtur | 07d19e6b77 | ||
surtur | e10fdc5042 | ||
surtur | 1b2d860beb | ||
surtur | e8515d9a89 | ||
surtur | d0867f0686 | ||
surtur | fcea85e54b | ||
surtur | fa1253a675 | ||
surtur | 4e17a6c911 | ||
surtur | 0c8f867316 | ||
surtur | 5527caa3a8 | ||
surtur | 5d494fca8d | ||
surtur | 010e54168a | ||
surtur | 15994c9d8f | ||
surtur | 34babd8335 | ||
surtur | 0cb77e096f | ||
surtur | b1e2168023 | ||
surtur | c10b4326b8 | ||
surtur | fd2916e73e | ||
surtur | f4bd798821 | ||
surtur | 047471e6d4 |
|
@ -237,7 +237,7 @@ steps:
|
|||
|
||||
- name: kaniko publish
|
||||
pull: always
|
||||
image: docker.io/immawanderer/drone-kaniko:linux-amd64
|
||||
image: docker.io/plugins/kaniko:1.7.5-kaniko1.9.1
|
||||
settings:
|
||||
dockerfile: Containerfile
|
||||
context: .
|
||||
|
|
|
@ -9,10 +9,6 @@ archives:
|
|||
# - name_template: '{{ .ProjectName }}-v{{ .Version }}-{{ .Os }}-{{ .Arch }}'
|
||||
- name_template: '{{ .ProjectName }}_v{{ .Version }}'
|
||||
meta: true
|
||||
files:
|
||||
- README.md
|
||||
- LICENSE
|
||||
- exampleConfig.dhall
|
||||
before:
|
||||
hooks:
|
||||
- go generate .
|
||||
|
@ -25,10 +21,10 @@ builds:
|
|||
- -trimpath
|
||||
goarch:
|
||||
- amd64
|
||||
- arm64
|
||||
# - arm64
|
||||
goos:
|
||||
- linux
|
||||
- freebsd
|
||||
# - freebsd
|
||||
ldflags:
|
||||
- -s -w -X main.version={{ .Version }} -X main.commit={{ .Commit }}
|
||||
mod_timestamp: '{{ .CommitTimestamp }}'
|
||||
|
@ -38,9 +34,8 @@ release:
|
|||
prerelease: true
|
||||
skip_upload: false
|
||||
extra_files:
|
||||
- glob: ./README.md
|
||||
- glob: ./LICENSE
|
||||
- glob: ./exampleConfig.dhall
|
||||
- glob: ./dist/pcmt_linux_amd64_v2/pcmt
|
||||
mode: replace
|
||||
gitea:
|
||||
owner: mirre-mt
|
||||
|
@ -53,6 +48,9 @@ snapshot:
|
|||
checksum:
|
||||
name_template: "{{ .ProjectName }}_{{ .Version }}_SHA512SUMS.txt"
|
||||
algorithm: sha512
|
||||
extra_files:
|
||||
- glob: ./exampleConfig.dhall
|
||||
- glob: ./dist/pcmt_linux_amd64_v2/pcmt
|
||||
changelog:
|
||||
use: git
|
||||
sort: asc
|
||||
|
|
|
@ -75,7 +75,7 @@ func (a *App) SetupRoutes() error {
|
|||
user := e.Group("/user", handlers.MiddlewareSession, xsrf)
|
||||
|
||||
user.GET("/initial-password-change", handlers.InitialPasswordChange())
|
||||
user.POST("/initial-password-change", handlers.InitialPasswordChange())
|
||||
user.POST("/initial-password-change", handlers.InitialPasswordChangePost())
|
||||
user.GET("/hibp-search", handlers.GetSearchHIBP())
|
||||
user.POST("/hibp-search", handlers.SearchHIBP())
|
||||
user.GET("/hibp-breach-details/:name", handlers.ViewHIBP())
|
||||
|
@ -92,11 +92,8 @@ func (a *App) SetupRoutes() error {
|
|||
manage.POST("/users/:id/update", handlers.UpdateUser())
|
||||
manage.POST("/users/:id/delete", handlers.DeleteUser())
|
||||
|
||||
e.GET("/logout", handlers.Logout())
|
||||
e.GET("/logout", handlers.Logout(), xsrf)
|
||||
e.POST("/logout", handlers.Logout(), handlers.MiddlewareSession, xsrf)
|
||||
|
||||
// administrative endpoints.
|
||||
e.GET("/admin/*", handlers.Admin())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -6,11 +6,14 @@ package app
|
|||
import (
|
||||
"encoding/hex"
|
||||
"net/http"
|
||||
"regexp"
|
||||
|
||||
"git.dotya.ml/mirre-mt/pcmt/modules/validation"
|
||||
"github.com/gorilla/sessions"
|
||||
"github.com/labstack/echo-contrib/session"
|
||||
"github.com/labstack/echo/v4"
|
||||
"github.com/labstack/echo/v4/middleware"
|
||||
"golang.org/x/exp/slog"
|
||||
"golang.org/x/time/rate"
|
||||
)
|
||||
|
||||
|
@ -20,7 +23,22 @@ func (a *App) SetServerSettings() {
|
|||
|
||||
e.HideBanner = true
|
||||
|
||||
e.Use(middleware.Logger())
|
||||
if a.setting.DefaultLoggerSkipAssets() {
|
||||
re := regexp.MustCompile("^/(assets|static)(.*)|favicon.ico")
|
||||
lC := middleware.DefaultLoggerConfig
|
||||
|
||||
lC.Skipper = func(c echo.Context) bool {
|
||||
r := c.Request().URL.Path
|
||||
|
||||
slog.Debug("logger skipper", "path", r)
|
||||
|
||||
return re.MatchString(r)
|
||||
}
|
||||
e.Use(middleware.LoggerWithConfig(lC))
|
||||
} else {
|
||||
e.Use(middleware.Logger())
|
||||
}
|
||||
|
||||
// e.Use(middleware.LoggerWithConfig(
|
||||
// middleware.LoggerConfig{
|
||||
// Format: `{"time":"${time_rfc3339_nano}","id":"${id}","remote_ip":"${remote_ip}",` +
|
||||
|
@ -56,6 +74,8 @@ func (a *App) SetServerSettings() {
|
|||
))
|
||||
}
|
||||
|
||||
e.Validator = validation.New()
|
||||
|
||||
// TODO: add check for prometheus config setting.
|
||||
// if true {
|
||||
// // import "github.com/labstack/echo-contrib/prometheus"
|
||||
|
@ -112,7 +132,9 @@ func (a *App) SetServerSettings() {
|
|||
)
|
||||
}
|
||||
|
||||
store.Options.Domain = a.setting.HTTPDomain()
|
||||
store.Options.Path = "/"
|
||||
// let the domain be set automatically based on where the app is running.
|
||||
// store.Options.Domain = a.setting.HTTPDomain()
|
||||
store.Options.HttpOnly = true
|
||||
store.Options.SameSite = http.SameSiteStrictMode
|
||||
store.Options.Secure = a.setting.HTTPSecure()
|
||||
|
@ -120,7 +142,13 @@ func (a *App) SetServerSettings() {
|
|||
|
||||
e.Use(session.Middleware(store))
|
||||
|
||||
e.Use(middleware.Secure())
|
||||
e.Use(
|
||||
middleware.SecureWithConfig(
|
||||
middleware.SecureConfig{
|
||||
ContentSecurityPolicy: a.setting.HTTPCSP(),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
if a.setting.HTTPGzipEnabled() {
|
||||
e.Use(middleware.GzipWithConfig(middleware.GzipConfig{
|
||||
|
@ -148,6 +176,7 @@ func (a *App) csrfConfig() echo.MiddlewareFunc {
|
|||
CookieHTTPOnly: true,
|
||||
CookieSameSite: http.SameSiteStrictMode,
|
||||
CookieMaxAge: a.setting.SessionMaxAge(),
|
||||
CookiePath: "/",
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
// Copyright 2023 wanderer <a_mirre at utb dot cz>
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package settings
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
func (s *Settings) setAPIKeys() {
|
||||
if hibpK := os.Getenv("PCMT_HIBP_API_KEY"); hibpK != "" {
|
||||
log.Info("setting HIBP API key from env var")
|
||||
s.SetAPIKeyHIBP(hibpK)
|
||||
}
|
||||
|
||||
if dehashedK := os.Getenv("PCMT_DEHASHED_API_KEY"); dehashedK != "" {
|
||||
log.Info("setting dehashed.com API key from env var")
|
||||
s.SetAPIKeyDehashed(dehashedK)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
// Copyright 2023 wanderer <a_mirre at utb dot cz>
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package settings
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
defaultAppName = "pcmt"
|
||||
defaultPort = 3000
|
||||
defaultSessionMaxAge = 86400 // seconds.
|
||||
defaultHTTPDomain = "localhost"
|
||||
defaultCSP = "upgrade-insecure-requests; default-src 'self'; manifest-src 'self'; font-src 'self'; connect-src 'self'; script-src 'self'; style-src 'self'; object-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'"
|
||||
defaultCSPDevel = "default-src 'self'; manifest-src 'self'; font-src 'self'; connect-src 'self' ws://localhost:3002 http://localhost:3002; script-src 'self' http://localhost:3002; style-src 'self'; object-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'"
|
||||
defaultServerWriteTimeout = 30 * time.Second
|
||||
defaultServerReadHeaderTimeout = 30 * time.Second
|
||||
defaultLoggerSkipAssets = true
|
||||
)
|
|
@ -3,7 +3,11 @@
|
|||
|
||||
package settings
|
||||
|
||||
import "flag"
|
||||
import (
|
||||
"flag"
|
||||
|
||||
"git.dotya.ml/mirre-mt/pcmt/config"
|
||||
)
|
||||
|
||||
// as per https://stackoverflow.com/a/54747682.
|
||||
func isFlagPassed(name string) bool {
|
||||
|
@ -17,3 +21,38 @@ func isFlagPassed(name string) bool {
|
|||
|
||||
return found
|
||||
}
|
||||
|
||||
// sortOutFlags checks whether any flag overrides were passed and their validity.
|
||||
func (s *Settings) sortOutFlags(conf *config.Config, hostFlag *string, portFlag *int, develFlag *bool) {
|
||||
log.Debug("checking flag overrides")
|
||||
|
||||
overrideMsg := "overriding '%s' based on a flag: %+v"
|
||||
|
||||
if isFlagPassed("host") {
|
||||
if h := *hostFlag; h != "unset" && h != conf.Host {
|
||||
log.Debugf(overrideMsg, "host", h)
|
||||
s.SetHost(h)
|
||||
}
|
||||
}
|
||||
|
||||
if isFlagPassed("port") {
|
||||
if p := *portFlag; p > 0 && p < 65536 {
|
||||
if p != conf.Port {
|
||||
log.Debugf(overrideMsg, "port", p)
|
||||
s.SetPort(p)
|
||||
}
|
||||
} else {
|
||||
log.Warnf("flag-supplied port '%d' outside of bounds, ignoring", p)
|
||||
}
|
||||
}
|
||||
|
||||
if isFlagPassed("devel") {
|
||||
if d := *develFlag; d != conf.DevelMode {
|
||||
log.Debugf(overrideMsg, "develMode", d)
|
||||
s.SetIsDevel(d)
|
||||
|
||||
log.Debug("making sure that CSP is set appropriately for devel mode (flag override)")
|
||||
s.SetHTTPCSP(conf.HTTP.ContentSecurityPolicy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ type Settings struct {
|
|||
httpGzipLevel int
|
||||
httpRateLimitEnabled bool
|
||||
httpRateLimit int
|
||||
httpCSP string
|
||||
isLive bool
|
||||
isDevel bool
|
||||
initCreateAdmin bool
|
||||
|
@ -40,16 +41,11 @@ type Settings struct {
|
|||
dbType string
|
||||
dbIsSetUp bool
|
||||
RegistrationAllowed bool
|
||||
hibpAPIKey string
|
||||
dehashedAPIKey string
|
||||
}
|
||||
|
||||
const (
|
||||
appName = "pcmt"
|
||||
defaultPort = 3000
|
||||
defaultSessionMaxAge = 86400 // seconds.
|
||||
defaultHTTPDomain = "localhost"
|
||||
defaultServerWriteTimeout = 30 * time.Second
|
||||
defaultServerReadHeaderTimeout = 30 * time.Second
|
||||
)
|
||||
var log slogging.Slogger
|
||||
|
||||
// cleantgt is a list of ENV vars pertaining to pcmt.
|
||||
var cleantgt = []string{
|
||||
|
@ -61,6 +57,7 @@ var cleantgt = []string{
|
|||
"PCMT_SESSION_ENCR_SECRET",
|
||||
"PCMT_INIT_ADMIN_PASSWORD",
|
||||
"PCMT_HIBP_API_KEY",
|
||||
"PCMT_DEHASHED_API_KEY",
|
||||
}
|
||||
|
||||
// New returns a new instance of the settings struct.
|
||||
|
@ -78,13 +75,18 @@ func (s *Settings) DefaultServerReadHeaderTimeout() time.Duration {
|
|||
return defaultServerReadHeaderTimeout
|
||||
}
|
||||
|
||||
// DefaultLoggerSkipAssets returns whether the logger skips reporting asset visits.
|
||||
func (s *Settings) DefaultLoggerSkipAssets() bool {
|
||||
return defaultLoggerSkipAssets
|
||||
}
|
||||
|
||||
// Consolidate reconciles whatever values are set in config and via flags and
|
||||
// sets it to one place that should be regarded as a single source of truth -
|
||||
// the settings struct. Order of preference for values is (from higher to
|
||||
// lower) as follows: flag -> Env var -> configuration file.
|
||||
func (s *Settings) Consolidate(conf *config.Config, host *string, port *int, devel *bool, version string) {
|
||||
log := *slogging.Logger() // have a local copy.
|
||||
log.Logger = log.Logger.With(
|
||||
log = *slogging.Logger() // have a local copy.
|
||||
log.Logger = log.With(
|
||||
slog.Group("pcmt extra", slog.String("module", "app/settings")),
|
||||
)
|
||||
|
||||
|
@ -103,7 +105,17 @@ func (s *Settings) Consolidate(conf *config.Config, host *string, port *int, dev
|
|||
s.SetIsLive(conf.LiveMode)
|
||||
s.SetIsDevel(conf.DevelMode)
|
||||
s.SetLoggerIsJSON(conf.Logger.JSON)
|
||||
s.SetSessionCookieName(conf.Session.CookieName)
|
||||
|
||||
if conf.HTTP.Secure {
|
||||
// https://www.sjoerdlangkemper.nl/2017/02/09/cookie-prefixes/
|
||||
// https://scotthelme.co.uk/tough-cookies/
|
||||
// https://check-your-website.server-daten.de/prefix-cookies.html
|
||||
s.SetSessionCookieName("__Host-" + conf.Session.CookieName)
|
||||
} else {
|
||||
s.SetSessionCookieName(conf.Session.CookieName)
|
||||
}
|
||||
|
||||
s.SetSessionMaxAge(conf.Session.MaxAge)
|
||||
s.SetSessionCookieAuthSecret(conf.Session.CookieAuthSecret)
|
||||
s.SetSessionCookieEncrSecret(conf.Session.CookieEncrSecret)
|
||||
|
||||
|
@ -117,6 +129,15 @@ func (s *Settings) Consolidate(conf *config.Config, host *string, port *int, dev
|
|||
s.sessionEncrIsHex = true
|
||||
}
|
||||
|
||||
if conf.Init.CreateAdmin {
|
||||
s.SetInitCreateAdmin(true)
|
||||
s.SetInitAdminPassword(conf.Init.AdminPassword)
|
||||
}
|
||||
|
||||
if conf.Registration.Allowed {
|
||||
s.RegistrationAllowed = true
|
||||
}
|
||||
|
||||
if conf.HTTP.Gzip > 0 {
|
||||
s.SetHTTPGzipEnabled(true)
|
||||
s.SetHTTPGzipLevel(conf.HTTP.Gzip)
|
||||
|
@ -127,47 +148,11 @@ func (s *Settings) Consolidate(conf *config.Config, host *string, port *int, dev
|
|||
s.SetHTTPRateLimit(conf.HTTP.RateLimit)
|
||||
}
|
||||
|
||||
if conf.Init.CreateAdmin {
|
||||
s.SetInitCreateAdmin(true)
|
||||
s.SetInitAdminPassword(conf.Init.AdminPassword)
|
||||
}
|
||||
|
||||
s.SetHTTPCSP(conf.HTTP.ContentSecurityPolicy)
|
||||
s.SetHTTPDomain(conf.HTTP.Domain)
|
||||
s.SetHTTPSecure(conf.HTTP.Secure)
|
||||
|
||||
log.Debug("checking flag overrides")
|
||||
|
||||
overrideMsg := "overriding '%s' based on a flag: %+v"
|
||||
|
||||
if isFlagPassed("host") {
|
||||
if h := *host; h != "unset" && h != conf.Host {
|
||||
log.Debugf(overrideMsg, "host", h)
|
||||
s.SetHost(h)
|
||||
}
|
||||
}
|
||||
|
||||
if isFlagPassed("port") {
|
||||
if p := *port; p > 0 && p < 65536 {
|
||||
if p != conf.Port {
|
||||
log.Debugf(overrideMsg, "port", p)
|
||||
s.SetPort(p)
|
||||
}
|
||||
} else {
|
||||
log.Warnf("flag-supplied port '%d' outside of bounds, ignoring", p)
|
||||
}
|
||||
}
|
||||
|
||||
if isFlagPassed("devel") {
|
||||
if d := *devel; d != conf.DevelMode {
|
||||
log.Debugf(overrideMsg, "develMode", d)
|
||||
s.SetIsDevel(d)
|
||||
}
|
||||
}
|
||||
|
||||
if conf.Registration.Allowed {
|
||||
s.RegistrationAllowed = true
|
||||
}
|
||||
|
||||
s.setAPIKeys()
|
||||
s.sortOutFlags(conf, host, port, devel)
|
||||
s.SetVersion(version)
|
||||
}
|
||||
|
||||
|
@ -183,7 +168,7 @@ func (s *Settings) Port() int {
|
|||
|
||||
// AppName returns the appName.
|
||||
func (s *Settings) AppName() string {
|
||||
return appName
|
||||
return defaultAppName
|
||||
}
|
||||
|
||||
// AppPath returns the appPath.
|
||||
|
@ -276,6 +261,11 @@ func (s *Settings) HTTPRateLimit() int {
|
|||
return s.httpRateLimit
|
||||
}
|
||||
|
||||
// HTTPCSP returns the httpCSP.
|
||||
func (s *Settings) HTTPCSP() string {
|
||||
return s.httpCSP
|
||||
}
|
||||
|
||||
// AssetsPath returns the assetsPath.
|
||||
func (s *Settings) AssetsPath() string {
|
||||
return s.assetsPath
|
||||
|
@ -296,6 +286,16 @@ func (s *Settings) DbIsSetUp() bool {
|
|||
return s.dbIsSetUp
|
||||
}
|
||||
|
||||
// APIKeyHIBP returns the hibpAPIKey.
|
||||
func (s *Settings) APIKeyHIBP() string {
|
||||
return s.hibpAPIKey
|
||||
}
|
||||
|
||||
// APIKeyDehashed returns the dehashedAPIKey.
|
||||
func (s *Settings) APIKeyDehashed() string {
|
||||
return s.dehashedAPIKey
|
||||
}
|
||||
|
||||
// DbConnstring returns the dbConnString.
|
||||
func (s *Settings) DbConnstring() string {
|
||||
return s.dbConnstring
|
||||
|
@ -364,8 +364,12 @@ func (s *Settings) SetSessionCookieEncrSecret(sessionCookieEncrSecret string) {
|
|||
// SetSessionMaxAge sets sessionMaxAge.
|
||||
func (s *Settings) SetSessionMaxAge(sessionMaxAge int) {
|
||||
if sessionMaxAge < 1 {
|
||||
log.Debug("setting cookie max age to the default")
|
||||
|
||||
s.sessionMaxAge = defaultSessionMaxAge
|
||||
} else {
|
||||
log.Debug("setting cookie max age to a config-provided value", "maxAge", sessionMaxAge)
|
||||
|
||||
s.sessionMaxAge = sessionMaxAge
|
||||
}
|
||||
}
|
||||
|
@ -405,6 +409,20 @@ func (s *Settings) SetHTTPRateLimit(rateLimit int) {
|
|||
s.httpRateLimit = rateLimit
|
||||
}
|
||||
|
||||
// SetHTTPCSP sets the content security policy.
|
||||
func (s *Settings) SetHTTPCSP(csp string) {
|
||||
switch csp {
|
||||
case "":
|
||||
if s.isDevel {
|
||||
s.httpCSP = defaultCSPDevel
|
||||
} else {
|
||||
s.httpCSP = defaultCSP
|
||||
}
|
||||
default:
|
||||
s.httpCSP = csp
|
||||
}
|
||||
}
|
||||
|
||||
// SetAssetsPath sets the assetsPath.
|
||||
func (s *Settings) SetAssetsPath(assetsPath string) {
|
||||
s.assetsPath = assetsPath
|
||||
|
@ -435,6 +453,16 @@ func (s *Settings) SetDbIsSetUp(is bool) {
|
|||
s.dbIsSetUp = is
|
||||
}
|
||||
|
||||
// SetAPIKeyHIBP sets the hibpAPIKey.
|
||||
func (s *Settings) SetAPIKeyHIBP(k string) {
|
||||
s.hibpAPIKey = k
|
||||
}
|
||||
|
||||
// SetAPIKeyDehashed sets the dehashedAPIKey.
|
||||
func (s *Settings) SetAPIKeyDehashed(k string) {
|
||||
s.dehashedAPIKey = k
|
||||
}
|
||||
|
||||
// EraseENVs attempts to clear environment vars pertaining to pcmt.
|
||||
func (s *Settings) EraseENVs() error {
|
||||
for _, v := range cleantgt {
|
||||
|
|
5
go.mod
5
go.mod
|
@ -5,6 +5,7 @@ go 1.20
|
|||
require (
|
||||
entgo.io/ent v0.12.3
|
||||
github.com/CAFxX/httpcompression v0.0.8
|
||||
github.com/go-playground/validator/v10 v10.15.3
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/gorilla/sessions v1.2.1
|
||||
github.com/labstack/echo-contrib v0.15.0
|
||||
|
@ -29,7 +30,10 @@ require (
|
|||
github.com/aymerick/douceur v0.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 // indirect
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/gorilla/context v1.1.1 // indirect
|
||||
|
@ -38,6 +42,7 @@ require (
|
|||
github.com/hashicorp/hcl/v2 v2.17.0 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/klauspost/compress v1.16.7 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
|
|
16
go.sum
16
go.sum
|
@ -25,8 +25,17 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
|
|||
github.com/fxamacker/cbor/v2 v2.2.1-0.20200511212021-28e39be4a84f/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
github.com/fxamacker/cbor/v2 v2.4.0 h1:ri0ArlOR+5XunOP8CRUowT0pSJOwhW098ZCUyskZD88=
|
||||
github.com/fxamacker/cbor/v2 v2.4.0/go.mod h1:TA1xS00nchWmaBnEIxPSE5oHLuJBAVvqrtAnWBwBCVo=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
|
||||
github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
|
||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.15.3 h1:S+sSpunYjNPDuXkWbK+x+bA7iXiW296KG4dL3X7xUZo=
|
||||
github.com/go-playground/validator/v10 v10.15.3/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
|
@ -71,6 +80,8 @@ github.com/labstack/gommon v0.4.0 h1:y7cvthEAEbU0yHOf4axH8ZG2NH8knB9iNSoTO8dyIk8
|
|||
github.com/labstack/gommon v0.4.0/go.mod h1:uW6kP17uPlLJsD3ijUYn3/M5bAxtlZhMI6m3MFxTMTM=
|
||||
github.com/leanovate/gopter v0.2.5-0.20190402064358-634a59d12406 h1:+OUpk+IVvmKU0jivOVFGtOzA6U5AWFs8HE4DRzWLOUE=
|
||||
github.com/leanovate/gopter v0.2.5-0.20190402064358-634a59d12406/go.mod h1:gNcbPWNEWRe4lm+bycKqxUYoH5uoVje5SkOJ3uoLer8=
|
||||
github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
|
||||
github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/matthewhartstonge/argon2 v0.3.3 h1:38/hupgfzqO2UGxqXqmSqErE8KJvQnIxWWg7IXUqWgQ=
|
||||
|
@ -108,9 +119,14 @@ github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD
|
|||
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
|
||||
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
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.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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
|
||||
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
|
|
|
@ -13,12 +13,13 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
ErrNoSession = errors.New("No session found, please log in")
|
||||
ErrSessionExpired = errors.New("Session expired, log in again")
|
||||
ErrNoSession = errors.New("No session found, please log in")
|
||||
ErrSessionExpired = errors.New("Session expired, log in again")
|
||||
ErrValidationFailed = errors.New("Check your input data")
|
||||
)
|
||||
|
||||
func renderErrorPage(c echo.Context, status int, statusText, error string) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
strStatus := strconv.Itoa(status)
|
||||
|
||||
|
|
|
@ -10,20 +10,17 @@ import (
|
|||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func Admin() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
return echo.NewHTTPError(http.StatusUnauthorized, "Invalid credentials")
|
||||
}
|
||||
}
|
||||
const unauthorisedAccess = "Unauthorised access detected"
|
||||
|
||||
func Index() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
if sess, _ := session.Get(setting.SessionCookieName(), c); sess != nil {
|
||||
username := sess.Values["username"]
|
||||
if username != nil {
|
||||
return c.Redirect(http.StatusFound, "/home")
|
||||
if uname, ok := sess.Values["username"].(string); ok {
|
||||
if uname != "" {
|
||||
return c.Redirect(http.StatusFound, "/home")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -62,7 +59,7 @@ func Healthz() echo.HandlerFunc {
|
|||
}
|
||||
|
||||
func addHeaders(c echo.Context) {
|
||||
c.Response().Writer.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
|
||||
c.Response().Header().Set("Cross-Origin-Opener-Policy", "same-origin")
|
||||
}
|
||||
|
||||
// experimental global redirect handler?
|
||||
|
|
|
@ -23,6 +23,7 @@ func getUserByID(ctx context.Context, client *ent.Client, id string) (*ent.User,
|
|||
|
||||
func refreshSession(sess *sessions.Session, path string, maxAge int, httpOnly, secure bool, sameSite http.SameSite) {
|
||||
sess.Options = &sessions.Options{
|
||||
Domain: setting.HTTPDomain(),
|
||||
Path: path,
|
||||
MaxAge: maxAge,
|
||||
HttpOnly: httpOnly,
|
||||
|
|
|
@ -15,9 +15,7 @@ import (
|
|||
|
||||
func Home(client *ent.Client) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
var username string
|
||||
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
sess, _ := session.Get(setting.SessionCookieName(), c)
|
||||
if sess == nil {
|
||||
|
@ -25,25 +23,15 @@ func Home(client *ent.Client) echo.HandlerFunc {
|
|||
return c.Redirect(http.StatusSeeOther, "/signin")
|
||||
}
|
||||
|
||||
if sess.Values["foo"] != nil {
|
||||
log.Info("gorilla session", "custom field test", sess.Values["foo"].(string))
|
||||
}
|
||||
var username string
|
||||
|
||||
uname := sess.Values["username"]
|
||||
if uname == nil {
|
||||
username, ok := sess.Values["username"].(string)
|
||||
if !ok {
|
||||
log.Info("session cookie found but username invalid, redirecting to signin", "endpoint", "/home")
|
||||
|
||||
return c.Redirect(http.StatusSeeOther, "/signin")
|
||||
}
|
||||
|
||||
log.Info("gorilla session", "username", sess.Values["username"].(string))
|
||||
username = sess.Values["username"].(string)
|
||||
|
||||
// example denial.
|
||||
// if _, err := c.Cookie("aha"); err != nil {
|
||||
// log.Printf("error: %q", err)
|
||||
// return echo.NewHTTPError(http.StatusUnauthorized, http.StatusText(http.StatusUnauthorized))
|
||||
// }
|
||||
log.Debug("session", "username", username, "endpoint", "/home")
|
||||
|
||||
var u moduser.User
|
||||
|
||||
|
@ -94,15 +82,28 @@ func Home(client *ent.Client) echo.HandlerFunc {
|
|||
p.Name = username
|
||||
p.User = u
|
||||
|
||||
data := make(map[string]any)
|
||||
flash := sess.Values["flash"]
|
||||
|
||||
if flash != nil {
|
||||
data["flash"] = flash.(string)
|
||||
if flsh, ok := sess.Values["flash"].(string); ok {
|
||||
p.Data["flash"] = flsh
|
||||
|
||||
delete(sess.Values, "flash")
|
||||
}
|
||||
|
||||
_ = sess.Save(c.Request(), c.Response())
|
||||
if _, ok := sess.Values["reauthFlash"].(string); ok {
|
||||
p.Data["info"] = "First time after changing the password, yay!"
|
||||
// if this is the first login after the initial password change, delete
|
||||
// the cookie value.
|
||||
delete(sess.Values, "reauthFlash")
|
||||
}
|
||||
|
||||
if err := sess.Save(c.Request(), c.Response()); err != nil {
|
||||
log.Error("Failed to save session", "module", "handlers/home")
|
||||
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError)+" (make sure you've got cookies enabled)",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
err := c.Render(http.StatusOK, "home.tmpl", p)
|
||||
|
|
|
@ -12,7 +12,7 @@ import (
|
|||
|
||||
func Logout() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
switch {
|
||||
case c.Request().Method == "POST": // nolint:goconst
|
||||
|
@ -21,14 +21,21 @@ func Logout() echo.HandlerFunc {
|
|||
log.Infof("max-age before logout: %d", sess.Options.MaxAge)
|
||||
sess.Options.MaxAge = -1
|
||||
|
||||
if username := sess.Values["username"]; username != nil {
|
||||
sess.Values["username"] = ""
|
||||
}
|
||||
delete(sess.Values, "username")
|
||||
|
||||
err := sess.Save(c.Request(), c.Response())
|
||||
if err != nil {
|
||||
c.Logger().Error("could not delete session cookie")
|
||||
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
c.Response().Header().Set(echo.HeaderCacheControl, "no-store")
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusMovedPermanently, "/logout")
|
||||
|
@ -36,8 +43,10 @@ func Logout() echo.HandlerFunc {
|
|||
case c.Request().Method == "GET": // nolint:goconst
|
||||
sess, _ := session.Get(setting.SessionCookieName(), c)
|
||||
if sess != nil {
|
||||
if username := sess.Values["username"]; username != nil {
|
||||
return c.Redirect(http.StatusSeeOther, "/home")
|
||||
if uname, ok := sess.Values["username"].(string); ok {
|
||||
if uname != "" {
|
||||
return c.Redirect(http.StatusSeeOther, "/home")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,14 +11,43 @@ import (
|
|||
|
||||
func ManageAPIKeys() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
u := c.Get("sessUsr").(moduser.User)
|
||||
defer addHeaders(c)
|
||||
|
||||
u, ok := c.Get("sessUsr").(moduser.User)
|
||||
if !ok {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusUnauthorized,
|
||||
http.StatusText(http.StatusUnauthorized),
|
||||
"username was nil",
|
||||
)
|
||||
}
|
||||
|
||||
if !u.IsAdmin {
|
||||
c.Logger().Debug("this is a restricted endpoint", "endpoint", "/manage/api-keys", "user", u.Username, "isAdmin", u.IsAdmin)
|
||||
|
||||
status := http.StatusForbidden
|
||||
msg := http.StatusText(status)
|
||||
|
||||
return renderErrorPage(
|
||||
c, status, msg+": You should not be here", "Restricted endpoint",
|
||||
)
|
||||
}
|
||||
|
||||
p := newPage()
|
||||
|
||||
p.Title = "Manage API Keys"
|
||||
p.Current = "api-keys"
|
||||
p.User = u
|
||||
|
||||
if setting.APIKeyHIBP() != "" {
|
||||
p.Data["hibpApiKey"] = setting.APIKeyHIBP()
|
||||
}
|
||||
|
||||
if setting.APIKeyDehashed() != "" {
|
||||
p.Data["dehashedApiKey"] = setting.APIKeyDehashed()
|
||||
}
|
||||
|
||||
err := c.Render(http.StatusOK, "manage/apikeys.tmpl",
|
||||
p,
|
||||
)
|
||||
|
|
|
@ -18,7 +18,7 @@ import (
|
|||
|
||||
func ManageUsers() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
u, ok := c.Get("sessUsr").(moduser.User)
|
||||
if !ok {
|
||||
|
@ -46,9 +46,9 @@ func ManageUsers() echo.HandlerFunc {
|
|||
}
|
||||
|
||||
if !u.IsAdmin {
|
||||
c.Logger().Debug("this is a restricted endpoint")
|
||||
log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin)
|
||||
|
||||
status := http.StatusUnauthorized
|
||||
status := http.StatusForbidden
|
||||
msg := http.StatusText(status)
|
||||
|
||||
return renderErrorPage(
|
||||
|
@ -105,6 +105,7 @@ func ManageUsers() echo.HandlerFunc {
|
|||
IsAdmin: u.IsAdmin,
|
||||
CreatedAt: u.CreatedAt,
|
||||
UpdatedAt: u.UpdatedAt,
|
||||
LastLogin: u.LastLogin,
|
||||
}
|
||||
|
||||
allUsers = append(allUsers, usr)
|
||||
|
@ -158,7 +159,7 @@ func ManageUsers() echo.HandlerFunc {
|
|||
|
||||
func CreateUser() echo.HandlerFunc { //nolint:gocognit
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
u, ok := c.Get("sessUsr").(moduser.User)
|
||||
if !ok {
|
||||
|
@ -176,9 +177,9 @@ func CreateUser() echo.HandlerFunc { //nolint:gocognit
|
|||
}
|
||||
|
||||
if !u.IsAdmin {
|
||||
c.Logger().Debug("this is a restricted endpoint")
|
||||
log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin)
|
||||
|
||||
status := http.StatusUnauthorized
|
||||
status := http.StatusForbidden
|
||||
msg := http.StatusText(status)
|
||||
|
||||
return renderErrorPage(
|
||||
|
@ -215,6 +216,7 @@ func CreateUser() echo.HandlerFunc { //nolint:gocognit
|
|||
data["flash"] = msg
|
||||
data["form"] = uc
|
||||
p.Data = data
|
||||
p.User = u
|
||||
|
||||
return c.Render(
|
||||
http.StatusBadRequest,
|
||||
|
@ -223,6 +225,15 @@ func CreateUser() echo.HandlerFunc { //nolint:gocognit
|
|||
)
|
||||
}
|
||||
|
||||
if err := c.Validate(uc); err != nil {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
var msg string
|
||||
|
||||
usr, err := moduser.CreateUser(ctx, dbclient, uc.Email, uc.Username, uc.Password, uc.IsAdmin)
|
||||
|
@ -259,7 +270,7 @@ func CreateUser() echo.HandlerFunc { //nolint:gocognit
|
|||
|
||||
func ViewUser() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
u, ok := c.Get("sessUsr").(moduser.User)
|
||||
if !ok {
|
||||
|
@ -272,9 +283,9 @@ func ViewUser() echo.HandlerFunc {
|
|||
}
|
||||
|
||||
if !u.IsAdmin {
|
||||
c.Logger().Debug("this is a restricted endpoint")
|
||||
log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin)
|
||||
|
||||
status := http.StatusUnauthorized
|
||||
status := http.StatusForbidden
|
||||
msg := http.StatusText(status)
|
||||
|
||||
return renderErrorPage(
|
||||
|
@ -308,6 +319,15 @@ func ViewUser() echo.HandlerFunc {
|
|||
|
||||
err := c.Bind(uid)
|
||||
if err == nil { //nolint:dupl
|
||||
if err := c.Validate(uid); err != nil {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
http.StatusText(http.StatusBadRequest)+" - make sure to pass in a proper UUID",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
usr, err := getUserByID(ctx, dbclient, uid.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, moduser.ErrUserNotFound) { //nolint:gocritic
|
||||
|
@ -387,7 +407,7 @@ func ViewUser() echo.HandlerFunc {
|
|||
//nolint:dupl
|
||||
func EditUser() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
u, ok := c.Get("sessUsr").(moduser.User)
|
||||
if !ok {
|
||||
|
@ -400,9 +420,9 @@ func EditUser() echo.HandlerFunc {
|
|||
}
|
||||
|
||||
if !u.IsAdmin {
|
||||
c.Logger().Debug("this is a restricted endpoint")
|
||||
log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin)
|
||||
|
||||
status := http.StatusUnauthorized
|
||||
status := http.StatusForbidden
|
||||
msg := http.StatusText(status)
|
||||
|
||||
return renderErrorPage(
|
||||
|
@ -489,7 +509,7 @@ func EditUser() echo.HandlerFunc {
|
|||
//nolint:dupl
|
||||
func UpdateUser() echo.HandlerFunc { //nolint:gocognit
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
u, ok := c.Get("sessUsr").(moduser.User)
|
||||
if !ok {
|
||||
|
@ -502,9 +522,9 @@ func UpdateUser() echo.HandlerFunc { //nolint:gocognit
|
|||
}
|
||||
|
||||
if !u.IsAdmin {
|
||||
c.Logger().Debug("this is a restricted endpoint")
|
||||
log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin)
|
||||
|
||||
status := http.StatusUnauthorized
|
||||
status := http.StatusForbidden
|
||||
msg := http.StatusText(status)
|
||||
|
||||
return renderErrorPage(
|
||||
|
@ -635,6 +655,15 @@ func UpdateUser() echo.HandlerFunc { //nolint:gocognit
|
|||
}
|
||||
}
|
||||
|
||||
if err := c.Validate(uu); err != nil {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
if usr.Username != uu.Username {
|
||||
exists, err := moduser.UsernameExists(ctx, dbclient, uu.Username)
|
||||
if err != nil {
|
||||
|
@ -716,7 +745,7 @@ func UpdateUser() echo.HandlerFunc { //nolint:gocognit
|
|||
// DeleteUserConfirmation displays user deletion confirmation confirmation page.
|
||||
func DeleteUserConfirmation() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
u, ok := c.Get("sessUsr").(moduser.User)
|
||||
if !ok {
|
||||
|
@ -727,7 +756,9 @@ func DeleteUserConfirmation() echo.HandlerFunc {
|
|||
"username was nil",
|
||||
)
|
||||
} else if !u.IsAdmin {
|
||||
status := http.StatusUnauthorized
|
||||
log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin)
|
||||
|
||||
status := http.StatusForbidden
|
||||
msg := http.StatusText(status)
|
||||
|
||||
return renderErrorPage(
|
||||
|
@ -815,7 +846,7 @@ func DeleteUserConfirmation() echo.HandlerFunc {
|
|||
// DeleteUser handles user deletion POST requests.
|
||||
func DeleteUser() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
u, ok := c.Get("sessUsr").(moduser.User)
|
||||
if !ok {
|
||||
|
@ -826,7 +857,9 @@ func DeleteUser() echo.HandlerFunc {
|
|||
"username was nil",
|
||||
)
|
||||
} else if !u.IsAdmin {
|
||||
status := http.StatusUnauthorized
|
||||
log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin)
|
||||
|
||||
status := http.StatusForbidden
|
||||
msg := http.StatusText(status)
|
||||
|
||||
return renderErrorPage(
|
||||
|
|
|
@ -18,27 +18,42 @@ import (
|
|||
|
||||
func MiddlewareSession(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
if c.Request().URL.Path == "/logout" && c.Request().Method == "POST" {
|
||||
log.Debug("skipping auth middleware on /logout POST", "module", "handlers/middleware")
|
||||
return next(c)
|
||||
}
|
||||
|
||||
sess, _ := session.Get(setting.SessionCookieName(), c)
|
||||
|
||||
var username string
|
||||
if sess == nil {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusUnauthorized,
|
||||
http.StatusText(http.StatusUnauthorized)+" you need to log in again",
|
||||
"you need to log in again",
|
||||
)
|
||||
}
|
||||
|
||||
// uname, ok := sess.Values["username"].(string)
|
||||
uname := sess.Values["username"]
|
||||
|
||||
if uname != nil {
|
||||
username = uname.(string)
|
||||
|
||||
log.Debug("Refreshing session cookie", "username", username, "module", "middleware")
|
||||
if username, ok := sess.Values["username"].(string); ok {
|
||||
log.Info("Refreshing session cookie",
|
||||
"username", username,
|
||||
"module", "middleware",
|
||||
"maxAge", setting.SessionMaxAge(),
|
||||
"secure", setting.HTTPSecure(),
|
||||
"domain", setting.HTTPDomain(),
|
||||
)
|
||||
|
||||
refreshSession(
|
||||
sess,
|
||||
"/",
|
||||
setting.SessionMaxAge(),
|
||||
true,
|
||||
c.Request().URL.Scheme == "https", //nolint:goconst
|
||||
setting.HTTPSecure(),
|
||||
http.SameSiteStrictMode,
|
||||
)
|
||||
|
||||
sess.Values["username"] = username
|
||||
|
||||
c.Set("sess", sess)
|
||||
|
||||
var u moduser.User
|
||||
|
@ -66,7 +81,7 @@ func MiddlewareSession(next echo.HandlerFunc) echo.HandlerFunc {
|
|||
c.Set("sessUsr", u)
|
||||
|
||||
if err := sess.Save(c.Request(), c.Response()); err != nil {
|
||||
c.Logger().Error("Failed to save session", "module", "middleware")
|
||||
log.Error("Failed to save session", "module", "middleware")
|
||||
|
||||
return renderErrorPage(
|
||||
c,
|
||||
|
@ -79,21 +94,33 @@ func MiddlewareSession(next echo.HandlerFunc) echo.HandlerFunc {
|
|||
return next(c)
|
||||
}
|
||||
|
||||
log.Warn("Could not get username from the cookie", "module", "handlers/middleware")
|
||||
|
||||
if !sess.IsNew {
|
||||
c.Logger().Errorf("%d - %s", http.StatusUnauthorized, "you need to log in")
|
||||
log.Warn("Expired session cookie (without a username) found, redirecting to sign in", "module", "handlers/middleware")
|
||||
|
||||
sess.Values["info"] = "Log in again, please."
|
||||
|
||||
if err := sess.Save(c.Request(), c.Response()); err != nil {
|
||||
log.Error("Failed to save session", "module", "middleware")
|
||||
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError)+" could not save the session cookie",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusTemporaryRedirect, "/signin")
|
||||
}
|
||||
|
||||
// return renderErrorPage(
|
||||
// c,
|
||||
// http.StatusUnauthorized,
|
||||
// http.StatusText(http.StatusUnauthorized),
|
||||
// ErrNoSession.Error(),
|
||||
// )
|
||||
|
||||
c.Logger().Warn("Could not get username from the cookie")
|
||||
|
||||
return next(c)
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusUnauthorized,
|
||||
http.StatusText(http.StatusUnauthorized),
|
||||
ErrNoSession.Error(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package handlers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
@ -16,13 +17,15 @@ import (
|
|||
|
||||
func GetSearchHIBP() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
u, ok := c.Get("sessUsr").(moduser.User)
|
||||
if !ok {
|
||||
c.Logger().Warnf("Error getting user from session cookie")
|
||||
} else if u.IsAdmin {
|
||||
status := http.StatusUnauthorized
|
||||
log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin)
|
||||
|
||||
status := http.StatusForbidden
|
||||
msg := http.StatusText(status)
|
||||
|
||||
return renderErrorPage(
|
||||
|
@ -30,6 +33,25 @@ func GetSearchHIBP() echo.HandlerFunc {
|
|||
)
|
||||
}
|
||||
|
||||
if !u.IsAdmin {
|
||||
ctx := context.WithValue(context.Background(), moduser.CtxKey{}, slogger)
|
||||
|
||||
f, err := moduser.UsrFinishedSetup(ctx, dbclient, u.ID)
|
||||
if err != nil {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
if !f {
|
||||
log.Warn("resource access attempt without performing the initial password change", "user", u.Username, "endpoint", "/user/hibp-search")
|
||||
return c.Redirect(http.StatusSeeOther, "/user/initial-password-change")
|
||||
}
|
||||
}
|
||||
|
||||
csrf := c.Get("csrf").(string)
|
||||
p := newPage()
|
||||
|
||||
|
@ -70,7 +92,7 @@ func GetSearchHIBP() echo.HandlerFunc {
|
|||
|
||||
func SearchHIBP() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
u, ok := c.Get("sessUsr").(moduser.User)
|
||||
if !ok {
|
||||
|
@ -82,7 +104,9 @@ func SearchHIBP() echo.HandlerFunc {
|
|||
// )
|
||||
c.Logger().Warnf("Error getting user from session cookie")
|
||||
} else if u.IsAdmin {
|
||||
status := http.StatusUnauthorized
|
||||
log.Warn(unauthorisedAccess, "endpoint", c.Path(), "method", c.Request().Method, "user", u.Username, "isAdmin", u.IsAdmin)
|
||||
|
||||
status := http.StatusForbidden
|
||||
msg := http.StatusText(status)
|
||||
|
||||
return renderErrorPage(
|
||||
|
@ -90,6 +114,29 @@ func SearchHIBP() echo.HandlerFunc {
|
|||
)
|
||||
}
|
||||
|
||||
if !u.IsAdmin {
|
||||
ctx := context.WithValue(context.Background(), moduser.CtxKey{}, slogger)
|
||||
|
||||
f, err := moduser.UsrFinishedSetup(ctx, dbclient, u.ID)
|
||||
if err != nil {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
if !f {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusUnauthorized,
|
||||
http.StatusText(http.StatusUnauthorized)+" - you need to perform your initial password change before accessing this resource",
|
||||
"user never changed password",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
csrf := c.Get("csrf").(string)
|
||||
|
||||
a := new(hibpSearch)
|
||||
|
@ -102,6 +149,15 @@ func SearchHIBP() echo.HandlerFunc {
|
|||
)
|
||||
}
|
||||
|
||||
if err := c.Validate(a); err != nil {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
breachNames, err := hibp.GetAllBreachesForAccount(a.Account)
|
||||
if err != nil {
|
||||
msg := "Error getting breaches for this account"
|
||||
|
|
|
@ -5,6 +5,7 @@ package handlers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
|
@ -18,15 +19,16 @@ import (
|
|||
|
||||
func Signin() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
if sess, _ := session.Get(setting.SessionCookieName(), c); sess != nil {
|
||||
username := sess.Values["username"]
|
||||
if username != nil {
|
||||
return c.Redirect(http.StatusFound, "/home")
|
||||
}
|
||||
reauth := false
|
||||
r := c.QueryParam("reauth")
|
||||
|
||||
if r == "true" {
|
||||
reauth = true
|
||||
}
|
||||
|
||||
sess, _ := session.Get(setting.SessionCookieName(), c)
|
||||
csrf := c.Get("csrf").(string)
|
||||
p := newPage()
|
||||
|
||||
|
@ -34,6 +36,47 @@ func Signin() echo.HandlerFunc {
|
|||
p.Current = "signin"
|
||||
p.CSRF = csrf
|
||||
|
||||
var uf *userSignin
|
||||
|
||||
if sess != nil {
|
||||
if uname, ok := sess.Values["username"].(string); ok && uname != "" {
|
||||
if !reauth {
|
||||
return c.Redirect(http.StatusPermanentRedirect, "/home")
|
||||
}
|
||||
|
||||
uf = &userSignin{Username: uname}
|
||||
}
|
||||
}
|
||||
|
||||
if reauth {
|
||||
fl := sess.Values["reauthFlash"]
|
||||
if reFl, ok := fl.(string); !ok {
|
||||
p.Data["info"] = "re-login, please."
|
||||
} else {
|
||||
p.Data["reauthFlash"] = reFl
|
||||
if uf != nil {
|
||||
p.Data["form"] = uf
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if i, ok := sess.Values["info"].(string); ok {
|
||||
p.Data["infoFlash"] = i
|
||||
|
||||
delete(sess.Values, "info")
|
||||
|
||||
if err := sess.Save(c.Request(), c.Response()); err != nil {
|
||||
log.Error("Failed to save session", "module", "middleware")
|
||||
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError)+" could not save the session cookie",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return c.Render(
|
||||
http.StatusOK,
|
||||
"signin.tmpl",
|
||||
|
@ -44,7 +87,7 @@ func Signin() echo.HandlerFunc {
|
|||
|
||||
func SigninPost(client *ent.Client) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
cu := new(userSignin)
|
||||
if err := c.Bind(cu); err != nil {
|
||||
|
@ -80,10 +123,21 @@ func SigninPost(client *ent.Client) echo.HandlerFunc {
|
|||
)
|
||||
}
|
||||
|
||||
if err := c.Validate(cu); err != nil {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
loginFailed := "Login Failed!"
|
||||
|
||||
ctx := context.WithValue(context.Background(), moduser.CtxKey{}, slogger)
|
||||
if usr, err := moduser.QueryUser(ctx, client, username); err == nil {
|
||||
usr, err := moduser.QueryUser(ctx, client, username)
|
||||
|
||||
if err == nil {
|
||||
log.Info("attempting login", "user", &usr.ID)
|
||||
|
||||
if !passwd.Compare(usr.Password, password) {
|
||||
|
@ -122,18 +176,15 @@ func SigninPost(client *ent.Client) echo.HandlerFunc {
|
|||
)
|
||||
}
|
||||
|
||||
secure := c.Request().URL.Scheme == "https" //nolint:goconst
|
||||
|
||||
sess, _ := session.Get(setting.SessionCookieName(), c)
|
||||
if sess != nil {
|
||||
sess.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: setting.SessionMaxAge(),
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
Secure: setting.HTTPSecure(),
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
sess.Values["foo"] = "bar"
|
||||
|
||||
c.Logger().Debug("saving username to the session cookie")
|
||||
|
||||
|
@ -152,6 +203,19 @@ func SigninPost(client *ent.Client) echo.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
if err = moduser.UpdateUserLastLogin(ctx, client, usr.ID, usr.IsAdmin); err != nil {
|
||||
if !errors.Is(err, moduser.ErrUnfinishedSetupLastLoginUpdate) {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
log.Error("could not update LastLogin", "endpoint", "/home", "method", "post")
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusMovedPermanently, "/home")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,34 +17,16 @@ import (
|
|||
|
||||
func Signup() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
sess, _ := session.Get(setting.SessionCookieName(), c)
|
||||
if sess != nil {
|
||||
log.Info("gorilla session", "endpoint", "signup")
|
||||
|
||||
username := sess.Values["username"]
|
||||
if username != nil {
|
||||
if uname, ok := sess.Values["username"].(string); ok && uname != "" {
|
||||
return c.Redirect(http.StatusFound, "/home")
|
||||
}
|
||||
}
|
||||
|
||||
// tpl := getTmpl("signup.tmpl")
|
||||
|
||||
csrf := c.Get("csrf").(string)
|
||||
|
||||
// secure := c.Request().URL.Scheme == "https"
|
||||
// cookieCSRF := &http.Cookie{
|
||||
// Name: "_csrf",
|
||||
// Value: csrf,
|
||||
// // SameSite: http.SameSiteStrictMode,
|
||||
// SameSite: http.SameSiteLaxMode,
|
||||
// MaxAge: 3600,
|
||||
// Secure: secure,
|
||||
// HttpOnly: true,
|
||||
// }
|
||||
// c.SetCookie(cookieCSRF)
|
||||
|
||||
p := newPage()
|
||||
|
||||
p.Title = "Sign up"
|
||||
|
@ -73,7 +55,7 @@ func Signup() echo.HandlerFunc {
|
|||
|
||||
func SignupPost(client *ent.Client) echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
cu := new(userSignup)
|
||||
if err := c.Bind(cu); err != nil {
|
||||
|
@ -90,6 +72,15 @@ func SignupPost(client *ent.Client) echo.HandlerFunc {
|
|||
return c.Redirect(http.StatusFound, "/singup")
|
||||
}
|
||||
|
||||
if err := c.Validate(cu); err != nil {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
username := cu.Username
|
||||
email := cu.Email
|
||||
passwd := cu.Password
|
||||
|
@ -140,17 +131,14 @@ func SignupPost(client *ent.Client) echo.HandlerFunc {
|
|||
log.Infof("successfully registered user '%s'", u.Username)
|
||||
log.Debug("user details", "id", u.ID, "email", u.Email, "isAdmin", u.IsAdmin)
|
||||
|
||||
secure := c.Request().URL.Scheme == "https" //nolint:goconst
|
||||
|
||||
sess, _ := session.Get(setting.SessionCookieName(), c)
|
||||
sess.Options = &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: setting.SessionMaxAge(),
|
||||
HttpOnly: true,
|
||||
Secure: secure,
|
||||
Secure: setting.HTTPSecure(),
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
sess.Values["foo"] = "bar"
|
||||
sess.Values["username"] = username
|
||||
|
||||
err = sess.Save(c.Request(), c.Response())
|
||||
|
|
|
@ -4,39 +4,40 @@
|
|||
package handlers
|
||||
|
||||
type userSignin struct {
|
||||
Username string `form:"username" json:"username" validate:"required,username"`
|
||||
Password string `form:"password" json:"password" validate:"required,password"`
|
||||
Username string `form:"username" json:"username" validate:"required,gte=2"`
|
||||
Password string `form:"password" json:"password" validate:"required,gte=20"`
|
||||
}
|
||||
|
||||
type userSignup struct {
|
||||
Username string `form:"username" json:"username" validate:"required,username"`
|
||||
Email string `form:"email" json:"email" validate:"required,email"`
|
||||
Password string `form:"password" json:"password" validate:"required,password"`
|
||||
Username string `form:"username" json:"username" validate:"required,gte=2"`
|
||||
Email string `form:"email" json:"email" validate:"required,email,gte=3"`
|
||||
Password string `form:"password" json:"password" validate:"required,gte=20"`
|
||||
}
|
||||
|
||||
// this struct is also used on update by admins, which is why the password fields are omitempty.
|
||||
// when users finish setting up, admins can no longer change their passwords.
|
||||
type userCreate struct {
|
||||
Username string `form:"username" json:"username" validate:"required,username"`
|
||||
Email string `form:"email" json:"email" validate:"required,email"`
|
||||
Password string `form:"password" json:"password" validate:"omitempty,password"`
|
||||
RepeatPassword string `form:"repeatPassword" json:"repeatPassword" validate:"omitempty,repeatPassword"`
|
||||
IsAdmin bool `form:"isAdmin" json:"isAdmin" validate:"required,isAdmin"`
|
||||
IsActive *bool `form:"isActive" json:"isActive" validate:"omitempty,isActive"`
|
||||
Username string `form:"username" json:"username" validate:"required,gte=2"`
|
||||
Email string `form:"email" json:"email" validate:"required,email,gte=3"`
|
||||
Password string `form:"password" json:"password" validate:"omitempty,gte=20,eqfield=RepeatPassword"`
|
||||
RepeatPassword string `form:"repeatPassword" json:"repeatPassword" validate:"omitempty,gte=20,eqfield=Password"`
|
||||
IsAdmin bool `form:"isAdmin" json:"isAdmin" validate:"omitempty"`
|
||||
IsActive *bool `form:"isActive" json:"isActive" validate:"omitempty"`
|
||||
}
|
||||
|
||||
type userID struct {
|
||||
ID string `param:"id" validate:"required,id"`
|
||||
ID string `param:"id" validate:"required,uuid"`
|
||||
}
|
||||
|
||||
type initPasswordChange struct {
|
||||
NewPassword string `form:"new-password" validate:"required,new-password"`
|
||||
NewPassword string `form:"new-password" validate:"required,gte=20,eqfield=RepeatNewPassword"`
|
||||
RepeatNewPassword string `form:"repeat-new-password" validate:"required,gte=20,eqfield=NewPassword"`
|
||||
}
|
||||
|
||||
type hibpSearch struct {
|
||||
Account string `form:"search" validate:"required,search"`
|
||||
Account string `form:"search" validate:"required,gt=2"`
|
||||
}
|
||||
|
||||
type hibpBreachDetail struct {
|
||||
BreachName string `param:"name" validate:"required,name"`
|
||||
BreachName string `param:"name" validate:"required,gt=0"`
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ package handlers
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
|
||||
moduser "git.dotya.ml/mirre-mt/pcmt/modules/user"
|
||||
|
@ -12,9 +13,9 @@ import (
|
|||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
func InitialPasswordChange() echo.HandlerFunc {
|
||||
func InitialPasswordChangePost() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
u, ok := c.Get("sessUsr").(moduser.User)
|
||||
if !ok {
|
||||
|
@ -25,7 +26,9 @@ func InitialPasswordChange() echo.HandlerFunc {
|
|||
"Username was nil",
|
||||
)
|
||||
} else if u.IsAdmin {
|
||||
status := http.StatusUnauthorized
|
||||
log.Warn("this is a restricted endpoint", "endpoint", "/user/initial-password-change", "user", u.Username, "isAdmin", u.IsAdmin, "route name", c.Path())
|
||||
|
||||
status := http.StatusForbidden
|
||||
msg := http.StatusText(status)
|
||||
|
||||
return renderErrorPage(
|
||||
|
@ -41,89 +44,164 @@ func InitialPasswordChange() echo.HandlerFunc {
|
|||
f, err := moduser.UsrFinishedSetup(ctx, dbclient, u.ID)
|
||||
|
||||
switch {
|
||||
case c.Request().Method == "POST":
|
||||
case err != nil:
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError),
|
||||
err.Error(),
|
||||
)
|
||||
|
||||
case f:
|
||||
return c.Redirect(http.StatusSeeOther, "/user/init-password-change")
|
||||
}
|
||||
|
||||
pw := new(initPasswordChange)
|
||||
|
||||
if err := c.Bind(pw); err != nil {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
http.StatusText(http.StatusBadRequest),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
if pw.NewPassword != pw.RepeatNewPassword {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
http.StatusText(http.StatusBadRequest)+" - the passwords were not the same",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
if err := c.Validate(pw); err != nil {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
err = moduser.ChangePassFirstLogin(ctx, dbclient, u.ID, pw.NewPassword)
|
||||
if err != nil {
|
||||
c.Logger().Errorf("error changing initial user password: %q", err)
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError),
|
||||
err.Error(),
|
||||
)
|
||||
|
||||
case f:
|
||||
return c.Redirect(http.StatusSeeOther, "/user/init-password-change")
|
||||
}
|
||||
|
||||
pw := new(initPasswordChange)
|
||||
|
||||
if err := c.Bind(pw); err != nil {
|
||||
case errors.Is(err, moduser.ErrPasswordEmpty):
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
http.StatusText(http.StatusBadRequest),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
err = moduser.ChangePassFirstLogin(ctx, dbclient, u.ID, pw.NewPassword)
|
||||
if err != nil {
|
||||
c.Logger().Errorf("error changing initial user password: %q", err)
|
||||
|
||||
case errors.Is(err, moduser.ErrNewPasswordCannotEqual):
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError),
|
||||
http.StatusBadRequest,
|
||||
http.StatusText(http.StatusBadRequest)+" - the new password needs to be different from the original",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
if sess, ok := c.Get("sess").(*sessions.Session); ok {
|
||||
sess.Values["flash"] = "Successfully updated your password"
|
||||
_ = sess.Save(c.Request(), c.Response())
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusSeeOther, "/home")
|
||||
|
||||
case c.Request().Method == "GET":
|
||||
switch {
|
||||
case err != nil:
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError),
|
||||
err.Error(),
|
||||
)
|
||||
|
||||
case f:
|
||||
return c.Redirect(http.StatusSeeOther, "/home")
|
||||
}
|
||||
|
||||
csrf := c.Get("csrf").(string)
|
||||
p := newPage()
|
||||
|
||||
p.Title = "Initial password change"
|
||||
p.Current = "init-password-change"
|
||||
p.CSRF = csrf
|
||||
p.User = u
|
||||
|
||||
err := c.Render(
|
||||
http.StatusOK,
|
||||
"user/init-password-change.tmpl",
|
||||
p,
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError),
|
||||
err.Error(),
|
||||
)
|
||||
if err != nil {
|
||||
c.Logger().Errorf("error: %q", err)
|
||||
}
|
||||
|
||||
log.Info("successfully performed initial password change", "user", u.Username)
|
||||
|
||||
if sess, ok := c.Get("sess").(*sessions.Session); ok {
|
||||
sess.Values["reauthFlash"] = "Successfully updated your password, log in again, please"
|
||||
|
||||
if err = sess.Save(c.Request(), c.Response()); err != nil {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError),
|
||||
http.StatusText(http.StatusInternalServerError)+" - could not change the session cookie",
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return c.Redirect(http.StatusSeeOther, "/signin?reauth=true")
|
||||
}
|
||||
}
|
||||
|
||||
func InitialPasswordChange() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
defer addHeaders(c)
|
||||
|
||||
u, ok := c.Get("sessUsr").(moduser.User)
|
||||
if !ok {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusUnauthorized,
|
||||
http.StatusText(http.StatusUnauthorized)+", perhaps you need to log in first?",
|
||||
"Username was nil",
|
||||
)
|
||||
} else if u.IsAdmin {
|
||||
log.Warn("this is a restricted endpoint", "endpoint", "/user/initial-password-change", "user", u.Username, "isAdmin", u.IsAdmin, "route name", c.Path())
|
||||
|
||||
status := http.StatusForbidden
|
||||
msg := http.StatusText(status)
|
||||
|
||||
return renderErrorPage(
|
||||
c, status, msg+": You should not be here", "This endpoint is for users only",
|
||||
)
|
||||
}
|
||||
|
||||
ctx, ok := c.Get("sloggerCtx").(context.Context)
|
||||
if !ok {
|
||||
ctx = context.WithValue(context.Background(), moduser.CtxKey{}, slogger)
|
||||
}
|
||||
|
||||
f, err := moduser.UsrFinishedSetup(ctx, dbclient, u.ID)
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError),
|
||||
err.Error(),
|
||||
)
|
||||
|
||||
case f:
|
||||
return c.Redirect(http.StatusSeeOther, "/home")
|
||||
}
|
||||
|
||||
csrf := c.Get("csrf").(string)
|
||||
p := newPage()
|
||||
|
||||
p.Title = "Initial password change"
|
||||
p.Current = "init-password-change"
|
||||
p.CSRF = csrf
|
||||
p.User = u
|
||||
p.Name = u.Username
|
||||
|
||||
err = c.Render(
|
||||
http.StatusOK,
|
||||
"user/init-password-change.tmpl",
|
||||
p,
|
||||
)
|
||||
if err != nil {
|
||||
c.Logger().Errorf("error: %q", err)
|
||||
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusInternalServerError,
|
||||
http.StatusText(http.StatusInternalServerError),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,18 +14,11 @@ import (
|
|||
|
||||
func ViewHIBP() echo.HandlerFunc {
|
||||
return func(c echo.Context) error {
|
||||
addHeaders(c)
|
||||
defer addHeaders(c)
|
||||
|
||||
u, ok := c.Get("sessUsr").(moduser.User)
|
||||
if !ok {
|
||||
c.Logger().Warnf("Error getting user from session cookie")
|
||||
} else if u.IsAdmin {
|
||||
status := http.StatusUnauthorized
|
||||
msg := http.StatusText(status)
|
||||
|
||||
return renderErrorPage(
|
||||
c, status, msg+": You should not be here", "This endpoint is for users only",
|
||||
)
|
||||
}
|
||||
|
||||
h := new(hibpBreachDetail)
|
||||
|
@ -38,6 +31,15 @@ func ViewHIBP() echo.HandlerFunc {
|
|||
)
|
||||
}
|
||||
|
||||
if err := c.Validate(h); err != nil {
|
||||
return renderErrorPage(
|
||||
c,
|
||||
http.StatusBadRequest,
|
||||
http.StatusText(http.StatusBadRequest)+" - "+ErrValidationFailed.Error(),
|
||||
err.Error(),
|
||||
)
|
||||
}
|
||||
|
||||
ctx, ok := c.Get("sloggerCtx").(context.Context)
|
||||
if !ok {
|
||||
ctx = context.WithValue(context.Background(), hibp.CtxKey{}, slogger)
|
||||
|
|
|
@ -6,8 +6,11 @@ package user
|
|||
import "errors"
|
||||
|
||||
var (
|
||||
ErrUsersAlreadyPresent = errors.New("don't call CreateFirst when there already are another users")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrFailedToQueryUser = errors.New("failed to query user")
|
||||
ErrBadUUID = errors.New("invalid uuid")
|
||||
ErrUsersAlreadyPresent = errors.New("don't call CreateFirst when there already are another users")
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrFailedToQueryUser = errors.New("failed to query user")
|
||||
ErrBadUUID = errors.New("invalid uuid")
|
||||
ErrPasswordEmpty = errors.New("password was empty")
|
||||
ErrNewPasswordCannotEqual = errors.New("the new password cannot be the same as the old one")
|
||||
ErrUnfinishedSetupLastLoginUpdate = errors.New("not updating last_login for users with unfinished setup")
|
||||
)
|
||||
|
|
|
@ -120,6 +120,19 @@ func QueryUserByID(ctx context.Context, client *ent.Client, strID string) (*ent.
|
|||
return nil, ErrBadUUID
|
||||
}
|
||||
|
||||
return QueryUserByUUID(ctx, client, id)
|
||||
}
|
||||
|
||||
// QueryUserByUUID returns user for the provided ID, and nil if err == nil, nil
|
||||
// and err otherwise.
|
||||
func QueryUserByUUID(ctx context.Context, client *ent.Client, id uuid.UUID) (*ent.User, error) {
|
||||
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
|
||||
log := *slogger
|
||||
|
||||
log.Logger = log.Logger.With(
|
||||
slog.Group("pcmt extra", slog.String("module", "modules/user")),
|
||||
)
|
||||
|
||||
u, err := client.User.
|
||||
Query().
|
||||
Where(user.IDEQ(id)).
|
||||
|
@ -171,6 +184,22 @@ func ChangePassFirstLogin(ctx context.Context, client *ent.Client, id uuid.UUID,
|
|||
return nil
|
||||
}
|
||||
|
||||
if password == "" {
|
||||
return ErrPasswordEmpty
|
||||
}
|
||||
|
||||
{
|
||||
u, err := QueryUserByUUID(ctx, client, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
equal := passwd.Compare(u.Password, password)
|
||||
if equal {
|
||||
return ErrNewPasswordCannotEqual
|
||||
}
|
||||
}
|
||||
|
||||
var digest []byte
|
||||
|
||||
digest, err = passwd.GetHash(password)
|
||||
|
@ -230,8 +259,9 @@ func UpdateUserByAdmin(ctx context.Context, client *ent.Client, id uuid.UUID, em
|
|||
|
||||
var u int
|
||||
|
||||
switch {
|
||||
// ignore updates to password when user finished setting up (if not admin).
|
||||
if !isAdmin && finishedSetup {
|
||||
case !isAdmin && finishedSetup:
|
||||
u, err = client.User.
|
||||
Update().Where(user.IDEQ(id)).
|
||||
SetEmail(email).
|
||||
|
@ -239,23 +269,44 @@ func UpdateUserByAdmin(ctx context.Context, client *ent.Client, id uuid.UUID, em
|
|||
SetIsAdmin(isAdmin).
|
||||
SetIsActive(active).
|
||||
Save(ctx)
|
||||
} else {
|
||||
|
||||
default:
|
||||
var digest []byte
|
||||
|
||||
digest, err = passwd.GetHash(password)
|
||||
if err != nil {
|
||||
if digest, err = passwd.GetHash(password); err != nil {
|
||||
log.Errorf("error hashing password: %s", err)
|
||||
return errors.New("could not hash password")
|
||||
}
|
||||
|
||||
u, err = client.User.
|
||||
Update().Where(user.IDEQ(id)).
|
||||
SetEmail(email).
|
||||
SetUsername(username).
|
||||
SetPassword(digest).
|
||||
SetIsAdmin(isAdmin).
|
||||
SetIsActive(active).
|
||||
Save(ctx)
|
||||
var origU *ent.User
|
||||
|
||||
if origU, err = QueryUserByUUID(ctx, client, id); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// handle a situation when an admin account is demoted to a
|
||||
// regular-user level. reset last-login so as to force the user to go
|
||||
// through the initial password change flow.
|
||||
if origU.IsAdmin && !isAdmin {
|
||||
u, err = client.User.
|
||||
Update().Where(user.IDEQ(id)).
|
||||
SetEmail(email).
|
||||
SetUsername(username).
|
||||
SetPassword(digest).
|
||||
SetIsAdmin(isAdmin).
|
||||
SetIsActive(active).
|
||||
SetLastLogin(time.Unix(0, 0)).
|
||||
Save(ctx)
|
||||
} else {
|
||||
u, err = client.User.
|
||||
Update().Where(user.IDEQ(id)).
|
||||
SetEmail(email).
|
||||
SetUsername(username).
|
||||
SetPassword(digest).
|
||||
SetIsAdmin(isAdmin).
|
||||
SetIsActive(active).
|
||||
Save(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
switch {
|
||||
|
@ -280,6 +331,42 @@ func UpdateUserByAdmin(ctx context.Context, client *ent.Client, id uuid.UUID, em
|
|||
return nil
|
||||
}
|
||||
|
||||
// UpdateUserLastLogin serves to update the last_login param of the user. This
|
||||
// parameter will not get updated for users that never finished setting up,
|
||||
// return nil on success and error on err.
|
||||
func UpdateUserLastLogin(ctx context.Context, client *ent.Client, id uuid.UUID, isAdmin bool) error {
|
||||
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
|
||||
log := *slogger
|
||||
|
||||
log.Logger = log.Logger.With(
|
||||
slog.Group("pcmt extra", slog.String("module", "modules/user")),
|
||||
)
|
||||
|
||||
finishedSetup, err := UsrFinishedSetup(ctx, client, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !isAdmin && !finishedSetup {
|
||||
return ErrUnfinishedSetupLastLoginUpdate
|
||||
}
|
||||
|
||||
u, err := client.User.
|
||||
Update().Where(user.IDEQ(id)).
|
||||
SetLastLogin(time.Now()).
|
||||
Save(ctx)
|
||||
|
||||
switch {
|
||||
case err != nil:
|
||||
return fmt.Errorf("failed to update last_login for user: %w", err)
|
||||
|
||||
case u > 1:
|
||||
return fmt.Errorf("somehow updated last_login for more than one user? count: %d", u)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteUserByID returns nil on successful deletion, err otherwise.
|
||||
func DeleteUserByID(ctx context.Context, client *ent.Client, strID string) error {
|
||||
slogger := ctx.Value(CtxKey{}).(*slogging.Slogger)
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
// Copyright 2023 wanderer <a_mirre at utb dot cz>
|
||||
// SPDX-License-Identifier: AGPL-3.0-only
|
||||
|
||||
package validation
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-playground/validator/v10"
|
||||
"github.com/labstack/echo/v4"
|
||||
)
|
||||
|
||||
// Validator defines a validator that can be used with the echo framework.
|
||||
type Validator struct {
|
||||
validator *validator.Validate
|
||||
}
|
||||
|
||||
// New provides a new instance of type Validator, initialised with the default
|
||||
// validator.
|
||||
func New() *Validator {
|
||||
return &Validator{
|
||||
validator: validator.New(
|
||||
validator.WithRequiredStructEnabled(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Validate implements echo framework's Validator interface.
|
||||
func (v *Validator) Validate(a any) error {
|
||||
if err := v.validator.Struct(a); err != nil {
|
||||
// Optionally, you could return the error to give each route more control over the status code
|
||||
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{{- if true -}}
|
||||
<meta charset="utf-8">
|
||||
|
@ -14,6 +14,8 @@
|
|||
<meta property="og:title" content="{{ .AppName }}">
|
||||
<meta property="og:site_name" content="{{ .AppName }}">
|
||||
{{- end -}}
|
||||
<meta name="description" content="Password Compromise Monitoring Tool">
|
||||
<meta property="og:description" content="Password Compromise Monitoring Tool">
|
||||
<meta property="og:type" content="website">
|
||||
<!-- <meta name="referrer" content="no-referrer, strict-origin-when-cross-origin"> -->
|
||||
<meta name="referrer" content="strict-origin-when-cross-origin">
|
||||
|
@ -23,11 +25,7 @@
|
|||
<link href="/assets/css/pcmt.css" rel="stylesheet" integrity="{{- sha384 "css/pcmt.css" -}}">
|
||||
|
||||
{{- if .DevelMode -}}
|
||||
<!-- <meta http-equiv="content-security-policy" content="upgrade-insecure-requests; default-src 'self'; connect-src 'self' ws://localhost:3002 http://localhost:3002; script-src 'self' http://localhost:3002; style-src 'self' 'unsafe-inline';"/> -->
|
||||
<meta http-equiv="content-security-policy" content="default-src 'self'; connect-src 'self' ws://localhost:3002 http://localhost:3002; script-src 'self' http://localhost:3002;"/>
|
||||
<!-- inject browsersync script if running in devel mode -->
|
||||
{{ template "browsersync.tmpl" }}
|
||||
{{ else }}
|
||||
<meta http-equiv="content-security-policy" content="upgrade-insecure-requests; default-src 'self'; connect-src 'self'; script-src 'self';"/>
|
||||
{{- end -}}
|
||||
</head>
|
||||
|
|
|
@ -4,19 +4,82 @@
|
|||
<main class="grow">
|
||||
<div class="px-2 md:px-0 place-items-center text-center">
|
||||
{{ if and .Data .Data.flash }}
|
||||
<h1 class="text-xl text-pink-600 dark:text-pink-500 py-2">
|
||||
<h1 class="mt-8 text-xl text-pink-600 dark:text-pink-500 py-2">
|
||||
{{ .Data.flash }}
|
||||
</h1>
|
||||
{{- end }}
|
||||
{{ if and .Data .Data.info }}
|
||||
<h2 class="mt-4 text-xl font-bold text-green-600 dark:text-green-500 py-2">
|
||||
{{ .Data.info }}
|
||||
</h2>
|
||||
{{- end }}
|
||||
{{ if .Name -}}
|
||||
<h1 class="mt-20 text-2xl text-pink-400 font-bold">
|
||||
Welcome, <code>{{.Name}}</code>!<br>
|
||||
<h1 class="mt-14 text-2xl text-pink-400 font-bold">
|
||||
Welcome to
|
||||
<code class="p-1 rounded-sm border-l-2 border-fuchsia-200">
|
||||
{{ .AppName }}</code>,
|
||||
<code class="p-1 rounded-sm bg-fuchsia-200">
|
||||
{{.Name}}</code>!
|
||||
</h1>
|
||||
{{if .User}}
|
||||
{{if .User.IsAdmin}}
|
||||
<div class="px-10 mx-16 md:mx-32">
|
||||
<p class="mt-4 px-12 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span class="font-mono text-md">🛈</span> To query
|
||||
the breach APIs, you need to create a <b>user-level</b>
|
||||
account. To use it, log into it in another browser
|
||||
(<em>not</em> just another browser window, as normally that
|
||||
would share the session details of this user). User and
|
||||
administrator-level accounts each have distinct
|
||||
capabilities. Similarly to how it is only possible to
|
||||
manage users using the administrator-level account,
|
||||
browsing breach data is only allowed for user-level
|
||||
accounts
|
||||
<a href="https://git.dotya.ml/mirre-mt/masters-thesis/" rel="noopener"
|
||||
target="_blank"
|
||||
class="font-medium text-blue-600 hover:underline dark:text-blue-500">
|
||||
🛈 learn more about this project</a>.
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span class="font-mono text-md">🛈</span> Add a new user
|
||||
<a href="/manage/users/new"
|
||||
class="font-medium text-blue-600 hover:underline dark:text-blue-500">
|
||||
from here</a>, or
|
||||
<a href="/manage/users"
|
||||
class="font-medium text-blue-600 hover:underline dark:text-blue-500">
|
||||
view a listing of all existing users</a>.
|
||||
</p>
|
||||
<p class="mt-4 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span class="font-mono text-md">🛈</span> You can also view the configured API keys
|
||||
<a href="/manage/api-keys"
|
||||
class="font-medium text-blue-600 hover:underline dark:text-blue-500">
|
||||
here</a>.
|
||||
</p>
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="px-10 mx-16 md:px-32">
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span class="font-mono text-md">🛈</span>
|
||||
Check whether your email account is a part of a breach
|
||||
<a href="/user/hibp-search"
|
||||
class="font-medium text-blue-600 hover:underline dark:text-blue-500">
|
||||
here</a>.
|
||||
</p>
|
||||
<p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
<span class="font-mono text-md">🛈</span>
|
||||
Alternatively,
|
||||
<a href="https://git.dotya.ml/mirre-mt/masters-thesis/"
|
||||
class="font-medium text-blue-600 hover:underline dark:text-blue-500">
|
||||
learn more</a> about this project.
|
||||
</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
<div class="mt-8 md:flex md:items-center md:place-items-center md:justify-between">
|
||||
<form method="POST" class="w-full md:w-5xl" action="/logout">
|
||||
<input type="hidden" name="csrf" value="{{- .CSRF -}}">
|
||||
<button
|
||||
class="w-full py-3 text-sm font-medium tracking-wide text-white capitalize transition-colors duration-300 transform bg-blue-500 rounded-lg md:w-1/4 hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50"
|
||||
class="w-full py-3 text-sm font-medium tracking-wide text-white capitalize transition-colors duration-300 transform bg-blue-500 rounded-lg md:w-1/4 hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50"
|
||||
type="submit">Logout</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -14,27 +14,52 @@
|
|||
Manage Your API keys
|
||||
</h1>
|
||||
|
||||
<!-- hibp form -->
|
||||
<div class="p-2 mx-auto mt-8 lg:max-w-5xl lg:mt-0 block">
|
||||
{{- $hibpK := .Data.hibpApiKey -}}
|
||||
<!-- hibp form -->
|
||||
<form method="post" class="w-full lg:max-w-7xl px-2">
|
||||
<input type="hidden" name="csrf" value="{{- .CSRF -}}">
|
||||
<div class="items-center">
|
||||
<label for="hibpApiKey" class="text-purple-500 font-bold">
|
||||
Have I Been Pwned?
|
||||
</label>
|
||||
<input name="hibpApiKey" type="text" placeholder="HIBP API key" {{if and .Data .Data.hibpApiKey}}value="{{.Data.hibpApiKey}}"{{end}} autofocus class="py-3 px-8 mt-2 w-full valid:border text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<p id="hibp-expl" class="hover:cursor-help mt-2 text-sm text-gray-500 dark:text-gray-400">Adds support for querying Troy Hunt's <a href="https://haveibeenpwned.com/API/v3" class="font-medium text-blue-600 hover:underline dark:text-blue-500">Have I Been Pwned? API</a>.</p>
|
||||
<input name="hibpApiKey" type="text"
|
||||
placeholder="HIBP API key" {{if $hibpK}}value="{{$hibpK}}"{{end}}
|
||||
autofocus
|
||||
disabled
|
||||
class="py-3 px-8 mt-2 w-full valid:border text-gray-500 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<p id="hibp-expl"
|
||||
class="group relative hover:cursor-help mt-2 text-sm text-gray-500 dark:text-gray-400">
|
||||
Adds support for querying Troy Hunt's
|
||||
<a href="https://haveibeenpwned.com/API/v3"
|
||||
class="font-medium text-blue-600 hover:underline dark:text-blue-500">Have I Been Pwned? API
|
||||
</a>
|
||||
<span
|
||||
class="absolute hidden group-hover:flex -left-5 -top-2 -translate-y-full w-48 px-2 py-1 bg-gray-700 rounded-lg text-center text-white text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700">
|
||||
The input field reflects the value of $PCMT_HIBP_API_KEY environment variable.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 block md:flex md:items-center place-items-end space-x-0 md:space-x-2">
|
||||
<button class="px-2 py-3 w-full md:w-auto text-sm font-medium tracking-wide text-white transition-colors duration-300 transform bg-blue-500 rounded-lg hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50">
|
||||
<div class="group relative mt-4 block md:flex md:items-center place-items-end space-x-0 md:space-x-2">
|
||||
<span
|
||||
class="absolute hidden group-hover:flex -left-5 -top-2 -translate-y-full w-48 px-2 py-1 bg-gray-700 rounded-lg text-center text-white text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700">
|
||||
These buttons currently don't perform any actions. The key view is currently read-only.
|
||||
</span>
|
||||
<button
|
||||
disabled
|
||||
class="px-2 py-3 w-full md:w-auto text-sm font-medium tracking-wide text-white transition-colors duration-300 transform bg-blue-500 rounded-lg hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50">
|
||||
Save
|
||||
</button>
|
||||
<button {{if and (not .Data) (not .Data.hibpApiKey)}}disabled class="cursor-not-allowed{{else}}class="transition-colors duration-300 transform hover:bg-gray-400 focus:outline-none focus:ring focus:ring-gray-300 focus:ring-opacity-50{{end}} px-2 py-3 mt-2 md:mt-0 w-full md:w-auto text-sm font-medium tracking-wide text-white bg-gray-500 rounded-lg">
|
||||
<button
|
||||
disabled {{if not $hibpK}}{{end}}
|
||||
class="{{if not $hibpK}}cursor-not-allowed{{else}}transition-colors duration-300 transform hover:bg-gray-400 focus:outline-none focus:ring focus:ring-gray-300 focus:ring-opacity-50{{end}} px-2 py-3 mt-2 md:mt-0 w-full md:w-auto text-sm font-medium tracking-wide text-white bg-gray-500 rounded-lg">
|
||||
Test key
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
<hr class="py-2 md:px-14 mt-4 lg:mx-22 sm:max-w-1/2">
|
||||
|
||||
{{- $dehashedK := .Data.dehashedApiKey -}}
|
||||
<!-- dehashed.com form -->
|
||||
<form method="post" class="w-full lg:max-w-7xl px-2">
|
||||
<input type="hidden" name="csrf" value="{{- .CSRF -}}">
|
||||
|
@ -42,14 +67,32 @@
|
|||
<label for="dehashed-api-key" class="text-purple-500 font-bold">
|
||||
DeHashed.com
|
||||
</label>
|
||||
<input name="dehashed-api-key" type="text" placeholder="DeHashed.com API key" {{if and .Data.form .Data.form.DehashedAPIKey}}value="{{.Data.form.DehashedAPIKey}}"{{end}} autofocus class="py-3 px-8 mt-2 w-full valid:border text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40" aria-described-by="dehashed-expl">
|
||||
<p id="dehashed-expl" class="hover:cursor-help mt-2 text-sm text-gray-500 dark:text-gray-400">Adds support for querying <a href="https://dehashed.com/" class="font-medium text-blue-600 hover:underline dark:text-blue-500">DeHashed API</a>.</p>
|
||||
<input name="dehashed-api-key" type="text"
|
||||
placeholder="DeHashed.com API key" {{if $dehashedK}}value="{{$dehashedK}}"{{end}}
|
||||
autofocus
|
||||
disabled
|
||||
class="py-3 px-8 mt-2 w-full valid:border text-gray-500 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40"
|
||||
aria-described-by="dehashed-expl">
|
||||
<p id="dehashed-expl" class="group relative hover:cursor-help mt-2 text-sm text-gray-500 dark:text-gray-400">Adds support for querying <a href="https://dehashed.com/" class="font-medium text-blue-600 hover:underline dark:text-blue-500">DeHashed API</a>
|
||||
<span
|
||||
class="absolute hidden group-hover:flex -left-5 -top-2 -translate-y-full w-54 px-2 py-1 bg-gray-700 rounded-lg text-center text-white text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700">
|
||||
Support for dehashed.com is coming up. The input field will reflect the value of $PCMT_DEHASHED_API_KEY environment variable.
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="mt-4 block md:flex md:items-center place-items-end space-x-0 md:space-x-2">
|
||||
<button class="px-2 py-3 w-full md:w-auto text-sm font-medium tracking-wide text-white transition-colors duration-300 transform bg-blue-500 rounded-lg hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50">
|
||||
<div class="group relative mt-4 block md:flex md:items-center place-items-end space-x-0 md:space-x-2">
|
||||
<span
|
||||
class="absolute hidden group-hover:flex -left-5 -top-2 -translate-y-full w-48 px-2 py-1 bg-gray-700 rounded-lg text-center text-white text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700">
|
||||
These buttons currently don't perform any actions.
|
||||
</span>
|
||||
<button
|
||||
disabled
|
||||
class="px-2 py-3 w-full md:w-auto text-sm font-medium tracking-wide text-white transition-colors duration-300 transform bg-blue-500 rounded-lg hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50">
|
||||
Save
|
||||
</button>
|
||||
<button {{if and (not .Data.form) (not .Data.form.DehashedAPIKey)}}disabled class="cursor-not-allowed{{else}}class="transition-colors duration-300 transform hover:bg-gray-400 focus:outline-none focus:ring focus:ring-gray-300 focus:ring-opacity-50{{end}} px-2 py-3 mt-2 md:mt-0 w-full md:w-auto text-sm font-medium tracking-wide text-white bg-gray-500 rounded-lg">
|
||||
<button
|
||||
disabled {{if not $dehashedK}}{{end}}
|
||||
class="{{if not $dehashedK}}cursor-not-allowed{{else}}transition-colors duration-300 transform hover:bg-gray-400 focus:outline-none focus:ring focus:ring-gray-300 focus:ring-opacity-50{{end}} px-2 py-3 mt-2 md:mt-0 w-full md:w-auto text-sm font-medium tracking-wide text-white bg-gray-500 rounded-lg">
|
||||
Test key
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -28,55 +28,55 @@
|
|||
<div class="p-2 mt-3 lg:mx-auto border-2 dark:border-slate-500 rounded-sm space-y-0">
|
||||
<div class="flex max-h-14 place-items-baseline justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">ID:</span>
|
||||
<h3 class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2 overflow-x-auto text-ellipsis select-all">
|
||||
<span class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2 overflow-x-auto text-ellipsis select-all">
|
||||
{{- .Data.user.ID -}}
|
||||
</h3>
|
||||
</span>
|
||||
</div><!-- id -->
|
||||
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Username:</span>
|
||||
<h3 class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||
<span class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||
{{- .Data.user.Username -}}
|
||||
</h3>
|
||||
</span>
|
||||
</div><!-- username -->
|
||||
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Email:</span>
|
||||
<h3 class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||
<span class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||
{{- .Data.user.Email -}}
|
||||
</h3>
|
||||
</span>
|
||||
</div><!-- email -->
|
||||
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Admin:</span>
|
||||
<h3 class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||
<span class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||
{{- .Data.user.IsAdmin -}}
|
||||
</h3>
|
||||
</span>
|
||||
</div><!-- isAdmin -->
|
||||
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Active:</span>
|
||||
<h3 class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||
<span class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||
{{- .Data.user.IsActive -}}
|
||||
</h3>
|
||||
</span>
|
||||
</div><!-- isActive -->
|
||||
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Last login:</span>
|
||||
<h3 class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||
<span class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||
{{ if usrFinishedSetup .Data.user.LastLogin }}
|
||||
{{- .Data.user.LastLogin -}}
|
||||
{{- else -}}
|
||||
never
|
||||
{{- end -}}
|
||||
</h3>
|
||||
</span>
|
||||
</div><!-- updated -->
|
||||
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Created:</span>
|
||||
<h3 class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||
<span class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||
{{- .Data.user.CreatedAt -}}
|
||||
</h3>
|
||||
</span>
|
||||
</div><!-- created -->
|
||||
<div class="flex place-items-center justify-left lg:justify-between overflow-x-auto text-ellipsis hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||
<span class="w-24 md:w-32 px-2 text-purple-500 dark:text-purple-300">Updated:</span>
|
||||
<h3 class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||
<span class="text-lg text-fuchsia-500 dark:text-fuchsia-400 px-2">
|
||||
{{- .Data.user.UpdatedAt -}}
|
||||
</h3>
|
||||
</span>
|
||||
</div><!-- updated -->
|
||||
</div>
|
||||
{{- end -}}
|
||||
|
|
|
@ -32,13 +32,19 @@
|
|||
<span class="absolute" role="img" aria-label="person outline icon for username">
|
||||
{{ template "svg-user.tmpl" }}
|
||||
</span>
|
||||
<input name="username" type="text" {{if and .Data.user .Data.user.Username}}value="{{.Data.user.Username}}"{{end}} placeholder="Username" required class="block w-full py-3 valid:border-green-300 required:border-blue-300 text-gray-700 bg-white border rounded-lg px-11 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<input name="username" type="text"
|
||||
{{if and .Data.user .Data.user.Username}}value="{{.Data.user.Username}}" {{end}}placeholder="Username"
|
||||
required
|
||||
class="block w-full py-3 valid:border-green-300 required:border-blue-300 text-gray-700 bg-white border rounded-lg px-11 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
</div>
|
||||
<div class="relative flex items-center mt-4">
|
||||
<span class="absolute" role="img" aria-label="email icon">
|
||||
{{ template "svg-email.tmpl" }}
|
||||
</span>
|
||||
<input name="email" type="email" {{if and .Data.user .Data.user.Email}}value="{{.Data.user.Email}}"{{end}} placeholder="Email address" required class="peer block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<input name="email" type="email"
|
||||
{{if and .Data.user .Data.user.Email}}value="{{.Data.user.Email}}" {{end}}placeholder="Email address"
|
||||
required
|
||||
class="peer block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<p class="mt-2 mx-4 hidden peer-[:not(:placeholder-shown):invalid]:block text-pink-600 text-sm">
|
||||
Please provide a valid email address.
|
||||
</p>
|
||||
|
@ -48,13 +54,19 @@
|
|||
<span class="absolute" role="img" aria-label="password lock icon">
|
||||
{{ template "svg-password.tmpl" }}
|
||||
</span>
|
||||
<input name="password" type="password" value="" placeholder="Password" required class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<input name="password" type="password"
|
||||
value="" placeholder="Password"
|
||||
required
|
||||
class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
</div>
|
||||
<div class="relative flex items-center mt-4">
|
||||
<span class="absolute" role="img" aria-label="password lock icon">
|
||||
{{ template "svg-password.tmpl" }}
|
||||
</span>
|
||||
<input name="repeatPassword" type="password" value="" placeholder="Repeat Password" required class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<input name="repeatPassword" type="password"
|
||||
value="" placeholder="Repeat Password"
|
||||
required
|
||||
class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
</div>
|
||||
{{- end -}}
|
||||
<div class="flex pt-2 px-2 items-center justify-center gap-6">
|
||||
|
|
|
@ -22,13 +22,21 @@
|
|||
<span class="absolute" role="img" aria-label="person outline icon for username">
|
||||
{{ template "svg-user.tmpl" }}
|
||||
</span>
|
||||
<input name="username" type="text" {{if and .Data.form .Data.form.Username}}value="{{.Data.form.Username}}"{{end}} placeholder="Username" required class="block w-full py-3 valid:border-green-300 required:border-blue-300 text-gray-700 bg-white border rounded-lg px-11 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<input name="username" type="text"
|
||||
placeholder="Username" {{if and .Data.form .Data.form.Username}}value="{{.Data.form.Username}}"{{end}}
|
||||
autofocus
|
||||
minlength="2"
|
||||
required
|
||||
class="block w-full py-3 valid:border required:border-blue-300 text-gray-700 bg-white border rounded-lg px-11 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
</div>
|
||||
<div class="relative flex items-center mt-4">
|
||||
<span class="absolute" role="img" aria-label="email icon">
|
||||
{{ template "svg-email.tmpl" }}
|
||||
</span>
|
||||
<input name="email" type="email" {{if and .Data.form .Data.form.Email}}value="{{.Data.form.Email}}"{{end}} placeholder="Email address" required class="peer block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<input name="email" type="email"
|
||||
placeholder="Email address" {{if and .Data.form .Data.form.Email}}value="{{.Data.form.Email}}"{{end}}
|
||||
required
|
||||
class="peer block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<p class="mt-2 mx-4 hidden peer-[:not(:placeholder-shown):invalid]:block text-pink-600 text-sm">
|
||||
Please provide a valid email address.
|
||||
</p>
|
||||
|
@ -37,13 +45,21 @@
|
|||
<span class="absolute" role="img" aria-label="password lock icon">
|
||||
{{ template "svg-password.tmpl" }}
|
||||
</span>
|
||||
<input name="password" type="password" {{if and .Data.form .Data.form.Password}}value="{{.Data.form.Password}}"{{end}} placeholder="Password" required class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<input name="password" type="password"
|
||||
placeholder="Password" {{if and .Data.form .Data.form.Password}}value="{{.Data.form.Password}}"{{end}}
|
||||
minlength="20"
|
||||
required
|
||||
class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
</div>
|
||||
<div class="relative flex items-center mt-4">
|
||||
<span class="absolute" role="img" aria-label="password lock icon">
|
||||
{{ template "svg-password.tmpl" }}
|
||||
</span>
|
||||
<input name="repeatPassword" type="password" {{if and .Data.form .Data.form.RepeatPassword}}value="{{.Data.form.RepeatPassword}}"{{end}} placeholder="Repeat Password" required class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<input name="repeatPassword" type="password"
|
||||
placeholder="Repeat Password" {{if and .Data.form .Data.form.RepeatPassword}}value="{{.Data.form.RepeatPassword}}"{{end}}
|
||||
minlength="20"
|
||||
required
|
||||
class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
</div>
|
||||
<div class="flex pt-2 px-2 items-center justify-center gap-6">
|
||||
<div class="mb-1 block min-h-3">
|
||||
|
@ -65,6 +81,7 @@
|
|||
value="true"
|
||||
id="isActive"
|
||||
name="isActive"
|
||||
checked
|
||||
/>
|
||||
<label
|
||||
class="inline-block pl-1 hover:cursor-pointer text-gray-700 dark:text-gray-300"
|
||||
|
|
|
@ -20,27 +20,52 @@
|
|||
<p class="mt-2 text-md text-rose-800 dark:text-rose-500"><span class="font-medium">Error:</span> {{.Data.flash}}</p>
|
||||
</div>
|
||||
{{- else -}}{{end}}
|
||||
{{ if and .Data .Data.reauthFlash }}
|
||||
<div class="relative flex items-center mb-4">
|
||||
<p class="mt-2 text-xl text-blue-500 dark:text-blue-400"><span class="italic font-medium">Success:</span> {{.Data.reauthFlash}}</p>
|
||||
</div>
|
||||
{{- else -}}{{end}}
|
||||
{{ if and .Data .Data.infoFlash }}
|
||||
<div class="relative flex items-center mb-4">
|
||||
<p class="mt-2 text-xl text-blue-500 dark:text-blue-400">
|
||||
{{.Data.infoFlash}}
|
||||
</p>
|
||||
</div>
|
||||
{{- else -}}{{end}}
|
||||
<!-- username field -->
|
||||
<div class="relative flex items-center">
|
||||
<span class="absolute" role="img" aria-label="person outline icon for username">
|
||||
{{ template "svg-user.tmpl" }}
|
||||
</span>
|
||||
<input name="username" type="text" placeholder="Username" {{if and .Data.form .Data.form.Username}}value="{{.Data.form.Username}}"{{end}} autofocus minlength="2" required class="block w-full py-3 required:border-slate-500 dark:required:border-slate-300 required:border-3 valid:border invalid:border-pink-600 focus:invalid:border-pink-600 focus:invalid:ring-pink-300 dark:invalid:border-pink-400 dark:focus:invalid:border-pink-400 dark:focus:invalid:ring-pink-250 text-gray-700 bg-white border rounded-lg px-11 dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<input name="username" type="text"
|
||||
placeholder="Username" {{if and .Data.form .Data.form.Username}}value="{{.Data.form.Username}}"{{end}}
|
||||
autofocus
|
||||
minlength="2"
|
||||
required
|
||||
class="block w-full py-3 required:border-slate-500 dark:required:border-slate-300 required:border-3 valid:border text-gray-700 bg-white border rounded-lg px-11 dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
</div>
|
||||
<!-- password field -->
|
||||
<div class="relative flex items-center mt-4">
|
||||
<span class="absolute" role="img" aria-label="password lock icon">
|
||||
{{ template "svg-password.tmpl" }}
|
||||
</span>
|
||||
<input name="password" type="password" placeholder="Password" {{if and .Data.form .Data.form.Password}}value="{{.Data.form.Password}}"{{else}}{{end}} minlength=12 required class="block w-full px-10 py-3 required:border-slate-500 dark:required:border-slate-300 required:border-3 valid:border invalid:border-pink-600 focus:invalid:border-pink-600 focus:invalid:ring-pink-300 dark:invalid:border-pink-400 dark:focus:invalid:border-pink-400 dark:focus:invalid:ring-pink-250 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<input name="password" type="password"
|
||||
placeholder="Password" {{if and .Data.form .Data.form.Password}}value="{{.Data.form.Password}}"{{else}}{{end}}
|
||||
minlength=12
|
||||
required
|
||||
class="block w-full px-10 py-3 required:border-slate-500 dark:required:border-slate-300 required:border-3 valid:border text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
</div>
|
||||
|
||||
<div class="mt-8 md:flex md:items-center">
|
||||
<button class="w-full px-6 py-3 text-sm font-medium tracking-wide text-white capitalize transition-colors duration-300 transform bg-blue-500 rounded-lg md:w-1/2 hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50">
|
||||
Sign in
|
||||
</button>
|
||||
<a href="#" class="inline-block mt-4 text-center text-blue-500 md:mt-0 md:mx-6 lg:mx-auto hover:underline dark:text-blue-400">
|
||||
Forgot your password?
|
||||
<a href="#" class="group relative inline-block mt-4 text-center text-blue-500 md:mt-0 md:mx-6 lg:mx-auto hover:underline dark:text-blue-400">
|
||||
Forgot your password?
|
||||
<span
|
||||
class="absolute hidden group-hover:flex -left-5 -top-2 -translate-y-full w-48 px-2 py-1 bg-gray-700 rounded-lg text-center text-white text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700">
|
||||
This is currently a no-op
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div class="mt-4 md:flex md:items-center">
|
||||
|
@ -54,17 +79,6 @@
|
|||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-20">
|
||||
<a href="#" class="group relative inline-block text-blue-500 underline hover:text-red-500 duration-300">
|
||||
Link with top tooltip
|
||||
<!-- Tooltip text here -->
|
||||
<span
|
||||
class="absolute hidden group-hover:flex -left-5 -top-2 -translate-y-full w-48 px-2 py-1 bg-gray-700 rounded-lg text-center text-white text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700">This
|
||||
is some extra useful information</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
|
|
@ -18,14 +18,21 @@
|
|||
<span class="absolute" role="img" aria-label="person outline icon for username">
|
||||
{{ template "svg-user.tmpl" }}
|
||||
</span>
|
||||
<input name="username" type="text" placeholder="Username" required class="block w-full py-3 valid:border-green-300 required:border-blue-300 text-gray-700 bg-white border rounded-lg px-11 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<input name="username" type="text"
|
||||
placeholder="Username"
|
||||
minlength="2"
|
||||
required
|
||||
class="block w-full py-3 valid:border-green-300 required:border-blue-300 text-gray-700 bg-white border rounded-lg px-11 dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
</div>
|
||||
<div class="relative flex items-center mt-4">
|
||||
<!-- <label class="block"> -->
|
||||
<span class="absolute" role="img" aria-label="email icon">
|
||||
{{ template "svg-email.tmpl" }}
|
||||
</span>
|
||||
<input name="email" type="email" placeholder="Email address" required class="peer block w-full px-10 py-3 valid:border-green-300 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<input name="email" type="email"
|
||||
placeholder="Email address"
|
||||
required
|
||||
class="peer block w-full px-10 py-3 valid:border-green-300 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<p class="mt-2 mx-4 hidden peer-[:not(:placeholder-shown):invalid]:block text-pink-600 text-sm">
|
||||
Please provide a valid email address.
|
||||
</p>
|
||||
|
@ -35,7 +42,11 @@
|
|||
<span class="absolute" role="img" aria-label="password lock icon">
|
||||
{{ template "svg-password.tmpl" }}
|
||||
</span>
|
||||
<input name="password" type="password" placeholder="Password" required class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<input name="password" type="password"
|
||||
placeholder="Password"
|
||||
required
|
||||
minlength="20"
|
||||
class="block w-full px-10 py-3 required:border-blue-300 text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
</div>
|
||||
<div class="mt-8 md:flex md:items-center">
|
||||
<button class="w-full px-6 py-3 text-sm font-medium tracking-wide text-white capitalize transition-colors duration-300 transform bg-blue-500 rounded-lg md:w-1/2 hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50">
|
||||
|
|
|
@ -10,20 +10,48 @@
|
|||
{{- end }}
|
||||
{{ if .User -}}
|
||||
<h1 class="mt-20 text-2xl text-pink-400 font-bold">
|
||||
Welcome, <code>{{.Name}}</code>, since you have never changed your password here before, now is the time to do that!<br>
|
||||
Welcome,
|
||||
<code class="p-1 rounded-sm bg-fuchsia-200">
|
||||
{{.Name}}</code>!
|
||||
</h1>
|
||||
<h3 class="mt-2 mb-4 text-2xl text-purple-500 dark:text-purple-300 font-bold">
|
||||
Since you have never changed your password here before, now is the time to do that!
|
||||
</h3>
|
||||
<span class="text-lg text-gray-500 dark:text-gray-300 font-italic">
|
||||
This is necessary in order to make sure that only you know the password, since the account was pre-created <em>for</em> you.
|
||||
This is necessary in order to make sure that <b>only you</b> know the
|
||||
password, as the account was pre-created <em>for</em> you.
|
||||
</span>
|
||||
<div class="block">
|
||||
<div class="mt-8 md:flex md:items-center md:place-items-center md:justify-between">
|
||||
<div class="mt-6 md:flex md:items-center md:place-items-center md:justify-between">
|
||||
<form method="POST" class="w-full md:w-5xl" action="/user/initial-password-change">
|
||||
<input type="hidden" name="csrf" value="{{- .CSRF -}}">
|
||||
<div class="relative flex items-center mt-4">
|
||||
<label class="group relative mt-2 font-bold text-fuchsia-500 dark:text-fuchsia-300 text-sm">
|
||||
Your new password 🛈
|
||||
<span
|
||||
class="absolute hidden group-hover:flex -left-5 -top-2 -translate-y-full w-48 px-2 py-1 bg-gray-700 rounded-lg text-center text-white text-sm after:content-[''] after:absolute after:left-1/2 after:top-[100%] after:-translate-x-1/2 after:border-8 after:border-x-transparent after:border-b-transparent after:border-t-gray-700">
|
||||
Please, save this password into a password manager because after clicking
|
||||
submit you will be logged out and will need to re-login again.
|
||||
</span>
|
||||
</label>
|
||||
<div class="relative flex items-center">
|
||||
<span class="absolute" role="img" aria-label="password lock icon">
|
||||
{{ template "svg-password.tmpl" }}
|
||||
</span>
|
||||
<input name="password" type="new-password" placeholder="New password" {{if and .Data.form .Data.form.Password}}value="{{.Data.form.Password}}"{{else}}{{end}} minlength=20 required class="block w-full px-10 py-3 required:border-slate-500 dark:required:border-slate-300 required:border-3 valid:border text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
<input name="new-password" type="password"
|
||||
placeholder="New password" {{if and .Data.form .Data.form.NewPassword}}value="{{.Data.form.NewPassword}}"{{else}}{{end}}
|
||||
minlength=20
|
||||
required
|
||||
class="block w-full px-10 py-3 required:border-slate-500 dark:required:border-slate-300 required:border-3 valid:border text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
</div>
|
||||
<div class="relative flex items-center">
|
||||
<span class="absolute" role="img" aria-label="password lock icon">
|
||||
{{ template "svg-password.tmpl" }}
|
||||
</span>
|
||||
<input name="repeat-new-password" type="password"
|
||||
placeholder="New password repeated" {{if and .Data.form .Data.form.RepeatNewPassword}}value="{{.Data.form.RepeatNewPassword}}"{{else}}{{end}}
|
||||
minlength=20
|
||||
required
|
||||
class="block w-full px-10 py-3 required:border-slate-500 dark:required:border-slate-300 required:border-3 valid:border text-gray-700 bg-white border rounded-lg dark:bg-gray-900 dark:text-gray-300 dark:valid:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:ring-blue-300 focus:outline-none focus:ring focus:ring-opacity-40">
|
||||
</div>
|
||||
<button
|
||||
class="w-full my-2 text-sm font-medium tracking-wide text-white capitalize transition-colors duration-300 transform bg-blue-500 rounded-lg md:w-1/4 hover:bg-blue-400 focus:outline-none focus:ring focus:ring-blue-300 focus:ring-opacity-50"
|
||||
|
|
Loading…
Reference in New Issue