Implement Ory authentication
continuous-integration/drone/push Build was killed Details

This commit is contained in:
nemunaire 2023-12-24 15:20:57 +01:00
parent c1744940f5
commit 9beff44f09
22 changed files with 660 additions and 44 deletions

View File

@ -30,6 +30,8 @@ import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
ory "github.com/ory/client-go"
"git.happydns.org/happyDomain/actions"
"git.happydns.org/happyDomain/config"
@ -139,7 +141,7 @@ func requireLogin(opts *config.Options, c *gin.Context, msg string) {
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"errmsg": msg})
}
func authMiddleware(opts *config.Options, optional bool) gin.HandlerFunc {
func authMiddleware(opts *config.Options, o *ory.APIClient, optional bool) gin.HandlerFunc {
return func(c *gin.Context) {
var token string
@ -150,6 +152,30 @@ func authMiddleware(opts *config.Options, optional bool) gin.HandlerFunc {
token = flds[1]
}
if o != nil {
cookies := c.Request.Header.Get("Cookie")
session, _, err := o.FrontendAPI.ToSession(c.Request.Context()).Cookie(cookies).TokenizeAs("jwt_happydomain").Execute()
if !((err != nil && session == nil) || (err == nil && !*session.Active)) {
if session.Tokenized != nil {
token = *session.Tokenized
setCookie(opts, c, token)
} else if session.Identity != nil && len(session.Identity.VerifiableAddresses) >= 0 {
uid, err := uuid.Parse(session.Identity.Id)
if err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"errmsg": "Unable to parse user UUID"})
return
}
tmp := [16]byte(uid)
_, token, _ = completeAuth(opts, c, UserProfile{
UserId: tmp[:],
Email: session.Identity.VerifiableAddresses[0].Value,
EmailVerified: session.Identity.VerifiableAddresses[0].Verified,
})
}
}
}
// Stop here if there is no cookie
if len(token) == 0 {
if optional {

View File

@ -23,6 +23,7 @@ package api
import (
"github.com/gin-gonic/gin"
ory "github.com/ory/client-go"
"git.happydns.org/happyDomain/config"
)
@ -47,10 +48,10 @@ import (
// @name Authorization
// @description Description for what is this security definition being used
func DeclareRoutes(cfg *config.Options, router *gin.Engine) {
func DeclareRoutes(cfg *config.Options, o *ory.APIClient, router *gin.Engine) {
apiRoutes := router.Group("/api")
declareAuthenticationRoutes(cfg, apiRoutes)
declareAuthenticationRoutes(cfg, o, apiRoutes)
declareProviderSpecsRoutes(apiRoutes)
declareResolverRoutes(apiRoutes)
declareServiceSpecsRoutes(apiRoutes)
@ -58,7 +59,7 @@ func DeclareRoutes(cfg *config.Options, router *gin.Engine) {
DeclareVersionRoutes(apiRoutes)
apiAuthRoutes := router.Group("/api")
apiAuthRoutes.Use(authMiddleware(cfg, false))
apiAuthRoutes.Use(authMiddleware(cfg, o, false))
declareDomainsRoutes(cfg, apiAuthRoutes)
declareProvidersRoutes(cfg, apiAuthRoutes)

View File

@ -31,6 +31,7 @@ import (
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
ory "github.com/ory/client-go"
"git.happydns.org/happyDomain/config"
"git.happydns.org/happyDomain/model"
@ -39,7 +40,7 @@ import (
const NO_AUTH_ACCOUNT = "_no_auth"
func declareAuthenticationRoutes(opts *config.Options, router *gin.RouterGroup) {
func declareAuthenticationRoutes(opts *config.Options, o *ory.APIClient, router *gin.RouterGroup) {
router.POST("/auth", func(c *gin.Context) {
checkAuth(opts, c)
})
@ -48,7 +49,7 @@ func declareAuthenticationRoutes(opts *config.Options, router *gin.RouterGroup)
})
apiAuthRoutes := router.Group("/auth")
apiAuthRoutes.Use(authMiddleware(opts, true))
apiAuthRoutes.Use(authMiddleware(opts, o, true))
apiAuthRoutes.GET("", func(c *gin.Context) {
if _, exists := c.Get("MySession"); exists {
@ -106,7 +107,7 @@ func displayNotAuthToken(opts *config.Options, c *gin.Context) *UserClaims {
return nil
}
claims, err := completeAuth(opts, c, UserProfile{
claims, _, err := completeAuth(opts, c, UserProfile{
UserId: []byte{0},
Email: NO_AUTH_ACCOUNT,
EmailVerified: true,
@ -199,7 +200,7 @@ func checkAuth(opts *config.Options, c *gin.Context) {
return
}
claims, err := completeAuth(opts, c, UserProfile{
claims, _, err := completeAuth(opts, c, UserProfile{
UserId: user.Id,
Email: user.Email,
EmailVerified: user.EmailVerification != nil,
@ -222,12 +223,12 @@ func checkAuth(opts *config.Options, c *gin.Context) {
}
}
func completeAuth(opts *config.Options, c *gin.Context, userprofile UserProfile) (*UserClaims, error) {
func completeAuth(opts *config.Options, c *gin.Context, userprofile UserProfile) (*UserClaims, string, error) {
// Issue a new JWT token
jti := make([]byte, 16)
_, err := rand.Read(jti)
if err != nil {
return nil, fmt.Errorf("unable to read enough random bytes: %w", err)
return nil, "", fmt.Errorf("unable to read enough random bytes: %w", err)
}
iat := jwt.NewNumericDate(time.Now())
@ -243,9 +244,15 @@ func completeAuth(opts *config.Options, c *gin.Context, userprofile UserProfile)
token, err := jwtToken.SignedString([]byte(opts.JWTSecretKey))
if err != nil {
return nil, fmt.Errorf("unable to sign user claims: %w", err)
return nil, "", fmt.Errorf("unable to sign user claims: %w", err)
}
setCookie(opts, c, token)
return claims, token, nil
}
func setCookie(opts *config.Options, c *gin.Context, token string) {
c.SetCookie(
COOKIE_NAME, // name
token, // value
@ -255,6 +262,4 @@ func completeAuth(opts *config.Options, c *gin.Context, userprofile UserProfile)
opts.DevProxy == "" && opts.ExternalURL.URL.Scheme != "http", // secure
true, // httpOnly
)
return claims, nil
}

View File

@ -40,6 +40,7 @@ func (o *Options) declareFlags() {
flag.BoolVar(&o.NoAuth, "no-auth", false, "Disable user access control, use default account")
flag.Var(&o.JWTSecretKey, "jwt-secret-key", "Secret key used to verify JWT authentication tokens (a random secret is used if undefined)")
flag.Var(&o.ExternalAuth, "external-auth", "Base URL to use for login and registration (use embedded forms if left empty)")
flag.Var(&o.OryKratosServer, "ory-kratos-server", "URL to the Ory Kratos server (default: none, use classical auth)")
// Others flags are declared in some other files likes sources, storages, ... when they need specials configurations
}

View File

@ -67,6 +67,9 @@ type Options struct {
// JWTSecretKey stores the private key to sign and verify JWT tokens.
JWTSecretKey JWTSecretKey
// OryKratosServer is the URL to the authentication server.
OryKratosServer URL
}
// BuildURL appends the given url to the absolute ExternalURL.

3
go.mod
View File

@ -10,8 +10,10 @@ require (
github.com/gin-gonic/gin v1.9.1
github.com/go-mail/mail v2.3.1+incompatible
github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.5.0
github.com/miekg/dns v1.1.57
github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022
github.com/ory/client-go v1.4.7
github.com/ovh/go-ovh v1.4.3
github.com/swaggo/files v1.0.1
github.com/swaggo/gin-swagger v1.6.0
@ -85,7 +87,6 @@ require (
github.com/golang/snappy v0.0.4 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/s2a-go v0.1.7 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect
github.com/googleapis/gax-go/v2 v2.12.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect

4
go.sum
View File

@ -212,6 +212,8 @@ github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs=
github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0=
github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas=
@ -328,6 +330,8 @@ github.com/onsi/gomega v1.19.0 h1:4ieX6qQjPP/BfC3mpsAtIGGlxTWPeA3Inl/7DtXw1tw=
github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro=
github.com/oracle/oci-go-sdk/v32 v32.0.0 h1:SSbzrQO3WRcPJEZ8+b3SFPYsPtkFM96clqrp03lrwbU=
github.com/oracle/oci-go-sdk/v32 v32.0.0/go.mod h1:aZc4jC59IuNP3cr5y1nj555QvwojMX2nMJaBiozuuEs=
github.com/ory/client-go v1.4.7 h1:uWPGGM5zVwpSBfcDIhvA6D+bu2YB7zF4STtpAvzkOco=
github.com/ory/client-go v1.4.7/go.mod h1:DfrTIlME7tgrdgpn4UN07s4OJ1SwzHfrkz+C6C0Lbm0=
github.com/ovh/go-ovh v1.4.3 h1:Gs3V823zwTFpzgGLZNI6ILS4rmxZgJwJCz54Er9LwD0=
github.com/ovh/go-ovh v1.4.3/go.mod h1:AkPXVtgwB6xlKblMjRKJJmjRp+ogrE7fz2lVgcQY8SY=
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=

View File

@ -28,6 +28,7 @@ import (
"time"
"github.com/gin-gonic/gin"
ory "github.com/ory/client-go"
"git.happydns.org/happyDomain/api"
"git.happydns.org/happyDomain/config"
@ -37,6 +38,7 @@ import (
type App struct {
router *gin.Engine
cfg *config.Options
ory *ory.APIClient
srv *http.Server
}
@ -49,14 +51,20 @@ func NewApp(cfg *config.Options) App {
router := gin.New()
router.Use(gin.Logger(), gin.Recovery())
api.DeclareRoutes(cfg, router)
ui.DeclareRoutes(cfg, router)
app := App{
router: router,
cfg: cfg,
}
if cfg.OryKratosServer.URL != nil {
c := ory.NewConfiguration()
c.Servers = ory.ServerConfigurations{{URL: cfg.OryKratosServer.URL.String()}}
app.ory = ory.NewAPIClient(c)
}
api.DeclareRoutes(cfg, app.ory, router)
ui.DeclareRoutes(cfg, router)
return app
}

View File

@ -23,6 +23,7 @@ package ui
import (
"flag"
"fmt"
"io"
"io/ioutil"
"log"
@ -63,6 +64,10 @@ func DeclareRoutes(cfg *config.Options, router *gin.Engine) {
CustomHeadHTML += fmt.Sprintf(`<script type="text/javascript">window.msg_header = { text: %q, color: %q };</script>`, MsgHeaderText, MsgHeaderColor)
}
if cfg.OryKratosServer.URL != nil {
CustomHeadHTML += fmt.Sprintf(`<script type="text/javascript">window.happydomain_ory_kratos_url = %q;</script>`, cfg.OryKratosServer.URL.String())
}
if cfg.DevProxy != "" {
router.GET("/.svelte-kit/*_", serveOrReverse("", cfg))
router.GET("/node_modules/*_", serveOrReverse("", cfg))

View File

@ -77,9 +77,6 @@
bind:this={formElm}
on:submit|preventDefault={goSendLink}
>
<p class="text-center">
{$t('email.recover')}.
</p>
<Row>
<label for="email-input" class="col-md-4 col-form-label text-truncate text-md-right fw-bold">
{$t('email.address')}

View File

@ -106,7 +106,34 @@
function logout() {
APILogout().then(
() => {
async () => {
if (window.happydomain_ory_kratos_url) {
await fetch(window.happydomain_ory_kratos_url + `self-service/logout/browser`,
{
method: "GET",
headers: [
["Accept", "application/json"],
["Content-Type", "application/json"]
],
credentials: 'include',
}
).then(
async (data) => data.json()
).then(
async (state) => {
await fetch(state.logout_url,
{
method: "GET",
headers: [
["Accept", "application/json"],
["Content-Type", "application/json"]
],
credentials: 'include',
})
}
)
}
refreshUserSession().then(
() => { },
() => {

View File

@ -0,0 +1,72 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-2024 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at <contact@happydomain.org>.
For AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import { createEventDispatcher } from 'svelte';
import KratosNode from '$lib/components/KratosNode.svelte';
const dispatch = createEventDispatcher();
export let flow: String;
export let nodes: Array;
export let only: String;
export let submissionInProgress = false;
let values = { };
function initializeValues(nodes) {
const vls = { };
for (const node of nodes) {
if (!only || node.group === only || node.group === 'default') {
vls[node.attributes.name] = node.attributes.value;
}
}
values = vls;
}
$: initializeValues(nodes);
function submission() {
dispatch('submit', values);
}
</script>
<form
class="container my-1"
method="post"
onsubmit="alert('test'); return false;"
on:submit|preventDefault={submission}
>
{#each nodes as node, i}
{#if !only || node.group === only || node.group === 'default'}
<KratosNode
{flow}
i={only + i}
{node}
{submissionInProgress}
bind:value={values[node.attributes.name]}
/>
{/if}
{/each}
</form>

View File

@ -0,0 +1,250 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-2024 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at <contact@happydomain.org>.
For AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import { goto } from '$app/navigation';
import { createEventDispatcher } from 'svelte';
import {
Alert,
Button,
Icon,
Spinner,
TabContent,
TabPane,
} from 'sveltestrap';
import KratosFlow from '$lib/components/KratosFlow.svelte';
import { t } from '$lib/translations';
import { toasts } from '$lib/stores/toasts';
import { refreshUserSession } from '$lib/stores/usersession';
const dispatch = createEventDispatcher();
export let flow: String;
export let tabs = false;
let form = { };
let groups = [];
let submissionInProgress = false;
let error = null;
let suggestLogout = false;
let action_method = "";
let action_url = "";
async function getFlow(aal) {
const res = await fetch(window.happydomain_ory_kratos_url + `self-service/${flow}/browser${aal ? '?aal=' + aal : ''}`,
{
method: "GET",
headers: [
["Accept", "application/json"],
["Content-Type", "application/json"]
],
credentials: 'include',
}
)
suggestLogout = false;
return {data: res, state: await res.json()};
}
async function treatForm({ data, next, state }) {
if (next && data.status === 200) {
if (state.session && (!state.session.active || !state.session.identity)) {
state = (await getFlow('aal2')).state;
suggestLogout = true;
} else {
dispatch('success', state);
}
}
if (state.error && state.error.id === 'session_already_available') {
state = (await getFlow('aal2')).state;
suggestLogout = true;
}
if (state.error) {
error = state.error;
toasts.addErrorToast({
title: state.error.message,
message: state.error.reason,
timeout: 30000,
});
} else {
error = null;
}
if (state.ui) {
const grps = [];
for(const node of state.ui.nodes) {
if (node.group !== "default" && !grps.includes(node.group)) {
grps.push(node.group);
}
}
form = state;
action_url = state.ui.action;
action_method = state.ui.method;
groups = grps;
}
submissionInProgress = false;
}
function submission(event) {
submissionInProgress = true;
fetch(action_url,
{
method: action_method,
body: JSON.stringify(event.detail),
headers: [
["Accept", "application/json"],
["Content-Type", "application/json"]
],
credentials: 'include',
}
).then(
async (data) => ({ data, next: true, state: await data.json() })
).then(treatForm);
}
async function forceLogout() {
await fetch(window.happydomain_ory_kratos_url + `self-service/logout/browser`,
{
method: "GET",
headers: [
["Accept", "application/json"],
["Content-Type", "application/json"]
],
credentials: 'include',
}
).then(
async (data) => data.json()
).then(
async (state) => {
await fetch(state.logout_url,
{
method: "GET",
headers: [
["Accept", "application/json"],
["Content-Type", "application/json"]
],
credentials: 'include',
}
);
}
);
formreq = getFlow();
formreq.then(treatForm);
}
let formreq = getFlow();
formreq.then(treatForm);
</script>
{#if error && error.message}
<Alert color="danger">
<Button
class="float-end"
color="link"
size="sm"
on:click={forceLogout}
>
<Icon
name="door-open"
/>
</Button>
<strong>
{error.message}.
</strong>
{error.reason}
{#if error.details}
<a href={error.details.docs} class="float-end" target="_blank">
<Icon
name="info-circle-fill"
title={error.details.hint}
/>
</a>
{/if}
</Alert>
{/if}
{#if form && form.ui}
{#if form.ui.messages}
{#each form.ui.messages as message}
<Alert color={message.type === "error"?"danger":"info"}>
<strong>
{message.text}
</strong>
</Alert>
{/each}
{/if}
{#if form.ui.nodes}
{#if tabs}
<TabContent>
{#each groups as group, ig}
<TabPane
class="pt-2"
tabId={group}
tab={group}
active={ig === 0}
>
<KratosFlow
{flow}
nodes={form.ui.nodes}
only={group}
{submissionInProgress}
on:submit={submission}
/>
</TabPane>
{/each}
</TabContent>
{:else}
{#each groups as group, i}
{#if i > 0}
<hr>
{/if}
<KratosFlow
{flow}
nodes={form.ui.nodes}
only={group}
{submissionInProgress}
on:submit={submission}
/>
{/each}
{/if}
{/if}
{/if}
{#if suggestLogout}
<div class="d-flex align-items-center justify-content-center">
Quelque chose s'est mal passé&nbsp;?
<Button
color="link"
type="button"
on:click={forceLogout}
>
Déconnectez-vous
</Button>
</div>
{/if}

View File

@ -0,0 +1,160 @@
<!--
This file is part of the happyDomain (R) project.
Copyright (c) 2022-2024 happyDomain
Authors: Pierre-Olivier Mercier, et al.
This program is offered under a commercial and under the AGPL license.
For commercial licensing, contact us at <contact@happydomain.org>.
For AGPL licensing:
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import {
Button,
FormGroup,
Input,
Label,
Spinner,
} from 'sveltestrap';
import { t } from '$lib/translations';
export let flow: String;
export let i;
export let node: Object;
export let submissionInProgress = false;
export let value: String;
let isSocial = false;
$: isSocial = node && node.attributes && (node.attributes.name === "provider" || node.attributes.name === "link") && node.group === "oidc"
</script>
{#if node.type === 'input'}
{#if node.attributes.type === "hidden"}
<input
{...node.attributes}
>
{:else if node.attributes.type === "submit"}
<FormGroup class="d-flex flex-column">
<Button
color="primary"
{...node.attributes}
disabled={submissionInProgress || node.attributes.disabled}
formnovalidate={isSocial || node.meta.label.id === 107008}
>
{#if submissionInProgress}
<Spinner size="sm" />
{/if}
{node.meta.label.text}
</Button>
</FormGroup>
{:else if node.attributes.type === "button"}
<FormGroup class="d-flex flex-column">
<Button
type="button"
color="secondary"
name={node.attributes.name}
value={node.attributes.value}
disabled={node.attributes.disabled || submissionInProgress}
on:click={(e) => {e.stopPropagation(); e.preventDefault(); const run = new Function(node.attributes.onclick); run();}}
>
{node.meta.label.text}
</Button>
</FormGroup>
{:else if node.attributes.type === "checkbox"}
<FormGroup>
<Input
type="checkbox"
label={node.meta.label.text}
id={"ns" + i}
{...node.attributes}
disabled={submissionInProgress || node.attributes.disabled}
invalid={node.messages.find(({ type }) => type === "error")}
feedback={node.messages.map(({ text, id }, k) => text)}
on:changed={(e) => { if (e.target.checked) { value = node.attributes.value; } else { value = undefined; } }}
/>
</FormGroup>
{:else}
<FormGroup>
<Label for={"ns" + i}>
{#if node.meta.label.id == 107001}
{$t('common.password')}
{:else if node.meta.label.id == 107002 || node.meta.label.id == 107004}
{$t('email.address')}
{:else}
{node.meta.label.text}
{/if}
</Label>
<Input
id={"ns" + i}
{...node.attributes}
disabled={submissionInProgress || node.attributes.disabled}
invalid={node.messages.find(({ type }) => type === "error")}
feedback={node.messages.map(({ text, id }, k) => text)}
placeholder={node.attributes.placeholder?node.attributes.placeholder:(node.meta.label.id===107001?"pMockapetris@usc.edu":(node.meta.label.id===107002?"xXxXxXxXxX":""))}
bind:value={value}
/>
{#if flow === "login" && node.attributes.type === "password"}
<div class="form-text">
<a
href="/forgotten-password"
>
{$t('password.forgotten')}
</a>
</div>
{/if}
</FormGroup>
{/if}
{:else if node.type === 'a'}
<Button
href={node.attributes.href}
>
{node.attributes.title.text}
</Button>
{:else if node.type === 'img'}
<img
src={node.attributes.src}
alt={node.meta.label?.text}
/>
{:else if node.type === 'script'}
<script {...node.attributes}></script>
{:else if node.type === 'text'}
<p>
{node.meta?.label?.text}
</p>
{#if node.attributes.text.id === 1050015}
<div
class="container-fluid"
>
<div class="row">
{#each node.attributes.text.context.secrets as text, k}
<div
key={k}
class="col-xs-3"
>
<code>{text.id === 1050014 ? "Used" : text.text}</code>
</div>
{/each}
</div>
</div>
{:else}
<div>
<pre>
{node.attributes.text.text}
</pre>
</div>
{/if}
{/if}

View File

@ -22,7 +22,7 @@
-->
<script lang="ts">
import { goto } from '$app/navigation';
import { createEventDispatcher } from 'svelte';
import {
Button,
@ -33,11 +33,11 @@
} from 'sveltestrap';
import { t } from '$lib/translations';
import { authUser, cleanUserSession } from '$lib/api/user';
import { authUser } from '$lib/api/user';
import type { LoginForm } from '$lib/model/user';
import { providers } from '$lib/stores/providers';
import { toasts } from '$lib/stores/toasts';
import { refreshUserSession } from '$lib/stores/usersession';
const dispatch = createEventDispatcher();
let loginForm: LoginForm = {
email: "",
@ -60,13 +60,10 @@
authUser(loginForm)
.then(
() => {
cleanUserSession();
providers.set(null);
formSent = false;
emailState = true;
passwordState = true;
refreshUserSession();
goto('/');
dispatch('success');
},
(error) => {
formSent = false;

View File

@ -22,7 +22,7 @@
-->
<script lang="ts">
import { goto } from '$app/navigation';
import { createEventDispatcher } from 'svelte';
import {
Button,
@ -39,6 +39,7 @@
import { checkWeakPassword, checkPasswordConfirmation } from '$lib/password';
import { toasts } from '$lib/stores/toasts';
const dispatch = createEventDispatcher();
let signupForm: SignUpForm = {
email: "",
@ -70,13 +71,7 @@
.then(
() => {
formSent = false;
toasts.addToast({
title: $t('account.signup.success'),
message: $t('email.instruction.check-inbox'),
type: 'success',
timeout: 5000,
});
goto('/login');
dispatch('success');
},
(error) => {
formSent = false;

View File

@ -332,6 +332,7 @@
},
"language": "Language",
"save": "Save settings",
"security": "Account Security",
"showrrtypes": "Show resource type associated with services (for users familiar with DNS)",
"success": "Continue to enjoy happyDomain.",
"success-change": "Your settings has been saved.",

View File

@ -305,6 +305,7 @@
},
"language": "Langue",
"save": "Enregistrer les paramètres",
"security": "Sécurité du compte",
"showrrtypes": "Afficher les types d'enregistrement associés aux services (pour les utilisateurs familiers de la terminologie DNS)",
"success": "Vous pouvez continuer d'apprécier happyDomain.",
"success-change": "Vos paramètres ont été sauvegardés.",

View File

@ -28,7 +28,9 @@
} from 'sveltestrap';
import ForgottenPasswordForm from '$lib/components/ForgottenPasswordForm.svelte';
import KratosForm from '$lib/components/KratosForm.svelte';
import RecoverAccountForm from '$lib/components/RecoverAccountForm.svelte';
import { t } from '$lib/translations';
let error = "";
export let data;
@ -42,6 +44,13 @@
{:else if data.user && data.key}
<RecoverAccountForm user={data.user} key={data.key} />
{:else}
<ForgottenPasswordForm />
<p class="text-center">
{$t('email.recover')}.
</p>
{#if window.happydomain_ory_kratos_url}
<KratosForm flow="recovery" />
{:else}
<ForgottenPasswordForm />
{/if}
{/if}
</Container>

View File

@ -22,6 +22,8 @@
-->
<script lang="ts">
import { goto } from '$app/navigation';
import {
Card,
CardBody,
@ -31,8 +33,20 @@
Row,
} from 'sveltestrap';
import { t } from '$lib/translations';
import SignUpForm from '$lib/components/SignUpForm.svelte';
import KratosForm from '$lib/components/KratosForm.svelte';
import { toasts } from '$lib/stores/toasts';
import { t } from '$lib/translations';
function next() {
toasts.addToast({
title: $t('account.signup.success'),
message: $t('email.instruction.check-inbox'),
type: 'success',
timeout: 5000,
});
goto('/login');
}
</script>
<Container class="my-3">
@ -48,7 +62,16 @@
</h6>
</CardHeader>
<CardBody>
<SignUpForm />
{#if window.happydomain_ory_kratos_url}
<KratosForm
flow="registration"
on:success={next}
/>
{:else}
<SignUpForm
on:success={next}
/>
{/if}
</CardBody>
</Card>
<div class="mt-3 text-justify">

View File

@ -22,6 +22,8 @@
-->
<script lang="ts">
import { goto } from '$app/navigation';
import {
Card,
CardBody,
@ -32,7 +34,18 @@
} from 'sveltestrap';
import { t } from '$lib/translations';
import { cleanUserSession } from '$lib/api/user';
import LoginForm from '$lib/components/LoginForm.svelte';
import KratosForm from '$lib/components/KratosForm.svelte';
import { providers } from '$lib/stores/providers';
import { refreshUserSession } from '$lib/stores/usersession';
function next() {
cleanUserSession();
providers.set(null);
refreshUserSession();
goto('/');
}
</script>
<Container class="my-3">
@ -48,7 +61,16 @@
</h6>
</CardHeader>
<CardBody>
<LoginForm />
{#if window.happydomain_ory_kratos_url}
<KratosForm
flow="login"
on:success={next}
/>
{:else}
<LoginForm
on:success={next}
/>
{/if}
</CardBody>
</Card>
<div class="text-center mt-4">

View File

@ -33,6 +33,7 @@
import ChangePasswordForm from '$lib/components/ChangePasswordForm.svelte';
import DeleteAccountCard from '$lib/components/DeleteAccountCard.svelte';
import KratosForm from '$lib/components/KratosForm.svelte';
import UserSettingsForm from '$lib/components/UserSettingsForm.svelte';
import { t } from '$lib/translations';
import { userSession } from '$lib/stores/usersession';
@ -57,14 +58,21 @@
{/if}
</Row>
{#if $userSession.email !== '_no_auth'}
<h2 id="password-change">
{$t('password.change')}
<h2 id="security-settings">
{$t('settings.security')}
</h2>
<Row>
<Col md={{size: 8, offset: 2}}>
<Card>
<CardBody>
<ChangePasswordForm />
{#if window.happydomain_ory_kratos_url}
<KratosForm
flow="settings"
tabs
/>
{:else}
<ChangePasswordForm />
{/if}
</CardBody>
</Card>
</Col>