snapshot of https://github.com/ory/kratos.git (master, 2026-09-15; blobs > 131072B omitted)
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
<p>Hello,</p>
|
||||
<p>A new sign-in method (device key) was added to your account. If you did this, no further action is needed.</p>
|
||||
<p>If this wasn't you, secure your account immediately and contact support.</p>
|
||||
<p><strong>Account ID:</strong> {{ index .Identity "id" }}<br/>
|
||||
<strong>Added at:</strong> {{ .AddedAt }}</p>
|
||||
@@ -0,0 +1,9 @@
|
||||
Hello,
|
||||
|
||||
A new sign-in method (device key) was added to your account. If you did
|
||||
this, no further action is needed.
|
||||
|
||||
If this wasn't you, secure your account immediately and contact support.
|
||||
|
||||
Account ID: {{ index .Identity "id" }}
|
||||
Added at: {{ .AddedAt }}
|
||||
@@ -0,0 +1 @@
|
||||
A new sign-in method was added to your account
|
||||
@@ -0,0 +1 @@
|
||||
A new sign-in method (device key) was added to your account. If this wasn't you, secure your account.
|
||||
@@ -0,0 +1,5 @@
|
||||
Login to your account with the following code:
|
||||
|
||||
{{ .LoginCode }}
|
||||
|
||||
It expires in {{ .ExpiresInMinutes }} minutes.
|
||||
@@ -0,0 +1,5 @@
|
||||
Login to your account with the following code:
|
||||
|
||||
{{ .LoginCode }}
|
||||
|
||||
It expires in {{ .ExpiresInMinutes }} minutes.
|
||||
@@ -0,0 +1 @@
|
||||
Use code {{ .LoginCode }} to log in
|
||||
@@ -0,0 +1,6 @@
|
||||
Your login code is: {{ .LoginCode }}
|
||||
|
||||
It expires in {{ .ExpiresInMinutes }} minutes.
|
||||
{{ if .RequestURLDomain }}
|
||||
@{{ .RequestURLDomain }} #{{ .LoginCode }}
|
||||
{{ end -}}
|
||||
@@ -0,0 +1 @@
|
||||
Your verification code is: {{ .Code }}
|
||||
@@ -0,0 +1,9 @@
|
||||
Hi,
|
||||
|
||||
you (or someone else) entered this email address when trying to recover access to an account.
|
||||
|
||||
However, this email address is not on our database of registered users and therefore the attempt has failed.
|
||||
|
||||
If this was you, check if you signed up using a different address.
|
||||
|
||||
If this was not you, please ignore this email.
|
||||
@@ -0,0 +1,9 @@
|
||||
Hi,
|
||||
|
||||
you (or someone else) entered this email address when trying to recover access to an account.
|
||||
|
||||
However, this email address is not on our database of registered users and therefore the attempt has failed.
|
||||
|
||||
If this was you, check if you signed up using a different address.
|
||||
|
||||
If this was not you, please ignore this email.
|
||||
@@ -0,0 +1 @@
|
||||
Account access attempted
|
||||
@@ -0,0 +1,5 @@
|
||||
Recover access to your account by clicking the following link:
|
||||
|
||||
<a href="{{ .RecoveryURL }}">{{ .RecoveryURL }}</a>
|
||||
|
||||
If this was not you, do nothing. This link expires in {{ .ExpiresInMinutes }} minutes.
|
||||
@@ -0,0 +1,6 @@
|
||||
Recover access to your account by clicking the following link:
|
||||
|
||||
{{ .RecoveryURL }}
|
||||
|
||||
If this was not you, do nothing. This link expires in {{ .ExpiresInMinutes }} minutes.
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Recover access to your account
|
||||
@@ -0,0 +1,9 @@
|
||||
Hi,
|
||||
|
||||
you (or someone else) entered this email address when trying to recover access to an account.
|
||||
|
||||
However, this email address is not on our database of registered users and therefore the attempt has failed.
|
||||
|
||||
If this was you, check if you signed up using a different address.
|
||||
|
||||
If this was not you, please ignore this email.
|
||||
@@ -0,0 +1,9 @@
|
||||
Hi,
|
||||
|
||||
you (or someone else) entered this email address when trying to recover access to an account.
|
||||
|
||||
However, this email address is not on our database of registered users and therefore the attempt has failed.
|
||||
|
||||
If this was you, check if you signed up using a different address.
|
||||
|
||||
If this was not you, please ignore this email.
|
||||
@@ -0,0 +1 @@
|
||||
Account access attempted
|
||||
@@ -0,0 +1,5 @@
|
||||
Recover access to your account by entering the following code:
|
||||
|
||||
{{ .RecoveryCode }}
|
||||
|
||||
If this was not you, do nothing. This code expires in {{ .ExpiresInMinutes }} minutes.
|
||||
@@ -0,0 +1,5 @@
|
||||
Recover access to your account by entering the following code:
|
||||
|
||||
{{ .RecoveryCode }}
|
||||
|
||||
If this was not you, do nothing. This code expires in {{ .ExpiresInMinutes }} minutes.
|
||||
@@ -0,0 +1 @@
|
||||
Use code {{ .RecoveryCode }} to recover access to your account
|
||||
@@ -0,0 +1,3 @@
|
||||
Your recovery code is: {{ .RecoveryCode }}
|
||||
|
||||
@{{ .RequestURLDomain }} #{{ .RecoveryCode }}
|
||||
@@ -0,0 +1,5 @@
|
||||
Complete your account registration with the following code:
|
||||
|
||||
{{ .RegistrationCode }}
|
||||
|
||||
This code expires in {{ .ExpiresInMinutes }} minutes.
|
||||
@@ -0,0 +1,5 @@
|
||||
Complete your account registration with the following code:
|
||||
|
||||
{{ .RegistrationCode }}
|
||||
|
||||
This code expires in {{ .ExpiresInMinutes }} minutes.
|
||||
@@ -0,0 +1 @@
|
||||
Use code {{ .RegistrationCode }} to complete your account registration
|
||||
@@ -0,0 +1,6 @@
|
||||
Your registration code is: {{ .RegistrationCode }}
|
||||
|
||||
It expires in {{ .ExpiresInMinutes }} minutes.
|
||||
{{ if .RequestURLDomain }}
|
||||
@{{ .RequestURLDomain }} #{{ .RegistrationCode }}
|
||||
{{ end -}}
|
||||
@@ -0,0 +1 @@
|
||||
stub email body {{ if .HTMLBody }}{{ .HTMLBody }}{{ else }}{{ .Body }}{{ end }}
|
||||
@@ -0,0 +1,2 @@
|
||||
{{ $l := cat "lang=" .lang }}
|
||||
{{ nospace $l }}
|
||||
@@ -0,0 +1,3 @@
|
||||
{{- if eq .lang "en_US" -}}
|
||||
{{ template "email.body.html.en_US.gotmpl" . }}
|
||||
{{- end -}}
|
||||
@@ -0,0 +1 @@
|
||||
stub email body {{ .Body }}
|
||||
@@ -0,0 +1,5 @@
|
||||
{{ $t1 := title .input }}
|
||||
{{ $t1 := nospace $t1 }}
|
||||
{{ $t2 := upper $t1 }}
|
||||
{{ $t3 := cat $t1 "," $t2 }}
|
||||
{{ nospace $t3 }}
|
||||
@@ -0,0 +1 @@
|
||||
stub email subject {{ .Subject }}
|
||||
@@ -0,0 +1,5 @@
|
||||
<p>Hello,</p>
|
||||
<p>A verifiable address (email or phone) on your account was recently changed. If you made this change, no further action is needed.</p>
|
||||
<p>If you did not make this change, please contact support immediately.</p>
|
||||
<p><strong>Account ID:</strong> {{ index .Identity "ID" }}<br/>
|
||||
<strong>Changed at:</strong> {{ .ChangedAt }}</p>
|
||||
@@ -0,0 +1,9 @@
|
||||
Hello,
|
||||
|
||||
A verifiable address (email or phone) on your account was recently
|
||||
changed. If you made this change, no further action is needed.
|
||||
|
||||
If you did not make this change, please contact support immediately.
|
||||
|
||||
Account ID: {{ index .Identity "ID" }}
|
||||
Changed at: {{ .ChangedAt }}
|
||||
@@ -0,0 +1 @@
|
||||
A verifiable address on your account was changed
|
||||
@@ -0,0 +1 @@
|
||||
An address on your account was changed. If this wasn't you, contact support.
|
||||
@@ -0,0 +1,5 @@
|
||||
Someone asked to verify this email address, but we were unable to find an account for this address.
|
||||
|
||||
If this was you, check if you signed up using a different address.
|
||||
|
||||
If this was not you, please ignore this email.
|
||||
@@ -0,0 +1,5 @@
|
||||
Someone asked to verify this email address, but we were unable to find an account for this address.
|
||||
|
||||
If this was you, check if you signed up using a different address.
|
||||
|
||||
If this was not you, please ignore this email.
|
||||
@@ -0,0 +1 @@
|
||||
Someone tried to verify this email address
|
||||
@@ -0,0 +1,5 @@
|
||||
Verify your account by opening the following link:
|
||||
|
||||
<a href="{{ .VerificationURL }}">{{ .VerificationURL }}</a>
|
||||
|
||||
If this was not you, do nothing. This link expires in {{ .ExpiresInMinutes }} minutes.
|
||||
@@ -0,0 +1,5 @@
|
||||
Verify your account by opening the following link:
|
||||
|
||||
{{ .VerificationURL }}
|
||||
|
||||
If this was not you, do nothing. This link expires in {{ .ExpiresInMinutes }} minutes.
|
||||
@@ -0,0 +1 @@
|
||||
Please verify your email address
|
||||
@@ -0,0 +1,7 @@
|
||||
Hi,
|
||||
|
||||
someone asked to verify this email address, but we were unable to find an account for this address.
|
||||
|
||||
If this was you, check if you signed up using a different address.
|
||||
|
||||
If this was not you, please ignore this email.
|
||||
@@ -0,0 +1,7 @@
|
||||
Hi,
|
||||
|
||||
someone asked to verify this email address, but we were unable to find an account for this address.
|
||||
|
||||
If this was you, check if you signed up using a different address.
|
||||
|
||||
If this was not you, please ignore this email.
|
||||
@@ -0,0 +1 @@
|
||||
Someone tried to verify this email address
|
||||
@@ -0,0 +1,9 @@
|
||||
Verify your account with the following code:
|
||||
|
||||
{{ .VerificationCode }}
|
||||
|
||||
or clicking the following link:
|
||||
|
||||
<a href="{{ .VerificationURL }}">{{ .VerificationURL }}</a>
|
||||
|
||||
If this was not you, do nothing. This code / link expires in {{ .ExpiresInMinutes }} minutes.
|
||||
@@ -0,0 +1,9 @@
|
||||
Verify your account with the following code:
|
||||
|
||||
{{ .VerificationCode }}
|
||||
|
||||
or clicking the following link:
|
||||
|
||||
{{ .VerificationURL }}
|
||||
|
||||
If this was not you, do nothing. This code / link expires in {{ .ExpiresInMinutes }} minutes.
|
||||
@@ -0,0 +1 @@
|
||||
Use code {{ .VerificationCode }} to verify your account
|
||||
@@ -0,0 +1,6 @@
|
||||
Your verification code is: {{ .VerificationCode }}
|
||||
|
||||
If this was not you, do nothing. It expires in {{ .ExpiresInMinutes }} minutes.
|
||||
{{ if .RequestURLDomain }}
|
||||
@{{ .RequestURLDomain }} #{{ .VerificationCode }}
|
||||
{{ end -}}
|
||||
55
courier/template/email/authenticator_key_added.go
Normal file
55
courier/template/email/authenticator_key_added.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
AuthenticatorKeyAdded struct {
|
||||
d template.Dependencies
|
||||
m *AuthenticatorKeyAddedModel
|
||||
}
|
||||
AuthenticatorKeyAddedModel struct {
|
||||
To string `json:"to"`
|
||||
Identity map[string]any `json:"identity"`
|
||||
AddedAt string `json:"added_at"`
|
||||
TransientPayload map[string]any `json:"transient_payload"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewAuthenticatorKeyAdded(d template.Dependencies, m *AuthenticatorKeyAddedModel) *AuthenticatorKeyAdded {
|
||||
return &AuthenticatorKeyAdded{d: d, m: m}
|
||||
}
|
||||
|
||||
func (t *AuthenticatorKeyAdded) EmailRecipient() (string, error) {
|
||||
return t.m.To, nil
|
||||
}
|
||||
|
||||
func (t *AuthenticatorKeyAdded) EmailSubject(ctx context.Context) (string, error) {
|
||||
subject, err := template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "authenticator_key_added/email.subject.gotmpl", "authenticator_key_added/email.subject*", t.m, t.d.CourierConfig().CourierTemplatesAuthenticatorKeyAdded(ctx).Subject)
|
||||
return strings.TrimSpace(subject), err
|
||||
}
|
||||
|
||||
func (t *AuthenticatorKeyAdded) EmailBody(ctx context.Context) (string, error) {
|
||||
return template.LoadHTML(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "authenticator_key_added/email.body.gotmpl", "authenticator_key_added/email.body*", t.m, t.d.CourierConfig().CourierTemplatesAuthenticatorKeyAdded(ctx).Body.HTML)
|
||||
}
|
||||
|
||||
func (t *AuthenticatorKeyAdded) EmailBodyPlaintext(ctx context.Context) (string, error) {
|
||||
return template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "authenticator_key_added/email.body.plaintext.gotmpl", "authenticator_key_added/email.body.plaintext*", t.m, t.d.CourierConfig().CourierTemplatesAuthenticatorKeyAdded(ctx).Body.PlainText)
|
||||
}
|
||||
|
||||
func (t *AuthenticatorKeyAdded) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.m)
|
||||
}
|
||||
|
||||
func (t *AuthenticatorKeyAdded) TemplateType() template.TemplateType {
|
||||
return template.TypeAuthenticatorKeyAdded
|
||||
}
|
||||
55
courier/template/email/authenticator_key_added_test.go
Normal file
55
courier/template/email/authenticator_key_added_test.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gofrs/uuid"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
"github.com/ory/kratos/identity"
|
||||
"github.com/ory/kratos/pkg"
|
||||
"github.com/ory/kratos/x"
|
||||
)
|
||||
|
||||
func TestAuthenticatorKeyAdded(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
|
||||
// Build the Identity map exactly as the courier does in production
|
||||
// (identity.Manager.sendIdentityNotifications uses x.StructToMap), so the
|
||||
// template's key lookup is tested against the real key casing instead of a
|
||||
// hand-crafted map. StructToMap marshals via JSON tags, so the ID lands under
|
||||
// the lowercase "id" key.
|
||||
id := &identity.Identity{ID: uuid.Must(uuid.FromString("00000000-0000-0000-0000-000000000001"))}
|
||||
idMap, err := x.StructToMap(id)
|
||||
require.NoError(t, err)
|
||||
|
||||
tpl := email.NewAuthenticatorKeyAdded(reg, &email.AuthenticatorKeyAddedModel{
|
||||
To: "owner@example.com",
|
||||
AddedAt: "2026-04-21T12:00:00Z",
|
||||
Identity: idMap,
|
||||
})
|
||||
|
||||
recipient, err := tpl.EmailRecipient()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "owner@example.com", recipient)
|
||||
|
||||
subject, err := tpl.EmailSubject(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, strings.ToLower(subject), "sign-in method")
|
||||
|
||||
body, err := tpl.EmailBody(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, body, "00000000-0000-0000-0000-000000000001", "the account ID must render in the email body")
|
||||
|
||||
plain, err := tpl.EmailBodyPlaintext(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, plain, "00000000-0000-0000-0000-000000000001", "the account ID must render in the plaintext body")
|
||||
assert.Contains(t, plain, "2026-04-21T12:00:00Z")
|
||||
}
|
||||
65
courier/template/email/login_code_valid.go
Normal file
65
courier/template/email/login_code_valid.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
LoginCodeValid struct {
|
||||
deps template.Dependencies
|
||||
model *LoginCodeValidModel
|
||||
}
|
||||
LoginCodeValidModel struct {
|
||||
To string `json:"to"`
|
||||
LoginCode string `json:"login_code"`
|
||||
Identity map[string]any `json:"identity"`
|
||||
RequestURL string `json:"request_url"`
|
||||
TransientPayload map[string]any `json:"transient_payload"`
|
||||
ExpiresInMinutes int `json:"expires_in_minutes"`
|
||||
OAuth2LoginRequest *template.OAuth2LoginRequest `json:"oauth2_login_request,omitempty"`
|
||||
UserRequestHeaders http.Header `json:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewLoginCodeValid(d template.Dependencies, m *LoginCodeValidModel) *LoginCodeValid {
|
||||
return &LoginCodeValid{deps: d, model: m}
|
||||
}
|
||||
|
||||
func (t *LoginCodeValid) EmailRecipient() (string, error) {
|
||||
return t.model.To, nil
|
||||
}
|
||||
|
||||
func (t *LoginCodeValid) EmailSubject(ctx context.Context) (string, error) {
|
||||
subject, err := template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.subject.gotmpl", "login_code/valid/email.subject*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Subject)
|
||||
|
||||
return strings.TrimSpace(subject), err
|
||||
}
|
||||
|
||||
func (t *LoginCodeValid) EmailBody(ctx context.Context) (string, error) {
|
||||
return template.LoadHTML(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.body.gotmpl", "login_code/valid/email.body*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Body.HTML)
|
||||
}
|
||||
|
||||
func (t *LoginCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) {
|
||||
return template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "login_code/valid/email.body.plaintext.gotmpl", "login_code/valid/email.body.plaintext*", t.model, t.deps.CourierConfig().CourierTemplatesLoginCodeValid(ctx).Body.PlainText)
|
||||
}
|
||||
|
||||
func (t *LoginCodeValid) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.model)
|
||||
}
|
||||
|
||||
func (t *LoginCodeValid) TemplateType() template.TemplateType {
|
||||
return template.TypeLoginCodeValid
|
||||
}
|
||||
|
||||
func (t *LoginCodeValid) RequestHeaders() http.Header {
|
||||
return t.model.UserRequestHeaders
|
||||
}
|
||||
30
courier/template/email/login_code_valid_test.go
Normal file
30
courier/template/email/login_code_valid_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
"github.com/ory/kratos/courier/template/testhelpers"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestLoginCodeValid(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("test=with courier templates directory", func(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
tpl := email.NewLoginCodeValid(reg, &email.LoginCodeValidModel{})
|
||||
|
||||
testhelpers.TestRendered(t, ctx, tpl)
|
||||
})
|
||||
|
||||
t.Run("test=with remote resources", func(t *testing.T) {
|
||||
testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/login_code/valid", template.TypeLoginCodeValid)
|
||||
})
|
||||
}
|
||||
58
courier/template/email/recovery_code_invalid.go
Normal file
58
courier/template/email/recovery_code_invalid.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
RecoveryCodeInvalid struct {
|
||||
deps template.Dependencies
|
||||
model *RecoveryCodeInvalidModel
|
||||
}
|
||||
RecoveryCodeInvalidModel struct {
|
||||
To string `json:"to"`
|
||||
RequestURL string `json:"request_url"`
|
||||
TransientPayload map[string]interface{} `json:"transient_payload"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewRecoveryCodeInvalid(d template.Dependencies, m *RecoveryCodeInvalidModel) *RecoveryCodeInvalid {
|
||||
return &RecoveryCodeInvalid{deps: d, model: m}
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeInvalid) EmailRecipient() (string, error) {
|
||||
return t.model.To, nil
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeInvalid) EmailSubject(ctx context.Context) (string, error) {
|
||||
filesystem := os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx))
|
||||
remoteURL := t.deps.CourierConfig().CourierTemplatesRecoveryCodeInvalid(ctx).Subject
|
||||
|
||||
subject, err := template.LoadText(ctx, t.deps, filesystem, "recovery_code/invalid/email.subject.gotmpl", "recovery_code/invalid/email.subject*", t.model, remoteURL)
|
||||
|
||||
return strings.TrimSpace(subject), err
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeInvalid) EmailBody(ctx context.Context) (string, error) {
|
||||
return template.LoadHTML(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "recovery_code/invalid/email.body.gotmpl", "recovery_code/invalid/email.body*", t.model, t.deps.CourierConfig().CourierTemplatesRecoveryCodeInvalid(ctx).Body.HTML)
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeInvalid) EmailBodyPlaintext(ctx context.Context) (string, error) {
|
||||
return template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "recovery_code/invalid/email.body.plaintext.gotmpl", "recovery_code/invalid/email.body.plaintext*", t.model, t.deps.CourierConfig().CourierTemplatesRecoveryCodeInvalid(ctx).Body.PlainText)
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeInvalid) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.model)
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeInvalid) TemplateType() template.TemplateType {
|
||||
return template.TypeRecoveryCodeInvalid
|
||||
}
|
||||
30
courier/template/email/recovery_code_invalid_test.go
Normal file
30
courier/template/email/recovery_code_invalid_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
"github.com/ory/kratos/courier/template/testhelpers"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestRecoveryCodeInvalid(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("test=with courier templates directory", func(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
tpl := email.NewRecoveryCodeInvalid(reg, &email.RecoveryCodeInvalidModel{})
|
||||
|
||||
testhelpers.TestRendered(t, ctx, tpl)
|
||||
})
|
||||
|
||||
t.Run("case=test remote resources", func(t *testing.T) {
|
||||
testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery_code/invalid", template.TypeRecoveryCodeInvalid)
|
||||
})
|
||||
}
|
||||
63
courier/template/email/recovery_code_valid.go
Normal file
63
courier/template/email/recovery_code_valid.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
RecoveryCodeValid struct {
|
||||
deps template.Dependencies
|
||||
model *RecoveryCodeValidModel
|
||||
}
|
||||
RecoveryCodeValidModel struct {
|
||||
To string `json:"to"`
|
||||
RecoveryCode string `json:"recovery_code"`
|
||||
Identity map[string]interface{} `json:"identity"`
|
||||
RequestURL string `json:"request_url"`
|
||||
TransientPayload map[string]interface{} `json:"transient_payload"`
|
||||
ExpiresInMinutes int `json:"expires_in_minutes"`
|
||||
UserRequestHeaders http.Header `json:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewRecoveryCodeValid(d template.Dependencies, m *RecoveryCodeValidModel) *RecoveryCodeValid {
|
||||
return &RecoveryCodeValid{deps: d, model: m}
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeValid) EmailRecipient() (string, error) {
|
||||
return t.model.To, nil
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeValid) EmailSubject(ctx context.Context) (string, error) {
|
||||
subject, err := template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "recovery_code/valid/email.subject.gotmpl", "recovery_code/valid/email.subject*", t.model, t.deps.CourierConfig().CourierTemplatesRecoveryCodeValid(ctx).Subject)
|
||||
|
||||
return strings.TrimSpace(subject), err
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeValid) EmailBody(ctx context.Context) (string, error) {
|
||||
return template.LoadHTML(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "recovery_code/valid/email.body.gotmpl", "recovery_code/valid/email.body*", t.model, t.deps.CourierConfig().CourierTemplatesRecoveryCodeValid(ctx).Body.HTML)
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) {
|
||||
return template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "recovery_code/valid/email.body.plaintext.gotmpl", "recovery_code/valid/email.body.plaintext*", t.model, t.deps.CourierConfig().CourierTemplatesRecoveryCodeValid(ctx).Body.PlainText)
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeValid) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.model)
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeValid) TemplateType() template.TemplateType {
|
||||
return template.TypeRecoveryCodeValid
|
||||
}
|
||||
func (t *RecoveryCodeValid) RequestHeaders() http.Header {
|
||||
return t.model.UserRequestHeaders
|
||||
}
|
||||
30
courier/template/email/recovery_code_valid_test.go
Normal file
30
courier/template/email/recovery_code_valid_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
"github.com/ory/kratos/courier/template/testhelpers"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestRecoveryCodeValid(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("test=with courier templates directory", func(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
tpl := email.NewRecoveryCodeValid(reg, &email.RecoveryCodeValidModel{})
|
||||
|
||||
testhelpers.TestRendered(t, ctx, tpl)
|
||||
})
|
||||
|
||||
t.Run("test=with remote resources", func(t *testing.T) {
|
||||
testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery_code/valid", template.TypeRecoveryCodeValid)
|
||||
})
|
||||
}
|
||||
55
courier/template/email/recovery_invalid.go
Normal file
55
courier/template/email/recovery_invalid.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
RecoveryInvalid struct {
|
||||
d template.Dependencies
|
||||
m *RecoveryInvalidModel
|
||||
}
|
||||
RecoveryInvalidModel struct {
|
||||
To string `json:"to"`
|
||||
RequestURL string `json:"request_url"`
|
||||
TransientPayload map[string]interface{} `json:"transient_payload"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewRecoveryInvalid(d template.Dependencies, m *RecoveryInvalidModel) *RecoveryInvalid {
|
||||
return &RecoveryInvalid{d: d, m: m}
|
||||
}
|
||||
|
||||
func (t *RecoveryInvalid) EmailRecipient() (string, error) {
|
||||
return t.m.To, nil
|
||||
}
|
||||
|
||||
func (t *RecoveryInvalid) EmailSubject(ctx context.Context) (string, error) {
|
||||
subject, err := template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "recovery/invalid/email.subject.gotmpl", "recovery/invalid/email.subject*", t.m, t.d.CourierConfig().CourierTemplatesRecoveryInvalid(ctx).Subject)
|
||||
|
||||
return strings.TrimSpace(subject), err
|
||||
}
|
||||
|
||||
func (t *RecoveryInvalid) EmailBody(ctx context.Context) (string, error) {
|
||||
return template.LoadHTML(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "recovery/invalid/email.body.gotmpl", "recovery/invalid/email.body*", t.m, t.d.CourierConfig().CourierTemplatesRecoveryInvalid(ctx).Body.HTML)
|
||||
}
|
||||
|
||||
func (t *RecoveryInvalid) EmailBodyPlaintext(ctx context.Context) (string, error) {
|
||||
return template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "recovery/invalid/email.body.plaintext.gotmpl", "recovery/invalid/email.body.plaintext*", t.m, t.d.CourierConfig().CourierTemplatesRecoveryInvalid(ctx).Body.PlainText)
|
||||
}
|
||||
|
||||
func (t *RecoveryInvalid) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.m)
|
||||
}
|
||||
|
||||
func (t *RecoveryInvalid) TemplateType() template.TemplateType {
|
||||
return template.TypeRecoveryInvalid
|
||||
}
|
||||
30
courier/template/email/recovery_invalid_test.go
Normal file
30
courier/template/email/recovery_invalid_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
"github.com/ory/kratos/courier/template/testhelpers"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestRecoverInvalid(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("test=with courier templates directory", func(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
tpl := email.NewRecoveryInvalid(reg, &email.RecoveryInvalidModel{})
|
||||
|
||||
testhelpers.TestRendered(t, ctx, tpl)
|
||||
})
|
||||
|
||||
t.Run("case=test remote resources", func(t *testing.T) {
|
||||
testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery/invalid", template.TypeRecoveryInvalid)
|
||||
})
|
||||
}
|
||||
58
courier/template/email/recovery_valid.go
Normal file
58
courier/template/email/recovery_valid.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
RecoveryValid struct {
|
||||
d template.Dependencies
|
||||
m *RecoveryValidModel
|
||||
}
|
||||
RecoveryValidModel struct {
|
||||
To string `json:"to"`
|
||||
RecoveryURL string `json:"recovery_url"`
|
||||
Identity map[string]interface{} `json:"identity"`
|
||||
RequestURL string `json:"request_url"`
|
||||
TransientPayload map[string]interface{} `json:"transient_payload"`
|
||||
ExpiresInMinutes int `json:"expires_in_minutes"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewRecoveryValid(d template.Dependencies, m *RecoveryValidModel) *RecoveryValid {
|
||||
return &RecoveryValid{d: d, m: m}
|
||||
}
|
||||
|
||||
func (t *RecoveryValid) EmailRecipient() (string, error) {
|
||||
return t.m.To, nil
|
||||
}
|
||||
|
||||
func (t *RecoveryValid) EmailSubject(ctx context.Context) (string, error) {
|
||||
subject, err := template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "recovery/valid/email.subject.gotmpl", "recovery/valid/email.subject*", t.m, t.d.CourierConfig().CourierTemplatesRecoveryValid(ctx).Subject)
|
||||
|
||||
return strings.TrimSpace(subject), err
|
||||
}
|
||||
|
||||
func (t *RecoveryValid) EmailBody(ctx context.Context) (string, error) {
|
||||
return template.LoadHTML(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "recovery/valid/email.body.gotmpl", "recovery/valid/email.body*", t.m, t.d.CourierConfig().CourierTemplatesRecoveryValid(ctx).Body.HTML)
|
||||
}
|
||||
|
||||
func (t *RecoveryValid) EmailBodyPlaintext(ctx context.Context) (string, error) {
|
||||
return template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "recovery/valid/email.body.plaintext.gotmpl", "recovery/valid/email.body.plaintext*", t.m, t.d.CourierConfig().CourierTemplatesRecoveryValid(ctx).Body.PlainText)
|
||||
}
|
||||
|
||||
func (t *RecoveryValid) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.m)
|
||||
}
|
||||
|
||||
func (t *RecoveryValid) TemplateType() template.TemplateType {
|
||||
return template.TypeRecoveryValid
|
||||
}
|
||||
30
courier/template/email/recovery_valid_test.go
Normal file
30
courier/template/email/recovery_valid_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
"github.com/ory/kratos/courier/template/testhelpers"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestRecoverValid(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("test=with courier templates directory", func(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
tpl := email.NewRecoveryValid(reg, &email.RecoveryValidModel{})
|
||||
|
||||
testhelpers.TestRendered(t, ctx, tpl)
|
||||
})
|
||||
|
||||
t.Run("test=with remote resources", func(t *testing.T) {
|
||||
testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/recovery/valid", template.TypeRecoveryValid)
|
||||
})
|
||||
}
|
||||
65
courier/template/email/registration_code_valid.go
Normal file
65
courier/template/email/registration_code_valid.go
Normal file
@@ -0,0 +1,65 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
RegistrationCodeValid struct {
|
||||
deps template.Dependencies
|
||||
model *RegistrationCodeValidModel
|
||||
}
|
||||
RegistrationCodeValidModel struct {
|
||||
To string `json:"to"`
|
||||
Traits map[string]any `json:"traits"`
|
||||
RegistrationCode string `json:"registration_code"`
|
||||
RequestURL string `json:"request_url"`
|
||||
TransientPayload map[string]any `json:"transient_payload"`
|
||||
ExpiresInMinutes int `json:"expires_in_minutes"`
|
||||
OAuth2LoginRequest *template.OAuth2LoginRequest `json:"oauth2_login_request,omitempty"`
|
||||
UserRequestHeaders http.Header `json:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewRegistrationCodeValid(d template.Dependencies, m *RegistrationCodeValidModel) *RegistrationCodeValid {
|
||||
return &RegistrationCodeValid{deps: d, model: m}
|
||||
}
|
||||
|
||||
func (t *RegistrationCodeValid) EmailRecipient() (string, error) {
|
||||
return t.model.To, nil
|
||||
}
|
||||
|
||||
func (t *RegistrationCodeValid) EmailSubject(ctx context.Context) (string, error) {
|
||||
subject, err := template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.subject.gotmpl", "registration_code/valid/email.subject*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Subject)
|
||||
|
||||
return strings.TrimSpace(subject), err
|
||||
}
|
||||
|
||||
func (t *RegistrationCodeValid) EmailBody(ctx context.Context) (string, error) {
|
||||
return template.LoadHTML(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.body.gotmpl", "registration_code/valid/email.body*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Body.HTML)
|
||||
}
|
||||
|
||||
func (t *RegistrationCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) {
|
||||
return template.LoadText(ctx, t.deps, os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)), "registration_code/valid/email.body.plaintext.gotmpl", "registration_code/valid/email.body.plaintext*", t.model, t.deps.CourierConfig().CourierTemplatesRegistrationCodeValid(ctx).Body.PlainText)
|
||||
}
|
||||
|
||||
func (t *RegistrationCodeValid) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.model)
|
||||
}
|
||||
|
||||
func (t *RegistrationCodeValid) TemplateType() template.TemplateType {
|
||||
return template.TypeRegistrationCodeValid
|
||||
}
|
||||
|
||||
func (t *RegistrationCodeValid) RequestHeaders() http.Header {
|
||||
return t.model.UserRequestHeaders
|
||||
}
|
||||
30
courier/template/email/registration_code_valid_test.go
Normal file
30
courier/template/email/registration_code_valid_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
"github.com/ory/kratos/courier/template/testhelpers"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestRegistrationCodeValid(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("test=with courier templates directory", func(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
tpl := email.NewRegistrationCodeValid(reg, &email.RegistrationCodeValidModel{})
|
||||
|
||||
testhelpers.TestRendered(t, ctx, tpl)
|
||||
})
|
||||
|
||||
t.Run("test=with remote resources", func(t *testing.T) {
|
||||
testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/registration_code/valid", template.TypeRegistrationCodeValid)
|
||||
})
|
||||
}
|
||||
41
courier/template/email/stub.go
Normal file
41
courier/template/email/stub.go
Normal file
@@ -0,0 +1,41 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
TestStub struct {
|
||||
m *TestStubModel
|
||||
}
|
||||
TestStubModel struct {
|
||||
To string `json:"to"`
|
||||
Subject string `json:"subject"`
|
||||
Body string `json:"body"`
|
||||
HTMLBody string `json:"html_body,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewTestStub(m *TestStubModel) *TestStub {
|
||||
return &TestStub{m: m}
|
||||
}
|
||||
|
||||
func (t *TestStub) EmailRecipient() (string, error) { return t.m.To, nil }
|
||||
func (t *TestStub) EmailSubject(context.Context) (string, error) { return t.m.Subject, nil }
|
||||
|
||||
func (t *TestStub) EmailBody(ctx context.Context) (string, error) {
|
||||
if t.m.HTMLBody != "" {
|
||||
return t.m.HTMLBody, nil
|
||||
}
|
||||
return t.EmailBodyPlaintext(ctx)
|
||||
}
|
||||
|
||||
func (t *TestStub) EmailBodyPlaintext(context.Context) (string, error) { return t.m.Body, nil }
|
||||
func (t *TestStub) MarshalJSON() ([]byte, error) { return json.Marshal(t.m) }
|
||||
func (t *TestStub) TemplateType() template.TemplateType { return template.TypeTestStub }
|
||||
55
courier/template/email/verifiable_address_changed.go
Normal file
55
courier/template/email/verifiable_address_changed.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
VerifiableAddressChanged struct {
|
||||
d template.Dependencies
|
||||
m *VerifiableAddressChangedModel
|
||||
}
|
||||
VerifiableAddressChangedModel struct {
|
||||
To string `json:"to"`
|
||||
Identity map[string]any `json:"identity"`
|
||||
ChangedAt string `json:"changed_at"`
|
||||
TransientPayload map[string]any `json:"transient_payload"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewVerifiableAddressChanged(d template.Dependencies, m *VerifiableAddressChangedModel) *VerifiableAddressChanged {
|
||||
return &VerifiableAddressChanged{d: d, m: m}
|
||||
}
|
||||
|
||||
func (t *VerifiableAddressChanged) EmailRecipient() (string, error) {
|
||||
return t.m.To, nil
|
||||
}
|
||||
|
||||
func (t *VerifiableAddressChanged) EmailSubject(ctx context.Context) (string, error) {
|
||||
subject, err := template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verifiable_address_changed/email.subject.gotmpl", "verifiable_address_changed/email.subject*", t.m, t.d.CourierConfig().CourierTemplatesVerifiableAddressChanged(ctx).Subject)
|
||||
return strings.TrimSpace(subject), err
|
||||
}
|
||||
|
||||
func (t *VerifiableAddressChanged) EmailBody(ctx context.Context) (string, error) {
|
||||
return template.LoadHTML(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verifiable_address_changed/email.body.gotmpl", "verifiable_address_changed/email.body*", t.m, t.d.CourierConfig().CourierTemplatesVerifiableAddressChanged(ctx).Body.HTML)
|
||||
}
|
||||
|
||||
func (t *VerifiableAddressChanged) EmailBodyPlaintext(ctx context.Context) (string, error) {
|
||||
return template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verifiable_address_changed/email.body.plaintext.gotmpl", "verifiable_address_changed/email.body.plaintext*", t.m, t.d.CourierConfig().CourierTemplatesVerifiableAddressChanged(ctx).Body.PlainText)
|
||||
}
|
||||
|
||||
func (t *VerifiableAddressChanged) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.m)
|
||||
}
|
||||
|
||||
func (t *VerifiableAddressChanged) TemplateType() template.TemplateType {
|
||||
return template.TypeVerifiableAddressChanged
|
||||
}
|
||||
44
courier/template/email/verifiable_address_changed_test.go
Normal file
44
courier/template/email/verifiable_address_changed_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestVerifiableAddressChanged(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
|
||||
tpl := email.NewVerifiableAddressChanged(reg, &email.VerifiableAddressChangedModel{
|
||||
To: "old@example.com",
|
||||
ChangedAt: "2026-04-21T12:00:00Z",
|
||||
Identity: map[string]any{"ID": "00000000-0000-0000-0000-000000000001", "Traits": map[string]any{"email": "new@example.com"}},
|
||||
})
|
||||
|
||||
recipient, err := tpl.EmailRecipient()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "old@example.com", recipient)
|
||||
|
||||
subject, err := tpl.EmailSubject(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, strings.ToLower(subject), "address")
|
||||
|
||||
body, err := tpl.EmailBody(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, body, "00000000-0000-0000-0000-000000000001")
|
||||
|
||||
plain, err := tpl.EmailBodyPlaintext(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, plain, "00000000-0000-0000-0000-000000000001")
|
||||
assert.Contains(t, plain, "2026-04-21T12:00:00Z")
|
||||
}
|
||||
79
courier/template/email/verification_code_invalid.go
Normal file
79
courier/template/email/verification_code_invalid.go
Normal file
@@ -0,0 +1,79 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
VerificationCodeInvalid struct {
|
||||
d template.Dependencies
|
||||
m *VerificationCodeInvalidModel
|
||||
}
|
||||
VerificationCodeInvalidModel struct {
|
||||
To string `json:"to"`
|
||||
RequestURL string `json:"request_url"`
|
||||
TransientPayload map[string]interface{} `json:"transient_payload"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewVerificationCodeInvalid(d template.Dependencies, m *VerificationCodeInvalidModel) *VerificationCodeInvalid {
|
||||
return &VerificationCodeInvalid{d: d, m: m}
|
||||
}
|
||||
|
||||
func (t *VerificationCodeInvalid) EmailRecipient() (string, error) {
|
||||
return t.m.To, nil
|
||||
}
|
||||
|
||||
func (t *VerificationCodeInvalid) EmailSubject(ctx context.Context) (string, error) {
|
||||
subject, err := template.LoadText(
|
||||
ctx,
|
||||
t.d,
|
||||
os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)),
|
||||
"verification_code/invalid/email.subject.gotmpl",
|
||||
"verification_code/invalid/email.subject*",
|
||||
t.m,
|
||||
t.d.CourierConfig().CourierTemplatesVerificationCodeInvalid(ctx).Subject,
|
||||
)
|
||||
|
||||
return strings.TrimSpace(subject), err
|
||||
}
|
||||
|
||||
func (t *VerificationCodeInvalid) EmailBody(ctx context.Context) (string, error) {
|
||||
return template.LoadHTML(
|
||||
ctx,
|
||||
t.d,
|
||||
os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)),
|
||||
"verification_code/invalid/email.body.gotmpl",
|
||||
"verification_code/invalid/email.body*",
|
||||
t.m,
|
||||
t.d.CourierConfig().CourierTemplatesVerificationCodeInvalid(ctx).Body.HTML,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *VerificationCodeInvalid) EmailBodyPlaintext(ctx context.Context) (string, error) {
|
||||
return template.LoadText(
|
||||
ctx,
|
||||
t.d,
|
||||
os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)),
|
||||
"verification_code/invalid/email.body.plaintext.gotmpl",
|
||||
"verification_code/invalid/email.body.plaintext*",
|
||||
t.m,
|
||||
t.d.CourierConfig().CourierTemplatesVerificationCodeInvalid(ctx).Body.PlainText,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *VerificationCodeInvalid) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.m)
|
||||
}
|
||||
|
||||
func (t *VerificationCodeInvalid) TemplateType() template.TemplateType {
|
||||
return template.TypeVerificationCodeInvalid
|
||||
}
|
||||
30
courier/template/email/verification_code_invalid_test.go
Normal file
30
courier/template/email/verification_code_invalid_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
"github.com/ory/kratos/courier/template/testhelpers"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestVerifyCodeInvalid(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("test=with courier templates directory", func(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
tpl := email.NewVerificationCodeInvalid(reg, &email.VerificationCodeInvalidModel{})
|
||||
|
||||
testhelpers.TestRendered(t, ctx, tpl)
|
||||
})
|
||||
|
||||
t.Run("test=with remote resources", func(t *testing.T) {
|
||||
testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification_code/invalid", template.TypeVerificationCodeInvalid)
|
||||
})
|
||||
}
|
||||
82
courier/template/email/verification_code_valid.go
Normal file
82
courier/template/email/verification_code_valid.go
Normal file
@@ -0,0 +1,82 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
VerificationCodeValid struct {
|
||||
d template.Dependencies
|
||||
m *VerificationCodeValidModel
|
||||
}
|
||||
VerificationCodeValidModel struct {
|
||||
To string `json:"to"`
|
||||
VerificationURL string `json:"verification_url"`
|
||||
VerificationCode string `json:"verification_code"`
|
||||
Identity map[string]any `json:"identity"`
|
||||
RequestURL string `json:"request_url"`
|
||||
TransientPayload map[string]any `json:"transient_payload"`
|
||||
ExpiresInMinutes int `json:"expires_in_minutes"`
|
||||
OAuth2LoginRequest *template.OAuth2LoginRequest `json:"oauth2_login_request,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewVerificationCodeValid(d template.Dependencies, m *VerificationCodeValidModel) *VerificationCodeValid {
|
||||
return &VerificationCodeValid{d: d, m: m}
|
||||
}
|
||||
|
||||
func (t *VerificationCodeValid) EmailRecipient() (string, error) {
|
||||
return t.m.To, nil
|
||||
}
|
||||
|
||||
func (t *VerificationCodeValid) EmailSubject(ctx context.Context) (string, error) {
|
||||
subject, err := template.LoadText(
|
||||
ctx,
|
||||
t.d,
|
||||
os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)),
|
||||
"verification_code/valid/email.subject.gotmpl",
|
||||
"verification_code/valid/email.subject*",
|
||||
t.m,
|
||||
t.d.CourierConfig().CourierTemplatesVerificationCodeValid(ctx).Subject,
|
||||
)
|
||||
|
||||
return strings.TrimSpace(subject), err
|
||||
}
|
||||
|
||||
func (t *VerificationCodeValid) EmailBody(ctx context.Context) (string, error) {
|
||||
return template.LoadHTML(ctx,
|
||||
t.d,
|
||||
os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)),
|
||||
"verification_code/valid/email.body.gotmpl",
|
||||
"verification_code/valid/email.body*",
|
||||
t.m,
|
||||
t.d.CourierConfig().CourierTemplatesVerificationCodeValid(ctx).Body.HTML,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *VerificationCodeValid) EmailBodyPlaintext(ctx context.Context) (string, error) {
|
||||
return template.LoadText(ctx,
|
||||
t.d,
|
||||
os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)),
|
||||
"verification_code/valid/email.body.plaintext.gotmpl",
|
||||
"verification_code/valid/email.body.plaintext*",
|
||||
t.m,
|
||||
t.d.CourierConfig().CourierTemplatesVerificationCodeValid(ctx).Body.PlainText,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *VerificationCodeValid) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.m)
|
||||
}
|
||||
|
||||
func (t *VerificationCodeValid) TemplateType() template.TemplateType {
|
||||
return template.TypeVerificationCodeValid
|
||||
}
|
||||
30
courier/template/email/verification_code_valid_test.go
Normal file
30
courier/template/email/verification_code_valid_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
"github.com/ory/kratos/courier/template/testhelpers"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestVerifyCodeValid(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("test=with courier templates directory", func(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
tpl := email.NewVerificationCodeValid(reg, &email.VerificationCodeValidModel{})
|
||||
|
||||
testhelpers.TestRendered(t, ctx, tpl)
|
||||
})
|
||||
|
||||
t.Run("test=with remote resources", func(t *testing.T) {
|
||||
testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification_code/valid", template.TypeVerificationCodeValid)
|
||||
})
|
||||
}
|
||||
55
courier/template/email/verification_invalid.go
Normal file
55
courier/template/email/verification_invalid.go
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
VerificationInvalid struct {
|
||||
d template.Dependencies
|
||||
m *VerificationInvalidModel
|
||||
}
|
||||
VerificationInvalidModel struct {
|
||||
To string `json:"to"`
|
||||
RequestURL string `json:"request_url"`
|
||||
TransientPayload map[string]interface{} `json:"transient_payload"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewVerificationInvalid(d template.Dependencies, m *VerificationInvalidModel) *VerificationInvalid {
|
||||
return &VerificationInvalid{d: d, m: m}
|
||||
}
|
||||
|
||||
func (t *VerificationInvalid) EmailRecipient() (string, error) {
|
||||
return t.m.To, nil
|
||||
}
|
||||
|
||||
func (t *VerificationInvalid) EmailSubject(ctx context.Context) (string, error) {
|
||||
subject, err := template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verification/invalid/email.subject.gotmpl", "verification/invalid/email.subject*", t.m, t.d.CourierConfig().CourierTemplatesVerificationInvalid(ctx).Subject)
|
||||
|
||||
return strings.TrimSpace(subject), err
|
||||
}
|
||||
|
||||
func (t *VerificationInvalid) EmailBody(ctx context.Context) (string, error) {
|
||||
return template.LoadHTML(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verification/invalid/email.body.gotmpl", "verification/invalid/email.body*", t.m, t.d.CourierConfig().CourierTemplatesVerificationInvalid(ctx).Body.HTML)
|
||||
}
|
||||
|
||||
func (t *VerificationInvalid) EmailBodyPlaintext(ctx context.Context) (string, error) {
|
||||
return template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verification/invalid/email.body.plaintext.gotmpl", "verification/invalid/email.body.plaintext*", t.m, t.d.CourierConfig().CourierTemplatesVerificationInvalid(ctx).Body.PlainText)
|
||||
}
|
||||
|
||||
func (t *VerificationInvalid) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.m)
|
||||
}
|
||||
|
||||
func (t *VerificationInvalid) TemplateType() template.TemplateType {
|
||||
return template.TypeVerificationInvalid
|
||||
}
|
||||
32
courier/template/email/verification_invalid_test.go
Normal file
32
courier/template/email/verification_invalid_test.go
Normal file
@@ -0,0 +1,32 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
"github.com/ory/kratos/courier/template/testhelpers"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestVerifyInvalid(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("test=with courier templates directory", func(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
tpl := email.NewVerificationInvalid(reg, &email.VerificationInvalidModel{})
|
||||
|
||||
testhelpers.TestRendered(t, ctx, tpl)
|
||||
})
|
||||
|
||||
t.Run("test=with remote resources", func(t *testing.T) {
|
||||
t.Run("test=with remote resources", func(t *testing.T) {
|
||||
testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification/invalid", template.TypeVerificationInvalid)
|
||||
})
|
||||
})
|
||||
}
|
||||
59
courier/template/email/verification_valid.go
Normal file
59
courier/template/email/verification_valid.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
VerificationValid struct {
|
||||
d template.Dependencies
|
||||
m *VerificationValidModel
|
||||
}
|
||||
VerificationValidModel struct {
|
||||
To string `json:"to"`
|
||||
VerificationURL string `json:"verification_url"`
|
||||
Identity map[string]any `json:"identity"`
|
||||
RequestURL string `json:"request_url"`
|
||||
TransientPayload map[string]any `json:"transient_payload"`
|
||||
ExpiresInMinutes int `json:"expires_in_minutes"`
|
||||
OAuth2LoginRequest *template.OAuth2LoginRequest `json:"oauth2_login_request,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewVerificationValid(d template.Dependencies, m *VerificationValidModel) *VerificationValid {
|
||||
return &VerificationValid{d: d, m: m}
|
||||
}
|
||||
|
||||
func (t *VerificationValid) EmailRecipient() (string, error) {
|
||||
return t.m.To, nil
|
||||
}
|
||||
|
||||
func (t *VerificationValid) EmailSubject(ctx context.Context) (string, error) {
|
||||
subject, err := template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verification/valid/email.subject.gotmpl", "verification/valid/email.subject*", t.m, t.d.CourierConfig().CourierTemplatesVerificationValid(ctx).Subject)
|
||||
|
||||
return strings.TrimSpace(subject), err
|
||||
}
|
||||
|
||||
func (t *VerificationValid) EmailBody(ctx context.Context) (string, error) {
|
||||
return template.LoadHTML(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verification/valid/email.body.gotmpl", "verification/valid/email.body*", t.m, t.d.CourierConfig().CourierTemplatesVerificationValid(ctx).Body.HTML)
|
||||
}
|
||||
|
||||
func (t *VerificationValid) EmailBodyPlaintext(ctx context.Context) (string, error) {
|
||||
return template.LoadText(ctx, t.d, os.DirFS(t.d.CourierConfig().CourierTemplatesRoot(ctx)), "verification/valid/email.body.plaintext.gotmpl", "verification/valid/email.body.plaintext*", t.m, t.d.CourierConfig().CourierTemplatesVerificationValid(ctx).Body.PlainText)
|
||||
}
|
||||
|
||||
func (t *VerificationValid) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.m)
|
||||
}
|
||||
|
||||
func (t *VerificationValid) TemplateType() template.TemplateType {
|
||||
return template.TypeVerificationValid
|
||||
}
|
||||
30
courier/template/email/verification_valid_test.go
Normal file
30
courier/template/email/verification_valid_test.go
Normal file
@@ -0,0 +1,30 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package email_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
"github.com/ory/kratos/courier/template/testhelpers"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestVerifyValid(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("test=with courier templates directory", func(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
tpl := email.NewVerificationValid(reg, &email.VerificationValidModel{})
|
||||
|
||||
testhelpers.TestRendered(t, ctx, tpl)
|
||||
})
|
||||
|
||||
t.Run("test=with remote resources", func(t *testing.T) {
|
||||
testhelpers.TestRemoteTemplates(t, "../courier/builtin/templates/verification/valid", template.TypeVerificationValid)
|
||||
})
|
||||
}
|
||||
190
courier/template/load_template.go
Normal file
190
courier/template/load_template.go
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
htemplate "html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"text/template"
|
||||
|
||||
"github.com/ory/x/fetcher"
|
||||
"github.com/ory/x/httpx"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
//go:embed courier/builtin/templates/*
|
||||
var templates embed.FS
|
||||
|
||||
var Cache, _ = lru.New[string, Template](16)
|
||||
|
||||
type Template interface {
|
||||
Execute(wr io.Writer, data interface{}) error
|
||||
}
|
||||
|
||||
type templateDependencies interface {
|
||||
httpx.ClientProvider
|
||||
}
|
||||
|
||||
func loadBuiltInTemplate(filesystem fs.FS, name string, html bool) (Template, error) {
|
||||
if t, found := Cache.Get(name); found {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
file, err := filesystem.Open(name)
|
||||
if err != nil {
|
||||
// try to fallback to bundled templates
|
||||
var fallbackErr error
|
||||
file, fallbackErr = templates.Open(filepath.Join("courier/builtin/templates", name))
|
||||
if fallbackErr != nil {
|
||||
// return original error from os.DirFS
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
|
||||
defer func() { _ = file.Close() }()
|
||||
|
||||
var b bytes.Buffer
|
||||
if _, err := io.Copy(&b, file); err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
|
||||
var tpl Template
|
||||
if html {
|
||||
t, err := htemplate.New(name).Funcs(sprig.HtmlFuncMap()).Parse(b.String())
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
tpl = t
|
||||
} else {
|
||||
t, err := template.New(name).Funcs(sprig.TxtFuncMap()).Parse(b.String())
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
tpl = t
|
||||
}
|
||||
|
||||
_ = Cache.Add(name, tpl)
|
||||
return tpl, nil
|
||||
}
|
||||
|
||||
func loadRemoteTemplate(ctx context.Context, d templateDependencies, url string, html bool) (t Template, err error) {
|
||||
if t, found := Cache.Get(url); found {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
f := fetcher.NewFetcher(fetcher.WithClient(d.HTTPClient(ctx)))
|
||||
bb, err := f.FetchContext(ctx, url)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
b := bb.Bytes()
|
||||
|
||||
if html {
|
||||
t, err = htemplate.New(url).Funcs(sprig.HermeticHtmlFuncMap()).Parse(string(b))
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
} else {
|
||||
t, err = template.New(url).Funcs(sprig.HermeticTxtFuncMap()).Parse(string(b))
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
}
|
||||
Cache.Add(url, t)
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func loadTemplate(filesystem fs.FS, name, pattern string, html bool) (Template, error) {
|
||||
if t, found := Cache.Get(name); found {
|
||||
return t, nil
|
||||
}
|
||||
|
||||
matches, _ := fs.Glob(filesystem, name)
|
||||
// make sure the file exists in the fs, otherwise fallback to built in templates
|
||||
if matches == nil {
|
||||
return loadBuiltInTemplate(filesystem, name, html)
|
||||
}
|
||||
|
||||
glob := name
|
||||
if pattern != "" {
|
||||
// pattern matching is used when we have more than one gotmpl for different use cases, such as i18n support
|
||||
// e.g. some_template/template_name* will match some_template/template_name.body.en_US.gotmpl
|
||||
matches, _ = fs.Glob(filesystem, pattern)
|
||||
// set the glob string to match patterns
|
||||
if matches != nil {
|
||||
glob = pattern
|
||||
}
|
||||
}
|
||||
|
||||
var tpl Template
|
||||
if html {
|
||||
t, err := htemplate.New(filepath.Base(name)).Funcs(sprig.HermeticHtmlFuncMap()).ParseFS(filesystem, glob)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
tpl = t
|
||||
} else {
|
||||
t, err := template.New(filepath.Base(name)).Funcs(sprig.HermeticTxtFuncMap()).ParseFS(filesystem, glob)
|
||||
if err != nil {
|
||||
return nil, errors.WithStack(err)
|
||||
}
|
||||
tpl = t
|
||||
}
|
||||
|
||||
_ = Cache.Add(name, tpl)
|
||||
return tpl, nil
|
||||
}
|
||||
|
||||
func LoadText(ctx context.Context, d templateDependencies, filesystem fs.FS, name, pattern string, model interface{}, remoteURL string) (string, error) {
|
||||
var t Template
|
||||
var err error
|
||||
if remoteURL != "" {
|
||||
t, err = loadRemoteTemplate(ctx, d, remoteURL, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
t, err = loadTemplate(filesystem, name, pattern, false)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := t.Execute(&b, model); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func LoadHTML(ctx context.Context, d templateDependencies, filesystem fs.FS, name, pattern string, model interface{}, remoteURL string) (string, error) {
|
||||
var t Template
|
||||
var err error
|
||||
if remoteURL != "" {
|
||||
t, err = loadRemoteTemplate(ctx, d, remoteURL, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
t, err = loadTemplate(filesystem, name, pattern, true)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
if err := t.Execute(&b, model); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
199
courier/template/load_template_test.go
Normal file
199
courier/template/load_template_test.go
Normal file
@@ -0,0 +1,199 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package template_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/pkg"
|
||||
"github.com/ory/x/fetcher"
|
||||
|
||||
lru "github.com/hashicorp/golang-lru/v2"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/x"
|
||||
)
|
||||
|
||||
func TestLoadTextTemplate(t *testing.T) {
|
||||
executeTextTemplate := func(t *testing.T, dir, name, pattern string, model map[string]interface{}) string {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
tp, err := template.LoadText(t.Context(), reg, os.DirFS(dir), name, pattern, model, "")
|
||||
require.NoError(t, err)
|
||||
return tp
|
||||
}
|
||||
|
||||
executeHTMLTemplate := func(t *testing.T, dir, name, pattern string, model map[string]interface{}) string {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
tp, err := template.LoadHTML(t.Context(), reg, os.DirFS(dir), name, pattern, model, "")
|
||||
require.NoError(t, err)
|
||||
return tp
|
||||
}
|
||||
|
||||
t.Run("method=from bundled", func(t *testing.T) {
|
||||
actual := executeTextTemplate(t, "courier/builtin/templates/test_stub", "email.body.gotmpl", "", nil)
|
||||
assert.Contains(t, actual, "stub email")
|
||||
})
|
||||
|
||||
t.Run("method=fallback to bundled", func(t *testing.T) {
|
||||
template.Cache, _ = lru.New[string, template.Template](16) // prevent Cache hit
|
||||
actual := executeTextTemplate(t, "some/inexistent/dir", "test_stub/email.body.gotmpl", "", nil)
|
||||
assert.Contains(t, actual, "stub email")
|
||||
})
|
||||
|
||||
t.Run("method=with Sprig functions", func(t *testing.T) {
|
||||
template.Cache, _ = lru.New[string, template.Template](16) // prevent Cache hit
|
||||
m := map[string]interface{}{"input": "hello world"} // create a simple model
|
||||
actual := executeTextTemplate(t, "courier/builtin/templates/test_stub", "email.body.sprig.gotmpl", "", m)
|
||||
assert.Contains(t, actual, "HelloWorld,HELLOWORLD")
|
||||
})
|
||||
|
||||
t.Run("method=sprig should not support non-hermetic", func(t *testing.T) {
|
||||
template.Cache, _ = lru.New[string, template.Template](16)
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
|
||||
nonhermetic := []string{"date", "date_in_zone", "date_modify", "now", "htmlDate", "htmlDateInZone", "dateInZone", "dateModify", "env", "expandenv", "getHostByName", "uuidv4", "randNumeric", "randAscii", "randAlpha", "randAlphaNum"}
|
||||
|
||||
for _, tc := range nonhermetic {
|
||||
t.Run("case=should not support function: "+tc, func(t *testing.T) {
|
||||
_, err := template.LoadText(t.Context(), reg, x.NewStubFS(tc, []byte(fmt.Sprintf("{{ %s }}", tc))), tc, "", map[string]interface{}{}, "")
|
||||
assert.ErrorContains(t, err, fmt.Sprintf("function %q not defined", tc))
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("method=html with nested templates", func(t *testing.T) {
|
||||
template.Cache, _ = lru.New[string, template.Template](16) // prevent Cache hit
|
||||
m := map[string]interface{}{"lang": "en_US"} // create a simple model
|
||||
actual := executeHTMLTemplate(t, "courier/builtin/templates/test_stub", "email.body.html.gotmpl", "email.body.html*", m)
|
||||
assert.Contains(t, actual, "lang=en_US")
|
||||
})
|
||||
|
||||
t.Run("method=Cache works", func(t *testing.T) {
|
||||
dir := os.TempDir()
|
||||
name := x.NewUUID().String() + ".body.gotmpl"
|
||||
fp := filepath.Join(dir, name)
|
||||
|
||||
require.NoError(t, os.WriteFile(fp, []byte("cached stub body"), 0o600))
|
||||
assert.Contains(t, executeTextTemplate(t, dir, name, "", nil), "cached stub body")
|
||||
|
||||
require.NoError(t, os.RemoveAll(fp))
|
||||
assert.Contains(t, executeTextTemplate(t, dir, name, "", nil), "cached stub body")
|
||||
})
|
||||
|
||||
t.Run("method=remote resource", func(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
t.Run("case=base64 encoded data", func(t *testing.T) {
|
||||
t.Run("html template", func(t *testing.T) {
|
||||
m := map[string]interface{}{"lang": "en_US"}
|
||||
f, err := os.ReadFile("courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl")
|
||||
require.NoError(t, err)
|
||||
b64 := base64.StdEncoding.EncodeToString(f)
|
||||
tp, err := template.LoadHTML(ctx, reg, nil, "", "", m, "base64://"+b64)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, tp, "lang=en_US")
|
||||
})
|
||||
|
||||
t.Run("case=plaintext", func(t *testing.T) {
|
||||
m := map[string]interface{}{"Body": "something"}
|
||||
f, err := os.ReadFile("courier/builtin/templates/test_stub/email.body.plaintext.gotmpl")
|
||||
require.NoError(t, err)
|
||||
|
||||
b64 := base64.StdEncoding.EncodeToString(f)
|
||||
|
||||
tp, err := template.LoadText(ctx, reg, nil, "", "", m, "base64://"+b64)
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, tp, "stub email body something")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("case=file resource", func(t *testing.T) {
|
||||
t.Run("case=html template", func(t *testing.T) {
|
||||
m := map[string]interface{}{"lang": "en_US"}
|
||||
tp, err := template.LoadHTML(ctx, reg, nil, "", "", m, "file://courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, tp, "lang=en_US")
|
||||
})
|
||||
|
||||
t.Run("case=plaintext", func(t *testing.T) {
|
||||
m := map[string]interface{}{"Body": "something"}
|
||||
tp, err := template.LoadText(ctx, reg, nil, "", "", m, "file://courier/builtin/templates/test_stub/email.body.plaintext.gotmpl")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, tp, "stub email body something")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("case=http resource", func(t *testing.T) {
|
||||
router := http.NewServeMux()
|
||||
router.HandleFunc("GET /html", func(writer http.ResponseWriter, request *http.Request) {
|
||||
http.ServeFile(writer, request, "courier/builtin/templates/test_stub/email.body.html.en_US.gotmpl")
|
||||
})
|
||||
router.HandleFunc("GET /plaintext", func(writer http.ResponseWriter, request *http.Request) {
|
||||
http.ServeFile(writer, request, "courier/builtin/templates/test_stub/email.body.plaintext.gotmpl")
|
||||
})
|
||||
ts := httptest.NewServer(router)
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
t.Run("case=html template", func(t *testing.T) {
|
||||
m := map[string]interface{}{"lang": "en_US"}
|
||||
tp, err := template.LoadHTML(ctx, reg, nil, "", "", m, ts.URL+"/html")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, tp, "lang=en_US")
|
||||
})
|
||||
|
||||
t.Run("case=plaintext", func(t *testing.T) {
|
||||
m := map[string]interface{}{"Body": "something"}
|
||||
tp, err := template.LoadText(ctx, reg, nil, "", "", m, ts.URL+"/plaintext")
|
||||
require.NoError(t, err)
|
||||
assert.Contains(t, tp, "stub email body something")
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("case=unsupported resource", func(t *testing.T) {
|
||||
tp, err := template.LoadHTML(ctx, reg, nil, "", "", map[string]interface{}{}, "grpc://unsupported-url")
|
||||
require.ErrorIs(t, err, fetcher.ErrUnknownScheme)
|
||||
require.Empty(t, tp)
|
||||
|
||||
tp, err = template.LoadText(ctx, reg, nil, "", "", map[string]interface{}{}, "grpc://unsupported-url")
|
||||
require.ErrorIs(t, err, fetcher.ErrUnknownScheme)
|
||||
require.Empty(t, tp)
|
||||
})
|
||||
|
||||
t.Run("case=disallowed resources", func(t *testing.T) {
|
||||
require.NoError(t, reg.Config().Set(ctx, config.ViperKeyClientHTTPNoPrivateIPRanges, true))
|
||||
reg.HTTPClient(ctx).RetryMax = 1
|
||||
reg.HTTPClient(ctx).RetryWaitMax = time.Millisecond
|
||||
|
||||
_, err := template.LoadHTML(ctx, reg, nil, "", "", map[string]interface{}{}, "http://localhost:8080/1234")
|
||||
assert.ErrorContains(t, err, "no such host")
|
||||
|
||||
_, err = template.LoadText(ctx, reg, nil, "", "", map[string]interface{}{}, "http://localhost:8080/1234")
|
||||
assert.ErrorContains(t, err, "no such host")
|
||||
})
|
||||
|
||||
t.Run("method=cache works", func(t *testing.T) {
|
||||
tp1, err := template.LoadText(ctx, reg, nil, "", "", map[string]interface{}{}, "base64://e3sgJGwgOj0gY2F0ICJsYW5nPSIgLmxhbmcgfX0Ke3sgbm9zcGFjZSAkbCB9fQ==")
|
||||
require.NoError(t, err)
|
||||
|
||||
tp2, err := template.LoadText(ctx, reg, nil, "", "", map[string]interface{}{}, "base64://c3R1YiBlbWFpbCBib2R5IHt7IC5Cb2R5IH19")
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.NotEqualf(t, tp1, tp2, "Expected remote template 1 and remote template 2 to not be equal")
|
||||
})
|
||||
})
|
||||
}
|
||||
48
courier/template/oauth2.go
Normal file
48
courier/template/oauth2.go
Normal file
@@ -0,0 +1,48 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
hydraclientgo "github.com/ory/hydra-client-go/v2"
|
||||
)
|
||||
|
||||
type (
|
||||
// OAuth2LoginRequest is a scoped view of the OAuth2 login request that
|
||||
// initiated a self-service flow. It is embedded into courier template data
|
||||
// so that messages can be branded per OAuth2 client without exposing the
|
||||
// full flow object.
|
||||
OAuth2LoginRequest struct {
|
||||
Challenge string `json:"challenge"`
|
||||
Client OAuth2Client `json:"client"`
|
||||
}
|
||||
|
||||
// OAuth2Client contains the branding-relevant, admin-controlled fields of
|
||||
// the OAuth2 client that initiated a self-service flow.
|
||||
OAuth2Client struct {
|
||||
ClientID string `json:"client_id"`
|
||||
ClientName string `json:"client_name,omitempty"`
|
||||
ClientURI string `json:"client_uri,omitempty"`
|
||||
LogoURI string `json:"logo_uri,omitempty"`
|
||||
Metadata any `json:"metadata,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
// NewOAuth2LoginRequest maps the Hydra login request to the scoped view embedded
|
||||
// into courier template data. Only branding-relevant, admin-controlled client
|
||||
// fields are exposed to messaging channels.
|
||||
func NewOAuth2LoginRequest(hlr *hydraclientgo.OAuth2LoginRequest) *OAuth2LoginRequest {
|
||||
if hlr == nil {
|
||||
return nil
|
||||
}
|
||||
return &OAuth2LoginRequest{
|
||||
Challenge: hlr.Challenge,
|
||||
Client: OAuth2Client{
|
||||
ClientID: hlr.Client.GetClientId(),
|
||||
ClientName: hlr.Client.GetClientName(),
|
||||
ClientURI: hlr.Client.GetClientUri(),
|
||||
LogoURI: hlr.Client.GetLogoUri(),
|
||||
Metadata: hlr.Client.GetMetadata(),
|
||||
},
|
||||
}
|
||||
}
|
||||
59
courier/template/sms/authenticator_key_added.go
Normal file
59
courier/template/sms/authenticator_key_added.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
AuthenticatorKeyAdded struct {
|
||||
deps template.Dependencies
|
||||
model *AuthenticatorKeyAddedModel
|
||||
}
|
||||
AuthenticatorKeyAddedModel struct {
|
||||
To string `json:"to"`
|
||||
Identity map[string]any `json:"identity"`
|
||||
AddedAt string `json:"added_at"`
|
||||
TransientPayload map[string]any `json:"transient_payload"`
|
||||
UserRequestHeaders http.Header `json:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewAuthenticatorKeyAdded(d template.Dependencies, m *AuthenticatorKeyAddedModel) *AuthenticatorKeyAdded {
|
||||
return &AuthenticatorKeyAdded{deps: d, model: m}
|
||||
}
|
||||
|
||||
func (t *AuthenticatorKeyAdded) PhoneNumber() (string, error) {
|
||||
return t.model.To, nil
|
||||
}
|
||||
|
||||
func (t *AuthenticatorKeyAdded) SMSBody(ctx context.Context) (string, error) {
|
||||
return template.LoadText(
|
||||
ctx,
|
||||
t.deps,
|
||||
os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)),
|
||||
"authenticator_key_added/sms.body.gotmpl",
|
||||
"authenticator_key_added/sms.body*",
|
||||
t.model,
|
||||
t.deps.CourierConfig().CourierSMSTemplatesAuthenticatorKeyAdded(ctx).Body.PlainText,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *AuthenticatorKeyAdded) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.model)
|
||||
}
|
||||
|
||||
func (t *AuthenticatorKeyAdded) TemplateType() template.TemplateType {
|
||||
return template.TypeAuthenticatorKeyAdded
|
||||
}
|
||||
|
||||
func (t *AuthenticatorKeyAdded) RequestHeaders() http.Header {
|
||||
return t.model.UserRequestHeaders
|
||||
}
|
||||
36
courier/template/sms/authenticator_key_added_test.go
Normal file
36
courier/template/sms/authenticator_key_added_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sms_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/courier/template/sms"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestAuthenticatorKeyAddedSMS(t *testing.T) {
|
||||
ctx := t.Context()
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
|
||||
tpl := sms.NewAuthenticatorKeyAdded(reg, &sms.AuthenticatorKeyAddedModel{
|
||||
To: "+15551234567",
|
||||
AddedAt: "2026-04-21T12:00:00Z",
|
||||
Identity: map[string]any{"ID": "00000000-0000-0000-0000-000000000001"},
|
||||
})
|
||||
|
||||
phone, err := tpl.PhoneNumber()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "+15551234567", phone)
|
||||
|
||||
body, err := tpl.SMSBody(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Must stay under 160 chars so we do not fragment SMS billing.
|
||||
assert.LessOrEqual(t, len(body), 160, "SMS body too long: %q", body)
|
||||
assert.Contains(t, body, "sign-in method")
|
||||
}
|
||||
63
courier/template/sms/login_code_valid.go
Normal file
63
courier/template/sms/login_code_valid.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
LoginCodeValid struct {
|
||||
deps template.Dependencies
|
||||
model *LoginCodeValidModel
|
||||
}
|
||||
LoginCodeValidModel struct {
|
||||
To string `json:"to"`
|
||||
LoginCode string `json:"login_code"`
|
||||
Identity map[string]any `json:"identity"`
|
||||
RequestURL string `json:"request_url"`
|
||||
RequestURLDomain string `json:"request_url_domain"`
|
||||
TransientPayload map[string]any `json:"transient_payload"`
|
||||
ExpiresInMinutes int `json:"expires_in_minutes"`
|
||||
OAuth2LoginRequest *template.OAuth2LoginRequest `json:"oauth2_login_request,omitempty"`
|
||||
UserRequestHeaders http.Header `json:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewLoginCodeValid(d template.Dependencies, m *LoginCodeValidModel) *LoginCodeValid {
|
||||
return &LoginCodeValid{deps: d, model: m}
|
||||
}
|
||||
|
||||
func (t *LoginCodeValid) PhoneNumber() (string, error) {
|
||||
return t.model.To, nil
|
||||
}
|
||||
|
||||
func (t *LoginCodeValid) SMSBody(ctx context.Context) (string, error) {
|
||||
return template.LoadText(
|
||||
ctx,
|
||||
t.deps,
|
||||
os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)),
|
||||
"login_code/valid/sms.body.gotmpl",
|
||||
"login_code/valid/sms.body*",
|
||||
t.model,
|
||||
t.deps.CourierConfig().CourierSMSTemplatesLoginCodeValid(ctx).Body.PlainText,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *LoginCodeValid) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.model)
|
||||
}
|
||||
|
||||
func (t *LoginCodeValid) TemplateType() template.TemplateType {
|
||||
return template.TypeLoginCodeValid
|
||||
}
|
||||
|
||||
func (t *LoginCodeValid) RequestHeaders() http.Header {
|
||||
return t.model.UserRequestHeaders
|
||||
}
|
||||
36
courier/template/sms/login_code_valid_test.go
Normal file
36
courier/template/sms/login_code_valid_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sms_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/courier/template/sms"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestNewLoginCodeValid(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
|
||||
const (
|
||||
expectedPhone = "+12345678901"
|
||||
otp = "012345"
|
||||
)
|
||||
|
||||
tpl := sms.NewLoginCodeValid(reg, &sms.LoginCodeValidModel{To: expectedPhone, LoginCode: otp})
|
||||
|
||||
expectedBody := "Your login code is: 012345\n\nIt expires in 0 minutes.\n"
|
||||
|
||||
actualBody, err := tpl.SMSBody(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedBody, actualBody)
|
||||
|
||||
actualPhone, err := tpl.PhoneNumber()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedPhone, actualPhone)
|
||||
}
|
||||
43
courier/template/sms/origin_bound_test.go
Normal file
43
courier/template/sms/origin_bound_test.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sms_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/courier/template/sms"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
// TestOriginBoundSMSBodies asserts that the login, registration, and
|
||||
// verification code SMS bodies include the Web OTP origin-bound last line
|
||||
// `@<domain> #<code>`, mirroring the recovery code template.
|
||||
func TestOriginBoundSMSBodies(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
|
||||
t.Run("template=login_code", func(t *testing.T) {
|
||||
body, err := sms.NewLoginCodeValid(reg, &sms.LoginCodeValidModel{LoginCode: "012345", RequestURLDomain: "auth.example.com"}).SMSBody(t.Context())
|
||||
require.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(strings.TrimRight(body, "\n"), "@auth.example.com #012345"),
|
||||
"body must end with the origin-bound line, got: %q", body)
|
||||
})
|
||||
|
||||
t.Run("template=registration_code", func(t *testing.T) {
|
||||
body, err := sms.NewRegistrationCodeValid(reg, &sms.RegistrationCodeValidModel{RegistrationCode: "012345", RequestURLDomain: "auth.example.com"}).SMSBody(t.Context())
|
||||
require.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(strings.TrimRight(body, "\n"), "@auth.example.com #012345"),
|
||||
"body must end with the origin-bound line, got: %q", body)
|
||||
})
|
||||
|
||||
t.Run("template=verification_code", func(t *testing.T) {
|
||||
body, err := sms.NewVerificationCodeValid(reg, &sms.VerificationCodeValidModel{VerificationCode: "012345", RequestURLDomain: "auth.example.com"}).SMSBody(t.Context())
|
||||
require.NoError(t, err)
|
||||
assert.True(t, strings.HasSuffix(strings.TrimRight(body, "\n"), "@auth.example.com #012345"),
|
||||
"body must end with the origin-bound line, got: %q", body)
|
||||
})
|
||||
}
|
||||
62
courier/template/sms/recovery_code.go
Normal file
62
courier/template/sms/recovery_code.go
Normal file
@@ -0,0 +1,62 @@
|
||||
// Copyright © 2025 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
RecoveryCodeValid struct {
|
||||
deps template.Dependencies
|
||||
model *RecoveryCodeValidModel
|
||||
}
|
||||
|
||||
RecoveryCodeValidModel struct {
|
||||
To string `json:"to"`
|
||||
RecoveryCode string `json:"verification_code"`
|
||||
Identity map[string]any `json:"identity"`
|
||||
RequestURL string `json:"request_url"`
|
||||
RequestURLDomain string `json:"request_url_domain"`
|
||||
TransientPayload map[string]any `json:"transient_payload"`
|
||||
ExpiresInMinutes int `json:"expires_in_minutes"`
|
||||
UserRequestHeaders http.Header `json:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewRecoveryCodeValid(d template.Dependencies, m *RecoveryCodeValidModel) *RecoveryCodeValid {
|
||||
return &RecoveryCodeValid{deps: d, model: m}
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeValid) PhoneNumber() (string, error) {
|
||||
return t.model.To, nil
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeValid) SMSBody(ctx context.Context) (string, error) {
|
||||
return template.LoadText(
|
||||
ctx,
|
||||
t.deps,
|
||||
os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)),
|
||||
"recovery_code/valid/sms.body.gotmpl",
|
||||
"recovery_code/valid/sms.body*",
|
||||
t.model,
|
||||
t.deps.CourierConfig().CourierSMSTemplatesRecoveryCodeValid(ctx).Body.PlainText,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *RecoveryCodeValid) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.model)
|
||||
}
|
||||
func (t *RecoveryCodeValid) TemplateType() template.TemplateType {
|
||||
return template.TypeRecoveryCodeValid
|
||||
}
|
||||
func (t *RecoveryCodeValid) RequestHeaders() http.Header {
|
||||
return t.model.UserRequestHeaders
|
||||
|
||||
}
|
||||
63
courier/template/sms/registration_code_valid.go
Normal file
63
courier/template/sms/registration_code_valid.go
Normal file
@@ -0,0 +1,63 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
RegistrationCodeValid struct {
|
||||
deps template.Dependencies
|
||||
model *RegistrationCodeValidModel
|
||||
}
|
||||
RegistrationCodeValidModel struct {
|
||||
To string `json:"to"`
|
||||
RegistrationCode string `json:"registration_code"`
|
||||
Identity map[string]any `json:"identity"`
|
||||
RequestURL string `json:"request_url"`
|
||||
RequestURLDomain string `json:"request_url_domain"`
|
||||
TransientPayload map[string]any `json:"transient_payload"`
|
||||
ExpiresInMinutes int `json:"expires_in_minutes"`
|
||||
OAuth2LoginRequest *template.OAuth2LoginRequest `json:"oauth2_login_request,omitempty"`
|
||||
UserRequestHeaders http.Header `json:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewRegistrationCodeValid(d template.Dependencies, m *RegistrationCodeValidModel) *RegistrationCodeValid {
|
||||
return &RegistrationCodeValid{deps: d, model: m}
|
||||
}
|
||||
|
||||
func (t *RegistrationCodeValid) PhoneNumber() (string, error) {
|
||||
return t.model.To, nil
|
||||
}
|
||||
|
||||
func (t *RegistrationCodeValid) SMSBody(ctx context.Context) (string, error) {
|
||||
return template.LoadText(
|
||||
ctx,
|
||||
t.deps,
|
||||
os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)),
|
||||
"registration_code/valid/sms.body.gotmpl",
|
||||
"registration_code/valid/sms.body*",
|
||||
t.model,
|
||||
t.deps.CourierConfig().CourierSMSTemplatesRegistrationCodeValid(ctx).Body.PlainText,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *RegistrationCodeValid) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.model)
|
||||
}
|
||||
|
||||
func (t *RegistrationCodeValid) TemplateType() template.TemplateType {
|
||||
return template.TypeRegistrationCodeValid
|
||||
}
|
||||
|
||||
func (t *RegistrationCodeValid) RequestHeaders() http.Header {
|
||||
return t.model.UserRequestHeaders
|
||||
}
|
||||
36
courier/template/sms/registration_code_valid_test.go
Normal file
36
courier/template/sms/registration_code_valid_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sms_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/courier/template/sms"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestNewRegistrationCodeValid(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
|
||||
const (
|
||||
expectedPhone = "+12345678901"
|
||||
otp = "012345"
|
||||
)
|
||||
|
||||
tpl := sms.NewRegistrationCodeValid(reg, &sms.RegistrationCodeValidModel{To: expectedPhone, RegistrationCode: otp})
|
||||
|
||||
expectedBody := "Your registration code is: 012345\n\nIt expires in 0 minutes.\n"
|
||||
|
||||
actualBody, err := tpl.SMSBody(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedBody, actualBody)
|
||||
|
||||
actualPhone, err := tpl.PhoneNumber()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedPhone, actualPhone)
|
||||
}
|
||||
27
courier/template/sms/stub.go
Normal file
27
courier/template/sms/stub.go
Normal file
@@ -0,0 +1,27 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
TestStub struct{ m *TestStubModel }
|
||||
|
||||
TestStubModel struct {
|
||||
To string `json:"to"`
|
||||
Body string `json:"body"`
|
||||
Identity map[string]interface{} `json:"identity"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewTestStub(m *TestStubModel) *TestStub { return &TestStub{m: m} }
|
||||
func (t *TestStub) PhoneNumber() (string, error) { return t.m.To, nil }
|
||||
func (t *TestStub) SMSBody(context.Context) (string, error) { return t.m.Body, nil }
|
||||
func (t *TestStub) MarshalJSON() ([]byte, error) { return json.Marshal(t.m) }
|
||||
func (t *TestStub) TemplateType() template.TemplateType { return template.TypeTestStub }
|
||||
59
courier/template/sms/verifiable_address_changed.go
Normal file
59
courier/template/sms/verifiable_address_changed.go
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
VerifiableAddressChanged struct {
|
||||
deps template.Dependencies
|
||||
model *VerifiableAddressChangedModel
|
||||
}
|
||||
VerifiableAddressChangedModel struct {
|
||||
To string `json:"to"`
|
||||
Identity map[string]any `json:"identity"`
|
||||
ChangedAt string `json:"changed_at"`
|
||||
TransientPayload map[string]any `json:"transient_payload"`
|
||||
UserRequestHeaders http.Header `json:"-"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewVerifiableAddressChanged(d template.Dependencies, m *VerifiableAddressChangedModel) *VerifiableAddressChanged {
|
||||
return &VerifiableAddressChanged{deps: d, model: m}
|
||||
}
|
||||
|
||||
func (t *VerifiableAddressChanged) PhoneNumber() (string, error) {
|
||||
return t.model.To, nil
|
||||
}
|
||||
|
||||
func (t *VerifiableAddressChanged) SMSBody(ctx context.Context) (string, error) {
|
||||
return template.LoadText(
|
||||
ctx,
|
||||
t.deps,
|
||||
os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)),
|
||||
"verifiable_address_changed/sms.body.gotmpl",
|
||||
"verifiable_address_changed/sms.body*",
|
||||
t.model,
|
||||
t.deps.CourierConfig().CourierSMSTemplatesVerifiableAddressChanged(ctx).Body.PlainText,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *VerifiableAddressChanged) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.model)
|
||||
}
|
||||
|
||||
func (t *VerifiableAddressChanged) TemplateType() template.TemplateType {
|
||||
return template.TypeVerifiableAddressChanged
|
||||
}
|
||||
|
||||
func (t *VerifiableAddressChanged) RequestHeaders() http.Header {
|
||||
return t.model.UserRequestHeaders
|
||||
}
|
||||
37
courier/template/sms/verifiable_address_changed_test.go
Normal file
37
courier/template/sms/verifiable_address_changed_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright © 2026 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sms_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/courier/template/sms"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestVerifiableAddressChangedSMS(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
|
||||
tpl := sms.NewVerifiableAddressChanged(reg, &sms.VerifiableAddressChangedModel{
|
||||
To: "+15551234567",
|
||||
ChangedAt: "2026-04-21T12:00:00Z",
|
||||
Identity: map[string]any{"ID": "00000000-0000-0000-0000-000000000001"},
|
||||
})
|
||||
|
||||
phone, err := tpl.PhoneNumber()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "+15551234567", phone)
|
||||
|
||||
body, err := tpl.SMSBody(ctx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Must stay under 160 chars so we do not fragment SMS billing.
|
||||
assert.LessOrEqual(t, len(body), 160, "SMS body too long: %q", body)
|
||||
assert.Contains(t, body, "changed")
|
||||
}
|
||||
58
courier/template/sms/verification_code.go
Normal file
58
courier/template/sms/verification_code.go
Normal file
@@ -0,0 +1,58 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sms
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
)
|
||||
|
||||
type (
|
||||
VerificationCodeValid struct {
|
||||
deps template.Dependencies
|
||||
model *VerificationCodeValidModel
|
||||
}
|
||||
|
||||
VerificationCodeValidModel struct {
|
||||
To string `json:"to"`
|
||||
VerificationCode string `json:"verification_code"`
|
||||
Identity map[string]any `json:"identity"`
|
||||
RequestURL string `json:"request_url"`
|
||||
RequestURLDomain string `json:"request_url_domain"`
|
||||
TransientPayload map[string]any `json:"transient_payload"`
|
||||
ExpiresInMinutes int `json:"expires_in_minutes"`
|
||||
OAuth2LoginRequest *template.OAuth2LoginRequest `json:"oauth2_login_request,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func NewVerificationCodeValid(d template.Dependencies, m *VerificationCodeValidModel) *VerificationCodeValid {
|
||||
return &VerificationCodeValid{deps: d, model: m}
|
||||
}
|
||||
|
||||
func (t *VerificationCodeValid) PhoneNumber() (string, error) {
|
||||
return t.model.To, nil
|
||||
}
|
||||
|
||||
func (t *VerificationCodeValid) SMSBody(ctx context.Context) (string, error) {
|
||||
return template.LoadText(
|
||||
ctx,
|
||||
t.deps,
|
||||
os.DirFS(t.deps.CourierConfig().CourierTemplatesRoot(ctx)),
|
||||
"verification_code/valid/sms.body.gotmpl",
|
||||
"verification_code/valid/sms.body*",
|
||||
t.model,
|
||||
t.deps.CourierConfig().CourierSMSTemplatesVerificationCodeValid(ctx).Body.PlainText,
|
||||
)
|
||||
}
|
||||
|
||||
func (t *VerificationCodeValid) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(t.model)
|
||||
}
|
||||
|
||||
func (t *VerificationCodeValid) TemplateType() template.TemplateType {
|
||||
return template.TypeVerificationCodeValid
|
||||
}
|
||||
36
courier/template/sms/verification_code_test.go
Normal file
36
courier/template/sms/verification_code_test.go
Normal file
@@ -0,0 +1,36 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package sms_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/courier/template/sms"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func TestNewOTPMessage(t *testing.T) {
|
||||
_, reg := pkg.NewFastRegistryWithMocks(t)
|
||||
|
||||
const (
|
||||
expectedPhone = "+12345678901"
|
||||
otp = "012345"
|
||||
)
|
||||
|
||||
tpl := sms.NewVerificationCodeValid(reg, &sms.VerificationCodeValidModel{To: expectedPhone, VerificationCode: otp})
|
||||
|
||||
expectedBody := "Your verification code is: 012345\n\nIf this was not you, do nothing. It expires in 0 minutes.\n"
|
||||
|
||||
actualBody, err := tpl.SMSBody(context.Background())
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedBody, actualBody)
|
||||
|
||||
actualPhone, err := tpl.PhoneNumber()
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, expectedPhone, actualPhone)
|
||||
}
|
||||
14
courier/template/template.go
Normal file
14
courier/template/template.go
Normal file
@@ -0,0 +1,14 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package template
|
||||
|
||||
import (
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/x/httpx"
|
||||
)
|
||||
|
||||
type Dependencies interface {
|
||||
CourierConfig() config.CourierConfigs
|
||||
httpx.ClientProvider
|
||||
}
|
||||
157
courier/template/testhelpers/testhelpers.go
Normal file
157
courier/template/testhelpers/testhelpers.go
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package testhelpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/ory/kratos/courier/template/email"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/ory/kratos/courier/template"
|
||||
"github.com/ory/kratos/driver"
|
||||
"github.com/ory/kratos/driver/config"
|
||||
"github.com/ory/kratos/pkg"
|
||||
)
|
||||
|
||||
func SetupRemoteConfig(t *testing.T, ctx context.Context, plaintext string, html string, subject string) *driver.RegistryDefault {
|
||||
_, reg := pkg.NewVeryFastRegistryWithoutDB(t)
|
||||
require.NoError(t, reg.Config().Set(ctx, config.ViperKeyCourierTemplatesRecoveryInvalidEmail, &config.CourierEmailTemplate{
|
||||
Body: &config.CourierEmailBodyTemplate{
|
||||
PlainText: plaintext,
|
||||
HTML: html,
|
||||
},
|
||||
Subject: subject,
|
||||
}))
|
||||
return reg
|
||||
}
|
||||
|
||||
func TestRendered(t *testing.T, ctx context.Context, tpl interface {
|
||||
EmailBody(context.Context) (string, error)
|
||||
EmailSubject(context.Context) (string, error)
|
||||
},
|
||||
) {
|
||||
rendered, err := tpl.EmailBody(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, rendered)
|
||||
|
||||
rendered, err = tpl.EmailSubject(ctx)
|
||||
require.NoError(t, err)
|
||||
assert.NotEmpty(t, rendered)
|
||||
}
|
||||
|
||||
func TestRemoteTemplates(t *testing.T, basePath string, tmplType template.TemplateType) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
|
||||
toBase64 := func(filePath string) string {
|
||||
f, err := os.ReadFile(filePath) // #nosec G304 -- test code
|
||||
require.NoError(t, err)
|
||||
return base64.StdEncoding.EncodeToString(f)
|
||||
}
|
||||
|
||||
getTemplate := func(tmpl template.TemplateType, d template.Dependencies) interface {
|
||||
EmailBody(context.Context) (string, error)
|
||||
EmailSubject(context.Context) (string, error)
|
||||
} {
|
||||
switch tmpl {
|
||||
case template.TypeRecoveryInvalid:
|
||||
return email.NewRecoveryInvalid(d, &email.RecoveryInvalidModel{})
|
||||
case template.TypeRecoveryValid:
|
||||
return email.NewRecoveryValid(d, &email.RecoveryValidModel{})
|
||||
case template.TypeRecoveryCodeValid:
|
||||
return email.NewRecoveryCodeValid(d, &email.RecoveryCodeValidModel{})
|
||||
case template.TypeRecoveryCodeInvalid:
|
||||
return email.NewRecoveryCodeInvalid(d, &email.RecoveryCodeInvalidModel{})
|
||||
case template.TypeTestStub:
|
||||
return email.NewTestStub(&email.TestStubModel{})
|
||||
case template.TypeVerificationInvalid:
|
||||
return email.NewVerificationInvalid(d, &email.VerificationInvalidModel{})
|
||||
case template.TypeVerificationValid:
|
||||
return email.NewVerificationValid(d, &email.VerificationValidModel{})
|
||||
case template.TypeVerificationCodeInvalid:
|
||||
return email.NewVerificationCodeInvalid(d, &email.VerificationCodeInvalidModel{})
|
||||
case template.TypeVerificationCodeValid:
|
||||
return email.NewVerificationCodeValid(d, &email.VerificationCodeValidModel{})
|
||||
case template.TypeLoginCodeValid:
|
||||
return email.NewLoginCodeValid(d, &email.LoginCodeValidModel{})
|
||||
case template.TypeRegistrationCodeValid:
|
||||
return email.NewRegistrationCodeValid(d, &email.RegistrationCodeValidModel{})
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("case=http resource", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
router := http.NewServeMux()
|
||||
router.HandleFunc("GET /{filename}", func(writer http.ResponseWriter, request *http.Request) {
|
||||
http.ServeFile(writer, request, path.Join(basePath, request.PathValue("filename")))
|
||||
})
|
||||
ts := httptest.NewServer(router)
|
||||
defer ts.Close()
|
||||
|
||||
tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx,
|
||||
ts.URL+"/email.body.plaintext.gotmpl",
|
||||
ts.URL+"/email.body.gotmpl",
|
||||
ts.URL+"/email.subject.gotmpl"))
|
||||
|
||||
require.NotNil(t, tpl, "Expected to find template for %s in %s", tmplType, basePath)
|
||||
|
||||
TestRendered(t, ctx, tpl)
|
||||
})
|
||||
|
||||
t.Run("case=base64 resource", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx,
|
||||
"base64://"+toBase64(path.Join(basePath, "email.body.plaintext.gotmpl")),
|
||||
"base64://"+toBase64(path.Join(basePath, "email.body.gotmpl")),
|
||||
"base64://"+toBase64(path.Join(basePath, "email.subject.gotmpl"))))
|
||||
|
||||
require.NotNil(t, tpl, "Expected to find template for %s in %s", tmplType, basePath)
|
||||
|
||||
TestRendered(t, ctx, tpl)
|
||||
})
|
||||
|
||||
t.Run("case=file resource", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx,
|
||||
"file://"+path.Join(basePath, "email.body.plaintext.gotmpl"),
|
||||
"file://"+path.Join(basePath, "email.body.gotmpl"),
|
||||
"file://"+path.Join(basePath, "email.subject.gotmpl")))
|
||||
|
||||
require.NotNil(t, tpl, "Expected to find template for %s in %s", tmplType, basePath)
|
||||
TestRendered(t, ctx, tpl)
|
||||
})
|
||||
|
||||
t.Run("case=partial subject override", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx,
|
||||
"",
|
||||
"",
|
||||
"base64://"+toBase64(path.Join(basePath, "email.subject.gotmpl"))))
|
||||
|
||||
require.NotNil(t, tpl, "Expected to find template for %s in %s", tmplType, basePath)
|
||||
TestRendered(t, ctx, tpl)
|
||||
})
|
||||
|
||||
t.Run("case=partial body override", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
tpl := getTemplate(tmplType, SetupRemoteConfig(t, ctx,
|
||||
"base64://"+toBase64(path.Join(basePath, "email.body.plaintext.gotmpl")),
|
||||
"base64://"+toBase64(path.Join(basePath, "email.body.gotmpl")),
|
||||
""))
|
||||
|
||||
require.NotNil(t, tpl, "Expected to find template for %s in %s", tmplType, basePath)
|
||||
TestRendered(t, ctx, tpl)
|
||||
})
|
||||
}
|
||||
25
courier/template/type.go
Normal file
25
courier/template/type.go
Normal file
@@ -0,0 +1,25 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package template
|
||||
|
||||
// A Template's type
|
||||
//
|
||||
// swagger:enum TemplateType
|
||||
type TemplateType string
|
||||
|
||||
const (
|
||||
TypeRecoveryInvalid TemplateType = "recovery_invalid"
|
||||
TypeRecoveryValid TemplateType = "recovery_valid"
|
||||
TypeRecoveryCodeInvalid TemplateType = "recovery_code_invalid"
|
||||
TypeRecoveryCodeValid TemplateType = "recovery_code_valid"
|
||||
TypeVerificationInvalid TemplateType = "verification_invalid"
|
||||
TypeVerificationValid TemplateType = "verification_valid"
|
||||
TypeVerificationCodeInvalid TemplateType = "verification_code_invalid"
|
||||
TypeVerificationCodeValid TemplateType = "verification_code_valid"
|
||||
TypeTestStub TemplateType = "stub"
|
||||
TypeLoginCodeValid TemplateType = "login_code_valid"
|
||||
TypeRegistrationCodeValid TemplateType = "registration_code_valid"
|
||||
TypeVerifiableAddressChanged TemplateType = "verifiable_address_changed"
|
||||
TypeAuthenticatorKeyAdded TemplateType = "authenticator_key_added"
|
||||
)
|
||||
Reference in New Issue
Block a user