Compare commits
5 Commits
Author | SHA1 | Date | |
---|---|---|---|
46ef5bd493 | |||
b0d037cca9 | |||
0a9bdb56ad | |||
9ffd6c0747 | |||
8381b9226e |
@ -3,7 +3,7 @@
|
|||||||
# This source code is licensed under the MIT license found in the
|
# This source code is licensed under the MIT license found in the
|
||||||
# LICENSE file in the root directory of this source tree.
|
# LICENSE file in the root directory of this source tree.
|
||||||
|
|
||||||
FROM golang:1.12-alpine AS build
|
FROM golang:1.13-alpine AS build
|
||||||
|
|
||||||
ARG VERSION
|
ARG VERSION
|
||||||
ARG GOPROXY
|
ARG GOPROXY
|
||||||
|
105
README.md
105
README.md
@ -166,13 +166,96 @@ After that you should set the directory path to the environment variable `WERTHE
|
|||||||
|
|
||||||
### Custom login page
|
### Custom login page
|
||||||
|
|
||||||
|
A login page's template must be a Go template. The template has access to data conforming the next JSON-schema:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
- WebBasePath:
|
||||||
|
description: The base path of the login page
|
||||||
|
type: string
|
||||||
|
- LangPrefs:
|
||||||
|
description: The user language preferences (the parsed value of the header Accept-Language)
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
- Lang:
|
||||||
|
description: The language canonical name.
|
||||||
|
type: string
|
||||||
|
- Weight:
|
||||||
|
description: The language weight.
|
||||||
|
type: number
|
||||||
|
required:
|
||||||
|
- Lang
|
||||||
|
- Weight
|
||||||
|
- Data:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
- CSRFToken:
|
||||||
|
description: A CSRF token.
|
||||||
|
type: string
|
||||||
|
- Challenge:
|
||||||
|
description: A login challenge ID.
|
||||||
|
type: string
|
||||||
|
- LoginURL:
|
||||||
|
description: An endpoint that finishes the login process.
|
||||||
|
type: string
|
||||||
|
- IsInvalidCredentials:
|
||||||
|
description: Specifies that a user types an invalid username or password.
|
||||||
|
type: boolean
|
||||||
|
- IsInternalError:
|
||||||
|
description: Specifies that an internal server error happens when finishing the login process.
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- CSRFToken
|
||||||
|
- Challenge
|
||||||
|
- LoginURL
|
||||||
|
- IsInvalidCredentials
|
||||||
|
- IsInternalError
|
||||||
|
required:
|
||||||
|
- WebBasePath
|
||||||
|
- LangPrefs
|
||||||
|
- Data
|
||||||
|
```
|
||||||
|
|
||||||
|
When a login page's template contains static resources (like styles, scripts, and images)
|
||||||
|
they must be placed in a subdirectory called `static`.
|
||||||
|
|
||||||
|
For a full example of a login page's template see [source code](internal/web/templates).
|
||||||
|
|
||||||
|
### Custom login page (old format)
|
||||||
|
|
||||||
|
*The old template format is also supported but it will be removed in the future major release.*
|
||||||
|
|
||||||
A login page's template should contains blocks `title`, `style`, `script`, `content`.
|
A login page's template should contains blocks `title`, `style`, `script`, `content`.
|
||||||
Each block has access to data that is an object with the next properties:
|
Each block has access to data conforming the next JSON-schema:
|
||||||
- `CSRFToken` (string) - a CSRF token;
|
|
||||||
- `Challenge` (string) - a login challenge ID;
|
```yaml
|
||||||
- `LoginURL` (string) - an endpoint that finishes the login process;
|
type: object
|
||||||
- `IsInvalidCredentials` (bool) - specifies that a user types an invalid username or password;
|
properties:
|
||||||
- `IsInternalError` (bool) specifies that an internal server error happens when finishing the login process.
|
- CSRFToken:
|
||||||
|
description: A CSRF token.
|
||||||
|
type: string
|
||||||
|
- Challenge:
|
||||||
|
description: A login challenge ID.
|
||||||
|
type: string
|
||||||
|
- LoginURL:
|
||||||
|
description: An endpoint that finishes the login process.
|
||||||
|
type: string
|
||||||
|
- IsInvalidCredentials:
|
||||||
|
description: Specifies that a user types an invalid username or password.
|
||||||
|
type: boolean
|
||||||
|
- IsInternalError:
|
||||||
|
description: Specifies that an internal server error happens when finishing the login process.
|
||||||
|
type: boolean
|
||||||
|
required:
|
||||||
|
- CSRFToken
|
||||||
|
- Challenge
|
||||||
|
- LoginURL
|
||||||
|
- IsInvalidCredentials
|
||||||
|
- IsInternalError
|
||||||
|
```
|
||||||
|
|
||||||
When a login page's template contains static resources (like styles, scripts, and images)
|
When a login page's template contains static resources (like styles, scripts, and images)
|
||||||
they must be placed in a subdirectory called `static`.
|
they must be placed in a subdirectory called `static`.
|
||||||
@ -230,7 +313,7 @@ For a full example of a login page's template see [source code](internal/web/tem
|
|||||||
- --grant-types
|
- --grant-types
|
||||||
- implicit
|
- implicit
|
||||||
- --scope
|
- --scope
|
||||||
- openid,profile,email
|
- openid,profile,email,roles
|
||||||
- --callbacks
|
- --callbacks
|
||||||
- http://localhost:3000
|
- http://localhost:3000
|
||||||
- --post-logout-callbacks
|
- --post-logout-callbacks
|
||||||
@ -255,8 +338,8 @@ For a full example of a login page's template see [source code](internal/web/tem
|
|||||||
URLS_LOGIN: http://localhost:8080/auth/login
|
URLS_LOGIN: http://localhost:8080/auth/login
|
||||||
URLS_CONSENT: http://localhost:8080/auth/consent
|
URLS_CONSENT: http://localhost:8080/auth/consent
|
||||||
URLS_LOGOUT: http://localhost:8080/auth/logout
|
URLS_LOGOUT: http://localhost:8080/auth/logout
|
||||||
WEBFINGER_OIDC_DISCOVERY_SUPPORTED_SCOPES: profile,email,phone
|
WEBFINGER_OIDC_DISCOVERY_SUPPORTED_SCOPES: profile,email,phone,roles
|
||||||
WEBFINGER_OIDC_DISCOVERY_SUPPORTED_CLAIMS: name,family_name,given_name,nickname,email,phone_number
|
WEBFINGER_OIDC_DISCOVERY_SUPPORTED_CLAIMS: name,family_name,given_name,nickname,email,phone_number,https://github.com/i-core/werther/claims/roles
|
||||||
DSN: memory
|
DSN: memory
|
||||||
command: serve all --dangerous-force-http
|
command: serve all --dangerous-force-http
|
||||||
networks:
|
networks:
|
||||||
@ -270,7 +353,7 @@ For a full example of a login page's template see [source code](internal/web/tem
|
|||||||
depends_on:
|
depends_on:
|
||||||
- werther
|
- werther
|
||||||
werther:
|
werther:
|
||||||
image: icoreru/werther:v1.0.0
|
image: icoreru/werther:v1.1.1
|
||||||
environment:
|
environment:
|
||||||
WERTHER_IDENTP_HYDRA_URL: http://hydra:4445
|
WERTHER_IDENTP_HYDRA_URL: http://hydra:4445
|
||||||
WERTHER_LDAP_ENDPOINTS: ldap:389
|
WERTHER_LDAP_ENDPOINTS: ldap:389
|
||||||
@ -307,7 +390,7 @@ For a full example of a login page's template see [source code](internal/web/tem
|
|||||||
docker stack deploy -c docker-compose.yml auth
|
docker stack deploy -c docker-compose.yml auth
|
||||||
```
|
```
|
||||||
|
|
||||||
4. Open the browser with http://localhost:4444/oauth2/auth?client_id=test-client&response_type=token&scope=openid%20profile%20email&state=12345678.
|
4. Open the browser with http://localhost:4444/oauth2/auth?client_id=test-client&response_type=token&scope=openid%20profile%20email%20roles&state=12345678.
|
||||||
|
|
||||||
## Resources
|
## Resources
|
||||||
|
|
||||||
|
4
go.mod
4
go.mod
@ -7,6 +7,7 @@ require (
|
|||||||
github.com/coocood/freecache v1.0.1
|
github.com/coocood/freecache v1.0.1
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/elazarl/go-bindata-assetfs v1.0.0
|
github.com/elazarl/go-bindata-assetfs v1.0.0
|
||||||
|
github.com/go-ldap/ldap/v3 v3.2.3
|
||||||
github.com/i-core/rlog v1.0.0
|
github.com/i-core/rlog v1.0.0
|
||||||
github.com/i-core/routegroup v1.0.0
|
github.com/i-core/routegroup v1.0.0
|
||||||
github.com/justinas/nosurf v0.0.0-20171023064657-7182011986c4
|
github.com/justinas/nosurf v0.0.0-20171023064657-7182011986c4
|
||||||
@ -16,8 +17,7 @@ require (
|
|||||||
github.com/sergi/go-diff v1.0.0 // indirect
|
github.com/sergi/go-diff v1.0.0 // indirect
|
||||||
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 // indirect
|
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 // indirect
|
||||||
go.uber.org/zap v1.10.0
|
go.uber.org/zap v1.10.0
|
||||||
gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225 // indirect
|
golang.org/x/text v0.3.2
|
||||||
gopkg.in/ldap.v2 v2.5.1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
go 1.13
|
go 1.13
|
||||||
|
20
go.sum
20
go.sum
@ -1,3 +1,5 @@
|
|||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c h1:/IBSNwUN8+eKzUzbJPqhK839ygXJ82sde8x3ogr6R28=
|
||||||
|
github.com/Azure/go-ntlmssp v0.0.0-20200615164410-66371956d46c/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||||
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
|
||||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||||
@ -11,6 +13,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
|
github.com/elazarl/go-bindata-assetfs v1.0.0 h1:G/bYguwHIzWq9ZoyUQqrjTmJbbYn3j3CKKpKinvZLFk=
|
||||||
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
github.com/elazarl/go-bindata-assetfs v1.0.0/go.mod h1:v+YaWX3bdea5J/mo8dSETolEo7R71Vk1u8bnjau5yw4=
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.1 h1:pDbRAunXzIUXfx4CB2QJFv5IuPiuoW+sWvr/Us009o8=
|
||||||
|
github.com/go-asn1-ber/asn1-ber v1.5.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.2.3 h1:FBt+5w3q/vPVPb4eYMQSn+pOiz4zewPamYhlGMmc7yM=
|
||||||
|
github.com/go-ldap/ldap/v3 v3.2.3/go.mod h1:iYS1MdmrmceOJ1QOTnRXrIs7i3kloqtmGQjRvjKpyMg=
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
github.com/gofrs/uuid v3.2.0+incompatible h1:y12jRkkFxsd7GpqdSZ+/KCs/fJbqpEXSGd4+jfEaewE=
|
||||||
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
|
||||||
github.com/i-core/rlog v1.0.0 h1:8CY2rsqvm3Z9cfl3hroppn8LTBwbtL45+ho79JTz8Jg=
|
github.com/i-core/rlog v1.0.0 h1:8CY2rsqvm3Z9cfl3hroppn8LTBwbtL45+ho79JTz8Jg=
|
||||||
@ -44,7 +50,13 @@ go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
|||||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||||
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
|
go.uber.org/zap v1.10.0 h1:ORx85nbTijNz8ljznvCMR1ZBIPKFn3jQrag10X2AsuM=
|
||||||
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||||
gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225 h1:JBwmEvLfCqgPcIq8MjVMQxsF3LVL4XG/HH0qiG0+IFY=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
gopkg.in/asn1-ber.v1 v1.0.0-20170511165959-379148ca0225/go.mod h1:cuepJuh7vyXfUyUwEgHQXw849cJrilpS5NeIjOWESAw=
|
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9 h1:vEg9joUBmeBcK9iSJftGNf3coIG4HqZElCPehJsfAYM=
|
||||||
gopkg.in/ldap.v2 v2.5.1 h1:wiu0okdNfjlBzg6UWvd1Hn8Y+Ux17/u/4nlk4CQr6tU=
|
golang.org/x/crypto v0.0.0-20200604202706-70a84ac30bf9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
gopkg.in/ldap.v2 v2.5.1/go.mod h1:oI0cpe/D7HRtBQl8aTg+ZmzFUAvu4lsv3eLXMLGFxWk=
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||||
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
@ -52,7 +52,7 @@ type oidcClaimsFinder interface {
|
|||||||
|
|
||||||
// TemplateRenderer renders a template with data and writes it to a http.ResponseWriter.
|
// TemplateRenderer renders a template with data and writes it to a http.ResponseWriter.
|
||||||
type TemplateRenderer interface {
|
type TemplateRenderer interface {
|
||||||
RenderTemplate(w http.ResponseWriter, name string, data interface{}) error
|
RenderTemplate(w http.ResponseWriter, r *http.Request, name string, data interface{}) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoginTmplData is a data that is needed for rendering the login page.
|
// LoginTmplData is a data that is needed for rendering the login page.
|
||||||
@ -147,7 +147,7 @@ func newLoginStartHandler(rproc oa2LoginReqProcessor, tmplRenderer TemplateRende
|
|||||||
Challenge: challenge,
|
Challenge: challenge,
|
||||||
LoginURL: strings.TrimPrefix(r.URL.String(), "/"),
|
LoginURL: strings.TrimPrefix(r.URL.String(), "/"),
|
||||||
}
|
}
|
||||||
if err := tmplRenderer.RenderTemplate(w, loginTmplName, data); err != nil {
|
if err := tmplRenderer.RenderTemplate(w, r, loginTmplName, data); err != nil {
|
||||||
log.Infow("Failed to render a login page template", zap.Error(err))
|
log.Infow("Failed to render a login page template", zap.Error(err))
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
@ -180,7 +180,7 @@ func newLoginEndHandler(ra oa2LoginReqAcceptor, auther authenticator, tmplRender
|
|||||||
data.IsInternalError = true
|
data.IsInternalError = true
|
||||||
log.Infow("Failed to authenticate a login request via the OAuth2 provider",
|
log.Infow("Failed to authenticate a login request via the OAuth2 provider",
|
||||||
zap.Error(err), "challenge", challenge, "username", username)
|
zap.Error(err), "challenge", challenge, "username", username)
|
||||||
if err = tmplRenderer.RenderTemplate(w, loginTmplName, data); err != nil {
|
if err = tmplRenderer.RenderTemplate(w, r, loginTmplName, data); err != nil {
|
||||||
log.Infow("Failed to render a login page template", zap.Error(err))
|
log.Infow("Failed to render a login page template", zap.Error(err))
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@ -188,7 +188,7 @@ func newLoginEndHandler(ra oa2LoginReqAcceptor, auther authenticator, tmplRender
|
|||||||
case !ok:
|
case !ok:
|
||||||
data.IsInvalidCredentials = true
|
data.IsInvalidCredentials = true
|
||||||
log.Debugw("Invalid credentials", zap.Error(err), "challenge", challenge, "username", username)
|
log.Debugw("Invalid credentials", zap.Error(err), "challenge", challenge, "username", username)
|
||||||
if err = tmplRenderer.RenderTemplate(w, loginTmplName, data); err != nil {
|
if err = tmplRenderer.RenderTemplate(w, r, loginTmplName, data); err != nil {
|
||||||
log.Infow("Failed to render a login page template", zap.Error(err))
|
log.Infow("Failed to render a login page template", zap.Error(err))
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@ -201,7 +201,7 @@ func newLoginEndHandler(ra oa2LoginReqAcceptor, auther authenticator, tmplRender
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
data.IsInternalError = true
|
data.IsInternalError = true
|
||||||
log.Infow("Failed to accept a login request via the OAuth2 provider", zap.Error(err))
|
log.Infow("Failed to accept a login request via the OAuth2 provider", zap.Error(err))
|
||||||
if err := tmplRenderer.RenderTemplate(w, loginTmplName, data); err != nil {
|
if err := tmplRenderer.RenderTemplate(w, r, loginTmplName, data); err != nil {
|
||||||
log.Infow("Failed to render a login page template", zap.Error(err))
|
log.Infow("Failed to render a login page template", zap.Error(err))
|
||||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
|
@ -98,7 +98,7 @@ func TestHandleLoginStart(t *testing.T) {
|
|||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
tmplRenderer := &testTemplateRenderer{
|
tmplRenderer := &testTemplateRenderer{
|
||||||
renderTmplFunc: func(w http.ResponseWriter, name string, data interface{}) error {
|
renderTmplFunc: func(w http.ResponseWriter, r *http.Request, name string, data interface{}) error {
|
||||||
if name != "login.tmpl" {
|
if name != "login.tmpl" {
|
||||||
t.Fatalf("wrong template name: got %q; want \"login.tmpl\"", name)
|
t.Fatalf("wrong template name: got %q; want \"login.tmpl\"", name)
|
||||||
}
|
}
|
||||||
@ -264,7 +264,7 @@ func TestHandleLoginEnd(t *testing.T) {
|
|||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
|
|
||||||
tmplRenderer := &testTemplateRenderer{
|
tmplRenderer := &testTemplateRenderer{
|
||||||
renderTmplFunc: func(w http.ResponseWriter, name string, data interface{}) error {
|
renderTmplFunc: func(w http.ResponseWriter, r *http.Request, name string, data interface{}) error {
|
||||||
if name != "login.tmpl" {
|
if name != "login.tmpl" {
|
||||||
t.Fatalf("wrong template name: got %q; want \"login.tmpl\"", name)
|
t.Fatalf("wrong template name: got %q; want \"login.tmpl\"", name)
|
||||||
}
|
}
|
||||||
@ -327,11 +327,11 @@ func TestHandleLoginEnd(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type testTemplateRenderer struct {
|
type testTemplateRenderer struct {
|
||||||
renderTmplFunc func(w http.ResponseWriter, name string, data interface{}) error
|
renderTmplFunc func(w http.ResponseWriter, r *http.Request, name string, data interface{}) error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (tl *testTemplateRenderer) RenderTemplate(w http.ResponseWriter, name string, data interface{}) error {
|
func (tl *testTemplateRenderer) RenderTemplate(w http.ResponseWriter, r *http.Request, name string, data interface{}) error {
|
||||||
return tl.renderTmplFunc(w, name, data)
|
return tl.renderTmplFunc(w, r, name, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
type testAuthenticator struct {
|
type testAuthenticator struct {
|
||||||
|
@ -18,10 +18,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/coocood/freecache"
|
"github.com/coocood/freecache"
|
||||||
|
"github.com/go-ldap/ldap/v3"
|
||||||
"github.com/i-core/rlog"
|
"github.com/i-core/rlog"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
ldap "gopkg.in/ldap.v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
File diff suppressed because one or more lines are too long
@ -1,45 +1,43 @@
|
|||||||
{{ define "title" }}
|
<!DOCTYPE html>
|
||||||
Login Provider Werther
|
<html lang="{{ (index .LangPrefs 0).Lang }}">
|
||||||
{{ end }}
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Login Provider Werther</title>
|
||||||
|
<base href="{{ .WebBasePath }}">
|
||||||
|
<link rel="stylesheet" href="static/style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-page">
|
||||||
|
<div class="form">
|
||||||
|
<p class="message">
|
||||||
|
{{ if .Data.IsInvalidCredentials }}
|
||||||
|
Invalid username or password
|
||||||
|
{{ else if .Data.IsInternalError }}
|
||||||
|
Internal server error
|
||||||
|
{{ else }}
|
||||||
|
|
||||||
|
{{ end }}
|
||||||
|
</p>
|
||||||
|
<form class="login-form" action="{{ .Data.LoginURL }}" method="POST">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ .Data.CSRFToken }}">
|
||||||
|
<input type="hidden" name="login_challenge" value="{{ .Data.Challenge }}">
|
||||||
|
|
||||||
{{ define "style" }}
|
<input type="text" placeholder="username" name="username"/>
|
||||||
<link rel="stylesheet" href="static/style.css">
|
<input type="password" placeholder="password" name="password"/>
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
{{ define "js" }}
|
<div class="checkbox remember-container">
|
||||||
<script type="text/javascript" src="static/script.js"></script>
|
<div class="checkbox-overlay">
|
||||||
{{ end }}
|
<input type="checkbox" name="remember" />
|
||||||
|
<div class="checkbox-container">
|
||||||
{{ define "content" }}
|
<div class="checkbox-checkmark"></div>
|
||||||
<div class="login-page">
|
</div>
|
||||||
<div class="form">
|
<label for="remember">Remember me</label>
|
||||||
<p class="message">
|
|
||||||
{{ if .IsInvalidCredentials }}
|
|
||||||
Invalid username or password
|
|
||||||
{{ else if .IsInternalError }}
|
|
||||||
Internal server error
|
|
||||||
{{ else }}
|
|
||||||
|
|
||||||
{{ end }}
|
|
||||||
</p>
|
|
||||||
<form class="login-form" action="{{ .LoginURL }}" method="POST">
|
|
||||||
<input type="hidden" name="csrf_token" value={{ .CSRFToken }}>
|
|
||||||
<input type="hidden" name="login_challenge" value={{ .Challenge }}>
|
|
||||||
|
|
||||||
<input type="text" placeholder="username" name="username"/>
|
|
||||||
<input type="password" placeholder="password" name="password"/>
|
|
||||||
|
|
||||||
<div class="checkbox remember-container">
|
|
||||||
<div class="checkbox-overlay">
|
|
||||||
<input type="checkbox" name="remember" />
|
|
||||||
<div class="checkbox-container">
|
|
||||||
<div class="checkbox-checkmark"></div>
|
|
||||||
</div>
|
</div>
|
||||||
<label for="remember">Remember me</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<button type="submit">login</button>
|
||||||
<button type="submit">login</button>
|
</form>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<script type="text/javascript" src="static/script.js"></script>
|
||||||
{{ end }}
|
</body>
|
||||||
|
</html>
|
||||||
|
BIN
internal/web/templates/static/fonts/Roboto-Light.ttf
Normal file
BIN
internal/web/templates/static/fonts/Roboto-Light.ttf
Normal file
Binary file not shown.
BIN
internal/web/templates/static/fonts/Roboto-Light.woff
Normal file
BIN
internal/web/templates/static/fonts/Roboto-Light.woff
Normal file
Binary file not shown.
BIN
internal/web/templates/static/fonts/Roboto-Light.woff2
Normal file
BIN
internal/web/templates/static/fonts/Roboto-Light.woff2
Normal file
Binary file not shown.
@ -1,4 +1,40 @@
|
|||||||
@import url(https://fonts.googleapis.com/css?family=Roboto:300);
|
/* cyrillic-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Roboto Light'), local('Roboto-Light'), url('./fonts/Roboto-Light.ttf') format('ttf'),
|
||||||
|
url('./fonts/Roboto-Light.woff') format('woff'), url('./fonts/Roboto-Light.woff2') format('woff2');
|
||||||
|
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||||
|
}
|
||||||
|
/* cyrillic */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Roboto Light'), local('Roboto-Light'), url('./fonts/Roboto-Light.ttf') format('ttf'),
|
||||||
|
url('./fonts/Roboto-Light.woff') format('woff'), url('./fonts/Roboto-Light.woff2') format('woff2');
|
||||||
|
unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||||
|
}
|
||||||
|
/* latin-ext */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Roboto Light'), local('Roboto-Light'), url('./fonts/Roboto-Light.ttf') format('ttf'),
|
||||||
|
url('./fonts/Roboto-Light.woff') format('woff'), url('./fonts/Roboto-Light.woff2') format('woff2');
|
||||||
|
unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;
|
||||||
|
}
|
||||||
|
/* latin */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Roboto';
|
||||||
|
font-style: normal;
|
||||||
|
font-weight: 400;
|
||||||
|
src: local('Roboto Light'), local('Roboto-Light'), url('./fonts/Roboto-Light.ttf') format('ttf'),
|
||||||
|
url('./fonts/Roboto-Light.woff') format('woff'), url('./fonts/Roboto-Light.woff2') format('woff2');
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC,
|
||||||
|
U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
html {
|
html {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@ -88,7 +124,7 @@ html {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.form button:active {
|
.form button:active {
|
||||||
background: #2384a3;
|
background: #2384a3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form .message {
|
.form .message {
|
||||||
|
@ -1,32 +1,10 @@
|
|||||||
external template
|
external template
|
||||||
WebBasePath: testBasePath;
|
WebBasePath: testBasePath;
|
||||||
|
|
||||||
Title:
|
Langs:
|
||||||
|
ru-RU;q=1,ru;q=0.9,en-US;q=0.8,en;q=0.7,
|
||||||
CSRFToken: testCSRFToken;
|
|
||||||
Challenge: testChalenge;
|
|
||||||
LoginURL: testLoginURL;
|
|
||||||
IsInvalidCredentials: true;
|
|
||||||
IsInternalError: true;
|
|
||||||
|
|
||||||
Style:
|
|
||||||
|
|
||||||
CSRFToken: testCSRFToken;
|
|
||||||
Challenge: testChalenge;
|
|
||||||
LoginURL: testLoginURL;
|
|
||||||
IsInvalidCredentials: true;
|
|
||||||
IsInternalError: true;
|
|
||||||
|
|
||||||
Js:
|
|
||||||
|
|
||||||
CSRFToken: testCSRFToken;
|
|
||||||
Challenge: testChalenge;
|
|
||||||
LoginURL: testLoginURL;
|
|
||||||
IsInvalidCredentials: true;
|
|
||||||
IsInternalError: true;
|
|
||||||
|
|
||||||
Content:
|
|
||||||
|
|
||||||
|
Data:
|
||||||
CSRFToken: testCSRFToken;
|
CSRFToken: testCSRFToken;
|
||||||
Challenge: testChalenge;
|
Challenge: testChalenge;
|
||||||
LoginURL: testLoginURL;
|
LoginURL: testLoginURL;
|
||||||
|
@ -1,31 +1,13 @@
|
|||||||
{{- define "title" }}
|
{{- define "main" }}external template
|
||||||
CSRFToken: {{ .CSRFToken }};
|
WebBasePath: {{ .WebBasePath }};
|
||||||
Challenge: {{ .Challenge }};
|
|
||||||
LoginURL: {{ .LoginURL }};
|
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
|
||||||
IsInternalError: {{ .IsInternalError }};
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- define "style" }}
|
Langs:
|
||||||
CSRFToken: {{ .CSRFToken }};
|
{{ range .LangPrefs }}{{ .Lang }};q={{ .Weight }},{{ end }}
|
||||||
Challenge: {{ .Challenge }};
|
|
||||||
LoginURL: {{ .LoginURL }};
|
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
|
||||||
IsInternalError: {{ .IsInternalError }};
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- define "js" }}
|
Data:
|
||||||
CSRFToken: {{ .CSRFToken }};
|
CSRFToken: {{ .Data.CSRFToken }};
|
||||||
Challenge: {{ .Challenge }};
|
Challenge: {{ .Data.Challenge }};
|
||||||
LoginURL: {{ .LoginURL }};
|
LoginURL: {{ .Data.LoginURL }};
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
IsInvalidCredentials: {{ .Data.IsInvalidCredentials }};
|
||||||
IsInternalError: {{ .IsInternalError }};
|
IsInternalError: {{ .Data.IsInternalError }};
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- define "content" }}
|
|
||||||
CSRFToken: {{ .CSRFToken }};
|
|
||||||
Challenge: {{ .Challenge }};
|
|
||||||
LoginURL: {{ .LoginURL }};
|
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
|
||||||
IsInternalError: {{ .IsInternalError }};
|
|
||||||
{{- end }}
|
{{- end }}
|
@ -1,32 +1,10 @@
|
|||||||
internal template
|
internal template
|
||||||
WebBasePath: testBasePath;
|
WebBasePath: testBasePath;
|
||||||
|
|
||||||
Title:
|
Langs:
|
||||||
|
ru-RU;q=1,ru;q=0.9,en-US;q=0.8,en;q=0.7,
|
||||||
CSRFToken: testCSRFToken;
|
|
||||||
Challenge: testChalenge;
|
|
||||||
LoginURL: testLoginURL;
|
|
||||||
IsInvalidCredentials: true;
|
|
||||||
IsInternalError: true;
|
|
||||||
|
|
||||||
Style:
|
|
||||||
|
|
||||||
CSRFToken: testCSRFToken;
|
|
||||||
Challenge: testChalenge;
|
|
||||||
LoginURL: testLoginURL;
|
|
||||||
IsInvalidCredentials: true;
|
|
||||||
IsInternalError: true;
|
|
||||||
|
|
||||||
Js:
|
|
||||||
|
|
||||||
CSRFToken: testCSRFToken;
|
|
||||||
Challenge: testChalenge;
|
|
||||||
LoginURL: testLoginURL;
|
|
||||||
IsInvalidCredentials: true;
|
|
||||||
IsInternalError: true;
|
|
||||||
|
|
||||||
Content:
|
|
||||||
|
|
||||||
|
Data:
|
||||||
CSRFToken: testCSRFToken;
|
CSRFToken: testCSRFToken;
|
||||||
Challenge: testChalenge;
|
Challenge: testChalenge;
|
||||||
LoginURL: testLoginURL;
|
LoginURL: testLoginURL;
|
||||||
|
@ -1,31 +1,13 @@
|
|||||||
{{- define "title" }}
|
{{- define "main" }}internal template
|
||||||
CSRFToken: {{ .CSRFToken }};
|
WebBasePath: {{ .WebBasePath }};
|
||||||
Challenge: {{ .Challenge }};
|
|
||||||
LoginURL: {{ .LoginURL }};
|
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
|
||||||
IsInternalError: {{ .IsInternalError }};
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- define "style" }}
|
Langs:
|
||||||
CSRFToken: {{ .CSRFToken }};
|
{{ range .LangPrefs }}{{ .Lang }};q={{ .Weight }},{{ end }}
|
||||||
Challenge: {{ .Challenge }};
|
|
||||||
LoginURL: {{ .LoginURL }};
|
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
|
||||||
IsInternalError: {{ .IsInternalError }};
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- define "js" }}
|
Data:
|
||||||
CSRFToken: {{ .CSRFToken }};
|
CSRFToken: {{ .Data.CSRFToken }};
|
||||||
Challenge: {{ .Challenge }};
|
Challenge: {{ .Data.Challenge }};
|
||||||
LoginURL: {{ .LoginURL }};
|
LoginURL: {{ .Data.LoginURL }};
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
IsInvalidCredentials: {{ .Data.IsInvalidCredentials }};
|
||||||
IsInternalError: {{ .IsInternalError }};
|
IsInternalError: {{ .Data.IsInternalError }};
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{- define "content" }}
|
|
||||||
CSRFToken: {{ .CSRFToken }};
|
|
||||||
Challenge: {{ .Challenge }};
|
|
||||||
LoginURL: {{ .LoginURL }};
|
|
||||||
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
|
||||||
IsInternalError: {{ .IsInternalError }};
|
|
||||||
{{- end }}
|
{{- end }}
|
37
internal/web/testdata/TestHTMLRenderer/old_style_external_template_happy_path/golden.file
vendored
Normal file
37
internal/web/testdata/TestHTMLRenderer/old_style_external_template_happy_path/golden.file
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
external template
|
||||||
|
WebBasePath: testBasePath;
|
||||||
|
|
||||||
|
Langs:
|
||||||
|
ru-RU;q=1,ru;q=0.9,en-US;q=0.8,en;q=0.7,
|
||||||
|
|
||||||
|
Title:
|
||||||
|
|
||||||
|
CSRFToken: testCSRFToken;
|
||||||
|
Challenge: testChalenge;
|
||||||
|
LoginURL: testLoginURL;
|
||||||
|
IsInvalidCredentials: true;
|
||||||
|
IsInternalError: true;
|
||||||
|
|
||||||
|
Style:
|
||||||
|
|
||||||
|
CSRFToken: testCSRFToken;
|
||||||
|
Challenge: testChalenge;
|
||||||
|
LoginURL: testLoginURL;
|
||||||
|
IsInvalidCredentials: true;
|
||||||
|
IsInternalError: true;
|
||||||
|
|
||||||
|
Js:
|
||||||
|
|
||||||
|
CSRFToken: testCSRFToken;
|
||||||
|
Challenge: testChalenge;
|
||||||
|
LoginURL: testLoginURL;
|
||||||
|
IsInvalidCredentials: true;
|
||||||
|
IsInternalError: true;
|
||||||
|
|
||||||
|
Content:
|
||||||
|
|
||||||
|
CSRFToken: testCSRFToken;
|
||||||
|
Challenge: testChalenge;
|
||||||
|
LoginURL: testLoginURL;
|
||||||
|
IsInvalidCredentials: true;
|
||||||
|
IsInternalError: true;
|
31
internal/web/testdata/TestHTMLRenderer/old_style_external_template_happy_path/login.tmpl
vendored
Normal file
31
internal/web/testdata/TestHTMLRenderer/old_style_external_template_happy_path/login.tmpl
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{{- define "title" }}
|
||||||
|
CSRFToken: {{ .CSRFToken }};
|
||||||
|
Challenge: {{ .Challenge }};
|
||||||
|
LoginURL: {{ .LoginURL }};
|
||||||
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "style" }}
|
||||||
|
CSRFToken: {{ .CSRFToken }};
|
||||||
|
Challenge: {{ .Challenge }};
|
||||||
|
LoginURL: {{ .LoginURL }};
|
||||||
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "js" }}
|
||||||
|
CSRFToken: {{ .CSRFToken }};
|
||||||
|
Challenge: {{ .Challenge }};
|
||||||
|
LoginURL: {{ .LoginURL }};
|
||||||
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "content" }}
|
||||||
|
CSRFToken: {{ .CSRFToken }};
|
||||||
|
Challenge: {{ .Challenge }};
|
||||||
|
LoginURL: {{ .LoginURL }};
|
||||||
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
|
{{- end }}
|
@ -1,6 +1,9 @@
|
|||||||
{{- define "main" }}external template
|
{{- define "main" }}external template
|
||||||
WebBasePath: {{ .WebBasePath }};
|
WebBasePath: {{ .WebBasePath }};
|
||||||
|
|
||||||
|
Langs:
|
||||||
|
{{ range .LangPrefs }}{{ .Lang }};q={{ .Weight }},{{ end }}
|
||||||
|
|
||||||
Title:
|
Title:
|
||||||
{{ block "title" .Data }}{{ end }}
|
{{ block "title" .Data }}{{ end }}
|
||||||
|
|
37
internal/web/testdata/TestHTMLRenderer/old_style_internal_template_happy_path/golden.file
vendored
Normal file
37
internal/web/testdata/TestHTMLRenderer/old_style_internal_template_happy_path/golden.file
vendored
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
internal template
|
||||||
|
WebBasePath: testBasePath;
|
||||||
|
|
||||||
|
Langs:
|
||||||
|
ru-RU;q=1,ru;q=0.9,en-US;q=0.8,en;q=0.7,
|
||||||
|
|
||||||
|
Title:
|
||||||
|
|
||||||
|
CSRFToken: testCSRFToken;
|
||||||
|
Challenge: testChalenge;
|
||||||
|
LoginURL: testLoginURL;
|
||||||
|
IsInvalidCredentials: true;
|
||||||
|
IsInternalError: true;
|
||||||
|
|
||||||
|
Style:
|
||||||
|
|
||||||
|
CSRFToken: testCSRFToken;
|
||||||
|
Challenge: testChalenge;
|
||||||
|
LoginURL: testLoginURL;
|
||||||
|
IsInvalidCredentials: true;
|
||||||
|
IsInternalError: true;
|
||||||
|
|
||||||
|
Js:
|
||||||
|
|
||||||
|
CSRFToken: testCSRFToken;
|
||||||
|
Challenge: testChalenge;
|
||||||
|
LoginURL: testLoginURL;
|
||||||
|
IsInvalidCredentials: true;
|
||||||
|
IsInternalError: true;
|
||||||
|
|
||||||
|
Content:
|
||||||
|
|
||||||
|
CSRFToken: testCSRFToken;
|
||||||
|
Challenge: testChalenge;
|
||||||
|
LoginURL: testLoginURL;
|
||||||
|
IsInvalidCredentials: true;
|
||||||
|
IsInternalError: true;
|
31
internal/web/testdata/TestHTMLRenderer/old_style_internal_template_happy_path/login.tmpl
vendored
Normal file
31
internal/web/testdata/TestHTMLRenderer/old_style_internal_template_happy_path/login.tmpl
vendored
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{{- define "title" }}
|
||||||
|
CSRFToken: {{ .CSRFToken }};
|
||||||
|
Challenge: {{ .Challenge }};
|
||||||
|
LoginURL: {{ .LoginURL }};
|
||||||
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "style" }}
|
||||||
|
CSRFToken: {{ .CSRFToken }};
|
||||||
|
Challenge: {{ .Challenge }};
|
||||||
|
LoginURL: {{ .LoginURL }};
|
||||||
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "js" }}
|
||||||
|
CSRFToken: {{ .CSRFToken }};
|
||||||
|
Challenge: {{ .Challenge }};
|
||||||
|
LoginURL: {{ .LoginURL }};
|
||||||
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "content" }}
|
||||||
|
CSRFToken: {{ .CSRFToken }};
|
||||||
|
Challenge: {{ .Challenge }};
|
||||||
|
LoginURL: {{ .LoginURL }};
|
||||||
|
IsInvalidCredentials: {{ .IsInvalidCredentials }};
|
||||||
|
IsInternalError: {{ .IsInternalError }};
|
||||||
|
{{- end }}
|
@ -1,6 +1,9 @@
|
|||||||
{{- define "main" }}internal template
|
{{- define "main" }}internal template
|
||||||
WebBasePath: {{ .WebBasePath }};
|
WebBasePath: {{ .WebBasePath }};
|
||||||
|
|
||||||
|
Langs:
|
||||||
|
{{ range .LangPrefs }}{{ .Lang }};q={{ .Weight }},{{ end }}
|
||||||
|
|
||||||
Title:
|
Title:
|
||||||
{{ block "title" .Data }}{{ end }}
|
{{ block "title" .Data }}{{ end }}
|
||||||
|
|
@ -22,6 +22,7 @@ import (
|
|||||||
assetfs "github.com/elazarl/go-bindata-assetfs"
|
assetfs "github.com/elazarl/go-bindata-assetfs"
|
||||||
"github.com/i-core/routegroup"
|
"github.com/i-core/routegroup"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The file systems provide templates and their resources that are stored in the application's internal assets.
|
// The file systems provide templates and their resources that are stored in the application's internal assets.
|
||||||
@ -74,8 +75,14 @@ func NewHTMLRenderer(cnf Config) (*HTMLRenderer, error) {
|
|||||||
return &HTMLRenderer{Config: cnf, mainTmpl: mainTmpl, fs: fs}, nil
|
return &HTMLRenderer{Config: cnf, mainTmpl: mainTmpl, fs: fs}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type langPref struct {
|
||||||
|
Lang string
|
||||||
|
Weight float32
|
||||||
|
}
|
||||||
|
|
||||||
// RenderTemplate renders a HTML page from a template with the specified name using the specified data.
|
// RenderTemplate renders a HTML page from a template with the specified name using the specified data.
|
||||||
func (r *HTMLRenderer) RenderTemplate(w http.ResponseWriter, name string, data interface{}) error {
|
func (r *HTMLRenderer) RenderTemplate(w http.ResponseWriter, req *http.Request, name string, data interface{}) error {
|
||||||
|
// Read and parse the requested template.
|
||||||
f, err := r.fs.Open(name)
|
f, err := r.fs.Open(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if v, ok := err.(*os.PathError); ok {
|
if v, ok := err.(*os.PathError); ok {
|
||||||
@ -89,20 +96,56 @@ func (r *HTMLRenderer) RenderTemplate(w http.ResponseWriter, name string, data i
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to read template %q: %s", name, err)
|
return fmt.Errorf("failed to read template %q: %s", name, err)
|
||||||
}
|
}
|
||||||
t, err := r.mainTmpl.Clone()
|
root, err := template.New("main").Parse(string(b))
|
||||||
if err != nil {
|
|
||||||
return errors.Wrapf(err, "failed to clone the main template for template %q: %s", name, err)
|
|
||||||
}
|
|
||||||
t, err = t.Parse(string(b))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.Wrapf(err, "failed to parse template %q: %s", name, err)
|
return errors.Wrapf(err, "failed to parse template %q: %s", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The old-style template of a web page showed itself as not flexible.
|
||||||
|
// It was changed with a new template that allows overriding the whole page.
|
||||||
|
// The old-style template left for backward compatibility
|
||||||
|
// and will be deprecated in the future major release.
|
||||||
|
if isOldStyleUserTemplate(root) {
|
||||||
|
var wrapper *template.Template
|
||||||
|
wrapper, err = r.mainTmpl.Clone()
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to clone the main template for template %q: %s", name, err)
|
||||||
|
}
|
||||||
|
root, err = root.AddParseTree("main", wrapper.Tree)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to create the main template for template %q: %s", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare template data.
|
||||||
|
basePath := r.BasePath
|
||||||
|
if basePath == "" {
|
||||||
|
basePath = "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
var langPrefs []langPref
|
||||||
|
if acceptLang := req.Header.Get(http.CanonicalHeaderKey("Accept-Language")); acceptLang != "" {
|
||||||
|
var tags []language.Tag
|
||||||
|
var weights []float32
|
||||||
|
tags, weights, err = language.ParseAcceptLanguage(acceptLang)
|
||||||
|
if err != nil {
|
||||||
|
return errors.Wrapf(err, "failed to parse the header \"Accept-Language\": %s", err)
|
||||||
|
}
|
||||||
|
for i, tag := range tags {
|
||||||
|
langPrefs = append(langPrefs, langPref{Lang: tag.String(), Weight: weights[i]})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
langPrefs = []langPref{{Lang: "en", Weight: 1}}
|
||||||
|
}
|
||||||
|
|
||||||
|
tmplData := map[string]interface{}{"WebBasePath": basePath, "LangPrefs": langPrefs, "Data": data}
|
||||||
|
|
||||||
|
// Render the template.
|
||||||
var (
|
var (
|
||||||
buf bytes.Buffer
|
buf bytes.Buffer
|
||||||
bw = bufio.NewWriter(&buf)
|
bw = bufio.NewWriter(&buf)
|
||||||
)
|
)
|
||||||
if err = t.Execute(bw, map[string]interface{}{"WebBasePath": r.BasePath, "Data": data}); err != nil {
|
if err = root.Execute(bw, tmplData); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err = bw.Flush(); err != nil {
|
if err = bw.Flush(); err != nil {
|
||||||
@ -111,16 +154,38 @@ func (r *HTMLRenderer) RenderTemplate(w http.ResponseWriter, name string, data i
|
|||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
_, err = buf.WriteTo(w)
|
_, err = buf.WriteTo(w)
|
||||||
return err
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns true if a template is the old-style template.
|
||||||
|
//
|
||||||
|
// A template is considered as the old-style template
|
||||||
|
// if it contains four blocks for customizing the page title,
|
||||||
|
// styles, markup, and scripts.
|
||||||
|
//
|
||||||
|
// See https://github.com/i-core/werther/issues/11.
|
||||||
|
func isOldStyleUserTemplate(root *template.Template) bool {
|
||||||
|
var tmpls []string
|
||||||
|
for _, tmpl := range root.Templates() {
|
||||||
|
tmpls = append(tmpls, tmpl.Name())
|
||||||
|
}
|
||||||
|
contains := func(arr []string, tgt string) bool {
|
||||||
|
for _, item := range arr {
|
||||||
|
if item == tgt {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return contains(tmpls, "title") && contains(tmpls, "style") && contains(tmpls, "js") && contains(tmpls, "content")
|
||||||
}
|
}
|
||||||
|
|
||||||
var mainT = `{{ define "main" }}
|
var mainT = `{{ define "main" }}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="{{ (index .LangPrefs 0).Lang }}">
|
||||||
<head>
|
<head>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>{{ block "title" .Data }}{{ end }}</title>
|
<title>{{ block "title" .Data }}{{ end }}</title>
|
||||||
<base href={{ .WebBasePath }}>
|
<base href="{{ .WebBasePath }}">
|
||||||
{{ block "style" .Data }}{{ end }}
|
{{ block "style" .Data }}{{ end }}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
@ -60,23 +60,64 @@ func TestHTMLRenderer(t *testing.T) {
|
|||||||
"IsInternalError": true,
|
"IsInternalError": true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: "old style internal template not found",
|
||||||
|
wantErr: fmt.Errorf(`the template "login.tmpl" does not exist`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "old style internal template happy path",
|
||||||
|
basePath: "testBasePath",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"CSRFToken": "testCSRFToken",
|
||||||
|
"Challenge": "testChalenge",
|
||||||
|
"LoginURL": "testLoginURL",
|
||||||
|
"IsInvalidCredentials": true,
|
||||||
|
"IsInternalError": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "old style external template not found",
|
||||||
|
ext: true,
|
||||||
|
wantErr: fmt.Errorf(`the template "login.tmpl" does not exist`),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "old style external template happy path",
|
||||||
|
ext: true,
|
||||||
|
basePath: "testBasePath",
|
||||||
|
data: map[string]interface{}{
|
||||||
|
"CSRFToken": "testCSRFToken",
|
||||||
|
"Challenge": "testChalenge",
|
||||||
|
"LoginURL": "testLoginURL",
|
||||||
|
"IsInvalidCredentials": true,
|
||||||
|
"IsInternalError": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, tc := range testCases {
|
for _, tc := range testCases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
tstDir := path.Join("testdata", t.Name())
|
tstDir := path.Join("testdata", t.Name())
|
||||||
|
|
||||||
// Read the main template.
|
|
||||||
var originMainT = mainT
|
var originMainT = mainT
|
||||||
defer func() { mainT = originMainT }()
|
defer func() { mainT = originMainT }()
|
||||||
f, err := os.Open(path.Join(tstDir, "main.tmpl"))
|
|
||||||
if err != nil {
|
// Read the main template if it is exist.
|
||||||
|
fpath := path.Join(tstDir, "main.tmpl")
|
||||||
|
stat, err := os.Stat(fpath)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
t.Fatalf("failed to open main template: %s", err)
|
t.Fatalf("failed to open main template: %s", err)
|
||||||
}
|
}
|
||||||
fc, err := ioutil.ReadAll(f)
|
if stat != nil {
|
||||||
if err != nil {
|
var f *os.File
|
||||||
t.Fatalf("failed to read main template: %s", err)
|
if f, err = os.Open(fpath); err != nil {
|
||||||
|
t.Fatalf("failed to open main template: %s", err)
|
||||||
|
}
|
||||||
|
var fc []byte
|
||||||
|
if fc, err = ioutil.ReadAll(f); err != nil {
|
||||||
|
t.Fatalf("failed to read main template: %s", err)
|
||||||
|
}
|
||||||
|
mainT = string(fc)
|
||||||
}
|
}
|
||||||
mainT = string(fc)
|
|
||||||
|
|
||||||
// Create the template renderer.
|
// Create the template renderer.
|
||||||
cnf := Config{BasePath: tc.basePath}
|
cnf := Config{BasePath: tc.basePath}
|
||||||
@ -93,7 +134,9 @@ func TestHTMLRenderer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rr := httptest.NewRecorder()
|
rr := httptest.NewRecorder()
|
||||||
err = r.RenderTemplate(rr, "login.tmpl", tc.data)
|
req := httptest.NewRequest(http.MethodGet, "http://localhost", nil)
|
||||||
|
req.Header.Set(http.CanonicalHeaderKey("Accept-Language"), "ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7")
|
||||||
|
err = r.RenderTemplate(rr, req, "login.tmpl", tc.data)
|
||||||
|
|
||||||
if tc.wantErr != nil {
|
if tc.wantErr != nil {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
@ -107,11 +150,11 @@ func TestHTMLRenderer(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("\ngot error\n\t%s\nwant no errors", err)
|
t.Fatalf("\ngot error\n\t%s\nwant no errors", err)
|
||||||
}
|
}
|
||||||
f, err = os.Open(path.Join(tstDir, "golden.file"))
|
f, err := os.Open(path.Join(tstDir, "golden.file"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to open golden file: %s", err)
|
t.Fatalf("failed to open golden file: %s", err)
|
||||||
}
|
}
|
||||||
fc, err = ioutil.ReadAll(f)
|
fc, err := ioutil.ReadAll(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to read golden file: %s", err)
|
t.Fatalf("failed to read golden file: %s", err)
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user