mirror of
https://github.com/zitadel/zitadel.git
synced 2024-12-04 23:45:07 +00:00
feat: notifications (#109)
* implement notification providers * email provider * notification handler * notify users * implement code sent on user eventstore * send email implementation * send init code * handle codes * fix project member handler * add some logs for debug * send emails * text changes * send sms * notification process * send password code * format phone number * test format phone * remove fmts * remove unused code * rename files * add mocks * merge master * Update internal/notification/providers/email/message.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/notification/repository/eventsourcing/handler/notification.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/notification/repository/eventsourcing/handler/notification.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/notification/providers/email/provider.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * requested changes of mr * move locker to eventstore pkg * Update internal/notification/providers/chat/message.go Co-authored-by: Livio Amstutz <livio.a@gmail.com> * move locker to eventstore pkg * linebreak * Update internal/notification/providers/email/provider.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/notification/repository/eventsourcing/handler/notification.go Co-authored-by: Silvan <silvan.reusser@gmail.com> * Update internal/notification/repository/eventsourcing/handler/notification.go Co-authored-by: Silvan <silvan.reusser@gmail.com> Co-authored-by: Silvan <silvan.reusser@gmail.com> Co-authored-by: Livio Amstutz <livio.a@gmail.com>
This commit is contained in:
parent
c365a98cc8
commit
e318139b37
@ -20,3 +20,19 @@ export ZITADEL_KEY_PATH="$BASEDIR/local_keys.yaml"
|
||||
|
||||
export ZITADEL_USER_VERIFICATION_KEY=UserVerificationKey_1
|
||||
export ZITADEL_OTP_VERIFICATION_KEY=OTPVerificationKey_1
|
||||
|
||||
# Notifications
|
||||
export DEBUG_MODE=TRUE
|
||||
export TWILIO_SERVICE_SID=$(gopass citadel-secrets/citadel/developer/default/twilio-sid)
|
||||
export TWILIO_TOKEN=$(gopass citadel-secrets/citadel/developer/default/twilio-auth-token)
|
||||
export TWILIO_SENDER_NAME=CAOS AG
|
||||
export SMTP_HOST=smtp.gmail.com:465
|
||||
export SMTP_USER=zitadel@caos.ch
|
||||
export SMTP_PASSWORD=$(gopass citadel-secrets/citadel/google/emailappkey)
|
||||
export EMAIL_SENDER_ADDRESS=noreply@caos.ch
|
||||
export EMAIL_SENDER_NAME=CAOS AG
|
||||
export SMTP_TLS=TRUE
|
||||
export CHAT_URL=$(gopass citadel-secrets/citadel/developer/default/google-chat-url | base64 -D)
|
||||
|
||||
# Zitadel
|
||||
export ZITADEL_ACCOUNTS=http://localhost:61121
|
||||
|
@ -9,6 +9,7 @@ import (
|
||||
|
||||
authz "github.com/caos/zitadel/internal/api/auth"
|
||||
"github.com/caos/zitadel/internal/config"
|
||||
"github.com/caos/zitadel/internal/notification"
|
||||
tracing "github.com/caos/zitadel/internal/tracing/config"
|
||||
"github.com/caos/zitadel/pkg/admin"
|
||||
"github.com/caos/zitadel/pkg/auth"
|
||||
@ -18,11 +19,12 @@ import (
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Mgmt management.Config
|
||||
Auth auth.Config
|
||||
Login login.Config
|
||||
Admin admin.Config
|
||||
Console console.Config
|
||||
Mgmt management.Config
|
||||
Auth auth.Config
|
||||
Login login.Config
|
||||
Admin admin.Config
|
||||
Console console.Config
|
||||
Notification notification.Config
|
||||
|
||||
Log logging.Config
|
||||
Tracing tracing.TracingConfig
|
||||
@ -38,6 +40,7 @@ func main() {
|
||||
loginEnabled := flag.Bool("login", true, "enable login ui")
|
||||
adminEnabled := flag.Bool("admin", true, "enable admin api")
|
||||
consoleEnabled := flag.Bool("console", true, "enable console ui")
|
||||
notificationEnabled := flag.Bool("notification", true, "enable notification handler")
|
||||
flag.Parse()
|
||||
|
||||
conf := new(Config)
|
||||
@ -58,6 +61,9 @@ func main() {
|
||||
if *adminEnabled {
|
||||
admin.Start(ctx, conf.Admin, conf.AuthZ, conf.SystemDefaults)
|
||||
}
|
||||
if *notificationEnabled {
|
||||
notification.Start(ctx, conf.Notification, conf.SystemDefaults)
|
||||
}
|
||||
if *consoleEnabled {
|
||||
err = console.Start(ctx, conf.Console)
|
||||
logging.Log("MAIN-3Dfuc").OnError(err).Fatal("error starting console ui")
|
||||
|
@ -110,3 +110,33 @@ Admin:
|
||||
Console:
|
||||
Port: 50050
|
||||
StaticDir: /app/console/dist
|
||||
|
||||
|
||||
Notification:
|
||||
Repository:
|
||||
Eventstore:
|
||||
ServiceName: 'Notification'
|
||||
Repository:
|
||||
SQL:
|
||||
Host: $ZITADEL_EVENTSTORE_HOST
|
||||
Port: $ZITADEL_EVENTSTORE_PORT
|
||||
User: 'notification'
|
||||
Database: 'eventstore'
|
||||
SSLmode: disable
|
||||
Cache:
|
||||
Type: 'fastcache'
|
||||
Config:
|
||||
MaxCacheSizeInByte: 10485760 #10mb
|
||||
View:
|
||||
Host: $ZITADEL_EVENTSTORE_HOST
|
||||
Port: $ZITADEL_EVENTSTORE_PORT
|
||||
User: 'notification'
|
||||
Database: 'notification'
|
||||
SSLmode: disable
|
||||
Spooler:
|
||||
ConcurrentTasks: 4
|
||||
BulkLimit: 100
|
||||
FailureCountUntilSkip: 5
|
||||
Handlers:
|
||||
Notification:
|
||||
MinimumCycleDuration: 10s
|
@ -109,3 +109,54 @@ SystemDefaults:
|
||||
AuthMethodType: 'AUTH_TYPE_NONE'
|
||||
Owners:
|
||||
- 'zitadel-admin@caos.ch'
|
||||
Notifications:
|
||||
DebugMode: $DEBUG_MODE
|
||||
Endpoints:
|
||||
InitCode: '$ZITADEL_ACCOUNTS/user/init?userID={{.UserID}}&code={{.Code}}'
|
||||
PasswordReset: '$ZITADEL_ACCOUNTS/password/init?userID={{.UserID}}&code={{.Code}}'
|
||||
VerifyEmail: '$ZITADEL_ACCOUNTS/mail/verification?userID={{.UserID}}&code={{.Code}}'
|
||||
Providers:
|
||||
Chat:
|
||||
Url: $CHAT_URL
|
||||
SplitCount: 4000
|
||||
Email:
|
||||
SMTP:
|
||||
Host: $SMTP_HOST
|
||||
User: $SMTP_USER
|
||||
Password: $SMTP_PASSWORD
|
||||
From: $EMAIL_SENDER_ADDRESS
|
||||
FromName: $EMAIL_SENDER_NAME
|
||||
Tls: $SMTP_TLS
|
||||
Twilio:
|
||||
SID: $TWILIO_SERVICE_SID
|
||||
Token: $TWILIO_TOKEN
|
||||
From: $TWILIO_SENDER_NAME
|
||||
TemplateData:
|
||||
InitCode:
|
||||
Title: 'Zitadel - User Initialisieren'
|
||||
PreHeader: 'User Initialisieren'
|
||||
Subject: 'User Initialisieren'
|
||||
Greeting: 'Hallo {{.FirstName}} {{.LastName}},'
|
||||
Text: 'Dieser Benutzer wurde soeben im Zitadel erstellt. Du kannst den Button unten verwenden, um die Initialisierung abzuschliesen. Falls du dieses Mail nicht angefordert hast, kannst du es einfach ignorieren.'
|
||||
ButtonText: 'Initialisierung abschliessen'
|
||||
PasswordReset:
|
||||
Title: 'Zitadel - Passwort zurücksetzen'
|
||||
PreHeader: 'Passwort zurücksetzen'
|
||||
Subject: 'Passwort zurücksetzen'
|
||||
Greeting: 'Hallo {{.FirstName}} {{.LastName}},'
|
||||
Text: 'Wir haben eine Anfrage für das Zurücksetzen deines Passwortes bekommen. Du kannst den untenstehenden Button verwenden, um dein Passwort zurückzusetzen. Falls du dieses Mail nicht angefordert hast, kannst du es ignorieren.'
|
||||
ButtonText: 'Passwort zurücksetzen'
|
||||
VerifyEmail:
|
||||
Title: 'Zitadel - Email Verifizieren'
|
||||
PreHeader: 'Email verifizieren'
|
||||
Subject: 'Email verifizieren'
|
||||
Greeting: 'Hallo {{.FirstName}} {{.LastName}},'
|
||||
Text: 'Eine neue E-Mail Adresse wurde hinzugefügt. Bitte verwende den untenstehenden Button um diese zu verifizieren. Falls du deine E-Mail Adresse nicht selber hinzugefügt hast, kannst du dieses E-Mail ignorieren.'
|
||||
ButtonText: 'Passwort zurücksetzen'
|
||||
VerifyPhone:
|
||||
Title: 'Zitadel - Telefonnummer Verifizieren'
|
||||
PreHeader: 'Telefonnummer Verifizieren'
|
||||
Subject: 'Telefonnummer Verifizieren'
|
||||
Greeting: 'Hallo {{.FirstName}} {{.LastName}},'
|
||||
Text: 'Eine Telefonnummer wurde hinzugefügt. Bitte verifiziere diese in dem du folgenden Code eingibst: {{.Code}}'
|
||||
ButtonText: 'Telefon verifizieren'
|
451
cmd/zitadel/template.html
Normal file
451
cmd/zitadel/template.html
Normal file
@ -0,0 +1,451 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||
<title>{{.Title}}</title>
|
||||
<style>
|
||||
|
||||
@import url('https://fonts.googleapis.com/css?family=Roboto:300,400&display=swap');
|
||||
|
||||
@font-face {
|
||||
font-family: Ailerons;
|
||||
src: url("https://www.caos.ch/fonts/Ailerons-Typeface.otf") format("opentype");
|
||||
}
|
||||
|
||||
|
||||
/* -------------------------------------
|
||||
GLOBAL RESETS
|
||||
------------------------------------- */
|
||||
|
||||
img {
|
||||
border: none;
|
||||
-ms-interpolation-mode: bicubic;
|
||||
max-width: 100%;
|
||||
}
|
||||
body {
|
||||
background: #222324 url('waves-bottomleft.png') bottom left no-repeat;
|
||||
background-size: 150px;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 300;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
font-size: 14px;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
table {
|
||||
border-collapse: separate;
|
||||
mso-table-lspace: 0pt;
|
||||
mso-table-rspace: 0pt;
|
||||
width: 100%; }
|
||||
table td {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
vertical-align: top;
|
||||
color: #fff;
|
||||
}
|
||||
/* -------------------------------------
|
||||
BODY & CONTAINER
|
||||
------------------------------------- */
|
||||
.body {
|
||||
background: url('waves-topright.png') top right no-repeat;
|
||||
background-size: 150px;
|
||||
width: 100%;
|
||||
}
|
||||
/* Set a max-width, and make it display as block so it will automatically stretch to that width, but will also shrink down on a phone or something */
|
||||
.container {
|
||||
display: block;
|
||||
margin: 0 auto !important;
|
||||
/* makes it centered */
|
||||
max-width: 580px;
|
||||
padding: 40px;
|
||||
width: 580px;
|
||||
}
|
||||
/* This should also be a block element, so that it will fill 100% of the .container */
|
||||
.content {
|
||||
box-sizing: border-box;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
max-width: 580px;
|
||||
padding: 10px;
|
||||
}
|
||||
/* -------------------------------------
|
||||
HEADER, FOOTER, MAIN
|
||||
------------------------------------- */
|
||||
.main {
|
||||
background: #191919;
|
||||
width: 100%;
|
||||
border-radius: 16px;
|
||||
}
|
||||
.wrapper {
|
||||
box-sizing: border-box;
|
||||
padding: 50px;
|
||||
}
|
||||
.content-block {
|
||||
padding-bottom: 10px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
.footer {
|
||||
clear: both;
|
||||
margin-top: 10px;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
.footer td,
|
||||
.footer p,
|
||||
.footer span,
|
||||
.footer a {
|
||||
color: #999999;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer a{
|
||||
color: #FE00FF;
|
||||
text-decoration: none;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.footer a:hover{
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.apple-link{
|
||||
margin-bottom: 10px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* -------------------------------------
|
||||
TYPOGRAPHY
|
||||
------------------------------------- */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
color: #fff;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: 300;
|
||||
line-height: 1.4;
|
||||
margin: 0;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
h1 {
|
||||
font-size: 35px;
|
||||
font-weight: 300;
|
||||
text-align: center;
|
||||
text-transform: capitalize;
|
||||
}
|
||||
p,
|
||||
ul,
|
||||
ol {
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-size: 14px;
|
||||
font-weight: 300;
|
||||
margin: 0;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
p li,
|
||||
ul li,
|
||||
ol li {
|
||||
list-style-position: inside;
|
||||
margin-left: 5px;
|
||||
}
|
||||
a {
|
||||
color: #0FBDA6;
|
||||
text-decoration: underline;
|
||||
}
|
||||
/* -------------------------------------
|
||||
BUTTONS
|
||||
------------------------------------- */
|
||||
.btn {
|
||||
box-sizing: border-box;
|
||||
width: 100%; }
|
||||
.btn > tbody > tr > td {
|
||||
padding-bottom: 15px; }
|
||||
.btn table {
|
||||
width: auto;
|
||||
}
|
||||
.btn table td {
|
||||
background-color: #ffffff;
|
||||
border-radius: 5px;
|
||||
text-align: center;
|
||||
}
|
||||
.btn a {
|
||||
background-color: #ffffff;
|
||||
border: solid 1px #0FBDA6;
|
||||
border-radius: 5px;
|
||||
box-sizing: border-box;
|
||||
color: #0FBDA6;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
padding: 20px 60px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.btn-primary table td {
|
||||
background-color: #0FBDA6;
|
||||
}
|
||||
|
||||
.btn-primary a {
|
||||
background-color: #0FBDA6;
|
||||
border-color: #0FBDA6;
|
||||
color: #ffffff;
|
||||
}
|
||||
/* -------------------------------------
|
||||
OTHER STYLES THAT MIGHT BE USEFUL
|
||||
------------------------------------- */
|
||||
.mheader {
|
||||
background-color: #131313;
|
||||
}
|
||||
|
||||
.headercell {
|
||||
color: #FFFFFF;
|
||||
}
|
||||
|
||||
.headertitle{
|
||||
padding: 30px 30px 70px;
|
||||
margin: 0;
|
||||
font-size: 42px;
|
||||
font-family: 'Ailerons', sans-serif;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo{
|
||||
height: 50px;
|
||||
margin: 10px;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
.hello {
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.last {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.first {
|
||||
margin-top: 0;
|
||||
}
|
||||
.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
.align-right {
|
||||
text-align: right;
|
||||
}
|
||||
.align-left {
|
||||
text-align: left;
|
||||
}
|
||||
.clear {
|
||||
clear: both;
|
||||
}
|
||||
.mt0 {
|
||||
margin-top: 0;
|
||||
}
|
||||
.mb0 {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.preheader {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
.subject {
|
||||
color: transparent;
|
||||
display: none;
|
||||
height: 0;
|
||||
max-height: 0;
|
||||
max-width: 0;
|
||||
opacity: 0;
|
||||
overflow: hidden;
|
||||
mso-hide: all;
|
||||
visibility: hidden;
|
||||
width: 0;
|
||||
}
|
||||
.powered-by a {
|
||||
text-decoration: none;
|
||||
}
|
||||
hr {
|
||||
border: 0;
|
||||
border-bottom: 1px solid #f6f6f6;
|
||||
margin: 20px 0;
|
||||
}
|
||||
/* -------------------------------------
|
||||
RESPONSIVE AND MOBILE FRIENDLY STYLES
|
||||
------------------------------------- */
|
||||
@media only screen and (max-width: 620px) {
|
||||
table[class=body] h1 {
|
||||
font-size: 28px !important;
|
||||
margin-bottom: 10px !important;
|
||||
}
|
||||
table[class=body] p,
|
||||
table[class=body] ul,
|
||||
table[class=body] ol,
|
||||
table[class=body] td,
|
||||
table[class=body] span,
|
||||
table[class=body] a {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
table[class=body] .wrapper,
|
||||
table[class=body] .article {
|
||||
padding: 10px !important;
|
||||
}
|
||||
table[class=body] .content {
|
||||
padding: 0 !important;
|
||||
}
|
||||
table[class=body] .container {
|
||||
padding: 0 !important;
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .main {
|
||||
border-left-width: 0 !important;
|
||||
border-radius: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
}
|
||||
table[class=body] .btn table {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .btn a {
|
||||
width: 100% !important;
|
||||
}
|
||||
table[class=body] .img-responsive {
|
||||
height: auto !important;
|
||||
max-width: 100% !important;
|
||||
width: auto !important;
|
||||
}
|
||||
}
|
||||
/* -------------------------------------
|
||||
PRESERVE THESE STYLES IN THE HEAD
|
||||
------------------------------------- */
|
||||
@media all {
|
||||
.ExternalClass {
|
||||
width: 100%;
|
||||
}
|
||||
.ExternalClass,
|
||||
.ExternalClass p,
|
||||
.ExternalClass span,
|
||||
.ExternalClass font,
|
||||
.ExternalClass td,
|
||||
.ExternalClass div {
|
||||
line-height: 100%;
|
||||
}
|
||||
.apple-link a {
|
||||
color: inherit !important;
|
||||
font-family: inherit !important;
|
||||
font-size: inherit !important;
|
||||
font-weight: inherit !important;
|
||||
line-height: inherit !important;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
.btn-primary table td:hover {
|
||||
background-color: #1ADFC5 !important;
|
||||
}
|
||||
.btn-primary a:hover {
|
||||
background-color: #1ADFC5 !important;
|
||||
border-color: #1ADFC5 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="">
|
||||
<span class="preheader">{{.PreHeader}}</span>
|
||||
<span class="subject">{{.Subject}}</span>
|
||||
<table role="pheader" class="mheader" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="mheadercell">
|
||||
|
||||
</td>
|
||||
<td class="logo">
|
||||
<img class="logo" src="https://caos.ch/images/LogoCaos.png" alt="CAOS AG - Logo">
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="body">
|
||||
<tr>
|
||||
<td></td>
|
||||
<td class="container">
|
||||
<div class="content">
|
||||
|
||||
<!-- START CENTERED WHITE CONTAINER -->
|
||||
<table role="pheader" class="tableheader" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="headercell">
|
||||
<p class="headertitle">Zitadel</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table role="presentation" class="main">
|
||||
|
||||
<!-- START MAIN CONTENT AREA -->
|
||||
<tr>
|
||||
<td class="wrapper">
|
||||
<table role="presentation" class="maincontent" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td>
|
||||
<p class="hello">{{.Greeting}}</p>
|
||||
<p>{{.Text}}</p>
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td align="center">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td> <a href="{{.URL}}" target="_blank">{{.ButtonText}}</a> </td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
<!-- END MAIN CONTENT AREA -->
|
||||
</table>
|
||||
<!-- END CENTERED WHITE CONTAINER -->
|
||||
|
||||
<!-- START FOOTER -->
|
||||
<div class="footer">
|
||||
<table role="presentation" border="0" cellpadding="0" cellspacing="0">
|
||||
<tr>
|
||||
<td class="content-block">
|
||||
<span class="apple-link">CAOS AG | Teufener Strasse 19 | CH-9000 St.Gallen</span>
|
||||
<br> <a href="http://www.caos.ch">caos.ch</a>.
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<!-- END FOOTER -->
|
||||
|
||||
</div>
|
||||
</td>
|
||||
<td> </td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
8
go.mod
8
go.mod
@ -27,8 +27,14 @@ require (
|
||||
github.com/grpc-ecosystem/grpc-gateway v1.14.5
|
||||
github.com/huandu/xstrings v1.3.1 // indirect
|
||||
github.com/imdario/mergo v0.3.9 // indirect
|
||||
github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1 // indirect
|
||||
github.com/jinzhu/gorm v1.9.12
|
||||
github.com/kevinburke/go-types v0.0.0-20200309064045-f2d4aea18a7a // indirect
|
||||
github.com/kevinburke/go.uuid v1.2.0 // indirect
|
||||
github.com/kevinburke/rest v0.0.0-20200429221318-0d2892b400f8 // indirect
|
||||
github.com/kevinburke/twilio-go v0.0.0-20200424172635-4f0b2357b852
|
||||
github.com/lib/pq v1.5.2
|
||||
github.com/mattn/go-colorable v0.1.6 // indirect
|
||||
github.com/mitchellh/copystructure v1.0.0 // indirect
|
||||
github.com/mitchellh/reflectwalk v1.0.1 // indirect
|
||||
github.com/nicksnyder/go-i18n/v2 v2.0.3
|
||||
@ -38,6 +44,8 @@ require (
|
||||
github.com/sirupsen/logrus v1.6.0 // indirect
|
||||
github.com/sony/sonyflake v1.0.0
|
||||
github.com/stretchr/testify v1.5.1
|
||||
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 // indirect
|
||||
github.com/ttacon/libphonenumber v1.1.0
|
||||
go.opencensus.io v0.22.3
|
||||
golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37
|
||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f // indirect
|
||||
|
20
go.sum
20
go.sum
@ -97,6 +97,7 @@ github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG
|
||||
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
|
||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
@ -167,6 +168,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:
|
||||
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
|
||||
github.com/imdario/mergo v0.3.9 h1:UauaLniWCFHWd+Jp9oCEkTBj8VO/9DKg3PV3VCNMDIg=
|
||||
github.com/imdario/mergo v0.3.9/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1 h1:KUDFlmBg2buRWNzIcwLlKvfcnujcHQRQ1As1LoaCLAM=
|
||||
github.com/inconshreveable/log15 v0.0.0-20200109203555-b30bc20e4fd1/go.mod h1:cOaXtrgN4ScfRrD9Bre7U1thNq5RtJ8ZoP4iXVGRj6o=
|
||||
github.com/jinzhu/gorm v1.9.12 h1:Drgk1clyWT9t9ERbzHza6Mj/8FY/CqMyVzOiHviMo6Q=
|
||||
github.com/jinzhu/gorm v1.9.12/go.mod h1:vhTjlKSJUTWNtcbQtrMBFCxy7eXTzeCAzfL5fBZT/Qs=
|
||||
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||
@ -179,6 +182,14 @@ github.com/jmespath/go-jmespath v0.3.0 h1:OS12ieG61fsCg5+qLJ+SsW9NicxNkg3b25OyT2
|
||||
github.com/jmespath/go-jmespath v0.3.0/go.mod h1:9QtRXoHjLGCJ5IBSaohpXITPlowMeeYCZ7fLUTSywik=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
|
||||
github.com/kevinburke/go-types v0.0.0-20200309064045-f2d4aea18a7a h1:Z7+SSApKiwPjNic+NF9+j7h657Uyvdp/jA3iTKhpj4E=
|
||||
github.com/kevinburke/go-types v0.0.0-20200309064045-f2d4aea18a7a/go.mod h1:/Pk5i/SqYdYv1cie5wGwoZ4P6TpgMi+Yf58mtJSHdOw=
|
||||
github.com/kevinburke/go.uuid v1.2.0 h1:+1qP8NdkJfgOSTrrrUuA7h0djr1VY77HFXYjR+zUcUo=
|
||||
github.com/kevinburke/go.uuid v1.2.0/go.mod h1:9gVngk1Hq1FjwewVAjsWEUT+xc6jP+p62CASaGmQ0NQ=
|
||||
github.com/kevinburke/rest v0.0.0-20200429221318-0d2892b400f8 h1:KpuDJTaTPQAyWqETt70dHX3pMz65/XYTAZymrKKNvh8=
|
||||
github.com/kevinburke/rest v0.0.0-20200429221318-0d2892b400f8/go.mod h1:pD+iEcdAGVXld5foVN4e24zb/6fnb60tgZPZ3P/3T/I=
|
||||
github.com/kevinburke/twilio-go v0.0.0-20200424172635-4f0b2357b852 h1:wJMykIkD7A4tlwQNzqBJ23hkLlKtRKYeNNt+n8ASqWE=
|
||||
github.com/kevinburke/twilio-go v0.0.0-20200424172635-4f0b2357b852/go.mod h1:Fm9alkN1/LPVY1eqD/psyMwPWE4VWl4P01/nTYZKzBk=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@ -195,6 +206,10 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lib/pq v1.1.1/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/lib/pq v1.5.2 h1:yTSXVswvWUOQ3k1sd7vJfDrbSl8lKuscqFJRqjC0ifw=
|
||||
github.com/lib/pq v1.5.2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
||||
github.com/mattn/go-colorable v0.1.6 h1:6Su7aK7lXmJ/U79bYtBjLNaha4Fs1Rg9plHpcH+vvnE=
|
||||
github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY=
|
||||
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible h1:xQ15muvnzGBHpIpdrNi1DA5x0+TcBZzsIDwmw9uTHzw=
|
||||
github.com/mattn/go-sqlite3 v2.0.1+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
|
||||
@ -234,6 +249,10 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0=
|
||||
github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w=
|
||||
github.com/ttacon/libphonenumber v1.1.0 h1:tC6kE4t8UI4OqQVQjW5q8gSWhG2wnY5moEpSEORdYm4=
|
||||
github.com/ttacon/libphonenumber v1.1.0/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkUl+yR4OAxyEg/M=
|
||||
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
|
||||
@ -343,6 +362,7 @@ golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX
|
||||
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
@ -1,46 +1,19 @@
|
||||
package spooler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_locker "github.com/caos/zitadel/internal/eventstore/locker"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/cockroach-go/crdb"
|
||||
)
|
||||
|
||||
const (
|
||||
lockTable = "auth.locks"
|
||||
lockedUntilKey = "locked_until"
|
||||
lockerIDKey = "locker_id"
|
||||
objectTypeKey = "object_type"
|
||||
lockTable = "auth.locks"
|
||||
)
|
||||
|
||||
type locker struct {
|
||||
dbClient *sql.DB
|
||||
}
|
||||
|
||||
type lock struct {
|
||||
LockerID string `gorm:"column:locker_id;primary_key"`
|
||||
LockedUntil time.Time `gorm:"column:locked_until"`
|
||||
ViewName string `gorm:"column:object_type;primary_key"`
|
||||
}
|
||||
|
||||
func (l *locker) Renew(lockerID, viewModel string, waitTime time.Duration) error {
|
||||
return crdb.ExecuteTx(context.Background(), l.dbClient, nil, func(tx *sql.Tx) error {
|
||||
query := fmt.Sprintf("INSERT INTO %s (%s, %s, %s) VALUES ($1, $2, now()+$3) ON CONFLICT (%s) DO UPDATE SET %s = now()+$4, %s = $5 WHERE (locks.%s < now() OR locks.%s = $6) AND locks.%s = $7",
|
||||
lockTable, objectTypeKey, lockerIDKey, lockedUntilKey, objectTypeKey, lockedUntilKey, lockerIDKey, lockedUntilKey, lockerIDKey, objectTypeKey)
|
||||
|
||||
rs, err := tx.Exec(query, viewModel, lockerID, waitTime.Seconds(), waitTime.Seconds(), lockerID, lockerID, viewModel)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if rows, _ := rs.RowsAffected(); rows == 0 {
|
||||
tx.Rollback()
|
||||
return caos_errs.ThrowAlreadyExists(nil, "SPOOL-lso0e", "view already locked")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return es_locker.Renew(l.dbClient, lockTable, lockerID, viewModel, waitTime)
|
||||
}
|
||||
|
@ -3,6 +3,10 @@ package systemdefaults
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/notification/providers/chat"
|
||||
"github.com/caos/zitadel/internal/notification/providers/email"
|
||||
"github.com/caos/zitadel/internal/notification/providers/twilio"
|
||||
"github.com/caos/zitadel/internal/notification/templates"
|
||||
pol "github.com/caos/zitadel/internal/policy"
|
||||
)
|
||||
|
||||
@ -14,6 +18,7 @@ type SystemDefaults struct {
|
||||
DefaultPolicies DefaultPolicies
|
||||
IamID string
|
||||
SetUp types.IAMSetUp
|
||||
Notifications Notifications
|
||||
}
|
||||
|
||||
type SecretGenerators struct {
|
||||
@ -46,3 +51,29 @@ type DefaultPolicies struct {
|
||||
Complexity pol.PasswordComplexityPolicyDefault
|
||||
Lockout pol.PasswordLockoutPolicyDefault
|
||||
}
|
||||
|
||||
type Notifications struct {
|
||||
DebugMode bool
|
||||
Endpoints Endpoints
|
||||
Providers Providers
|
||||
TemplateData TemplateData
|
||||
}
|
||||
|
||||
type Endpoints struct {
|
||||
InitCode string
|
||||
PasswordReset string
|
||||
VerifyEmail string
|
||||
}
|
||||
|
||||
type Providers struct {
|
||||
Chat chat.ChatConfig
|
||||
Email email.EmailConfig
|
||||
Twilio twilio.TwilioConfig
|
||||
}
|
||||
|
||||
type TemplateData struct {
|
||||
InitCode templates.TemplateData
|
||||
PasswordReset templates.TemplateData
|
||||
VerifyEmail templates.TemplateData
|
||||
VerifyPhone templates.TemplateData
|
||||
}
|
||||
|
40
internal/eventstore/locker/lock.go
Normal file
40
internal/eventstore/locker/lock.go
Normal file
@ -0,0 +1,40 @@
|
||||
package locker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/cockroachdb/cockroach-go/crdb"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
lockedUntilKey = "locked_until"
|
||||
lockerIDKey = "locker_id"
|
||||
objectTypeKey = "object_type"
|
||||
)
|
||||
|
||||
type lock struct {
|
||||
LockerID string `gorm:"column:locker_id;primary_key"`
|
||||
LockedUntil time.Time `gorm:"column:locked_until"`
|
||||
ViewName string `gorm:"column:object_type;primary_key"`
|
||||
}
|
||||
|
||||
func Renew(dbClient *sql.DB, lockTable, lockerID, viewModel string, waitTime time.Duration) error {
|
||||
return crdb.ExecuteTx(context.Background(), dbClient, nil, func(tx *sql.Tx) error {
|
||||
query := fmt.Sprintf("INSERT INTO %s (%s, %s, %s) VALUES ($1, $2, now()+$3) ON CONFLICT (%s) DO UPDATE SET %s = now()+$4, %s = $5 WHERE (locks.%s < now() OR locks.%s = $6) AND locks.%s = $7",
|
||||
lockTable, objectTypeKey, lockerIDKey, lockedUntilKey, objectTypeKey, lockedUntilKey, lockerIDKey, lockedUntilKey, lockerIDKey, objectTypeKey)
|
||||
|
||||
rs, err := tx.Exec(query, viewModel, lockerID, waitTime.Seconds(), waitTime.Seconds(), lockerID, lockerID, viewModel)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if rows, _ := rs.RowsAffected(); rows == 0 {
|
||||
tx.Rollback()
|
||||
return caos_errs.ThrowAlreadyExists(nil, "SPOOL-lso0e", "view already locked")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
125
internal/eventstore/locker/lock_test.go
Normal file
125
internal/eventstore/locker/lock_test.go
Normal file
@ -0,0 +1,125 @@
|
||||
package locker
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
)
|
||||
|
||||
type dbMock struct {
|
||||
db *sql.DB
|
||||
mock sqlmock.Sqlmock
|
||||
}
|
||||
|
||||
func mockDB(t *testing.T) *dbMock {
|
||||
mockDB := dbMock{}
|
||||
var err error
|
||||
mockDB.db, mockDB.mock, err = sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("error occured while creating stub db %v", err)
|
||||
}
|
||||
|
||||
mockDB.mock.MatchExpectationsInOrder(true)
|
||||
|
||||
return &mockDB
|
||||
}
|
||||
|
||||
func (db *dbMock) expectCommit() *dbMock {
|
||||
db.mock.ExpectCommit()
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectRollback() *dbMock {
|
||||
db.mock.ExpectRollback()
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectBegin() *dbMock {
|
||||
db.mock.ExpectBegin()
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectSavepoint() *dbMock {
|
||||
db.mock.ExpectExec("SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectReleaseSavepoint() *dbMock {
|
||||
db.mock.ExpectExec("RELEASE SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectRenew(lockerID, view string, affectedRows int64) *dbMock {
|
||||
query := db.mock.
|
||||
ExpectExec(`INSERT INTO table\.locks \(object_type, locker_id, locked_until\) VALUES \(\$1, \$2, now\(\)\+\$3\) ON CONFLICT \(object_type\) DO UPDATE SET locked_until = now\(\)\+\$4, locker_id = \$5 WHERE \(locks\.locked_until < now\(\) OR locks\.locker_id = \$6\) AND locks\.object_type = \$7`).
|
||||
WithArgs(view, lockerID, sqlmock.AnyArg(), sqlmock.AnyArg(), lockerID, lockerID, view).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
if affectedRows == 0 {
|
||||
query.WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
} else {
|
||||
query.WillReturnResult(sqlmock.NewResult(1, affectedRows))
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func Test_locker_Renew(t *testing.T) {
|
||||
type fields struct {
|
||||
db *dbMock
|
||||
}
|
||||
type args struct {
|
||||
tableName string
|
||||
lockerID string
|
||||
viewModel string
|
||||
waitTime time.Duration
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "renew succeeded",
|
||||
fields: fields{
|
||||
db: mockDB(t).
|
||||
expectBegin().
|
||||
expectSavepoint().
|
||||
expectRenew("locker", "view", 1).
|
||||
expectReleaseSavepoint().
|
||||
expectCommit(),
|
||||
},
|
||||
args: args{tableName: "table.locks", lockerID: "locker", viewModel: "view", waitTime: 1 * time.Second},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "renew now rows updated",
|
||||
fields: fields{
|
||||
db: mockDB(t).
|
||||
expectBegin().
|
||||
expectSavepoint().
|
||||
expectRenew("locker", "view", 0).
|
||||
expectRollback(),
|
||||
},
|
||||
args: args{tableName: "table.locks", lockerID: "locker", viewModel: "view", waitTime: 1 * time.Second},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := Renew(tt.fields.db.db, tt.args.tableName, tt.args.lockerID, tt.args.viewModel, tt.args.waitTime); (err != nil) != tt.wantErr {
|
||||
t.Errorf("locker.Renew() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if err := tt.fields.db.mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("not all database expectations met: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"github.com/caos/zitadel/internal/management/repository/eventsourcing/view"
|
||||
@ -12,7 +13,7 @@ import (
|
||||
type Configs map[string]*Config
|
||||
|
||||
type Config struct {
|
||||
MinimumCycleDurationMillisecond int
|
||||
MinimumCycleDuration types.Duration
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
@ -31,8 +32,8 @@ func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, ev
|
||||
return []spooler.Handler{
|
||||
&GrantedProject{handler: handler{view, bulkLimit, configs.cycleDuration("GrantedProject"), errorCount}, eventstore: eventstore, projectEvents: repos.ProjectEvents},
|
||||
&ProjectRole{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectRole"), errorCount}, projectEvents: repos.ProjectEvents},
|
||||
&ProjectMember{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectMember"), errorCount}},
|
||||
&ProjectGrantMember{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectGrantMember"), errorCount}},
|
||||
&ProjectMember{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectMember"), errorCount}, userEvents: repos.UserEvents},
|
||||
&ProjectGrantMember{handler: handler{view, bulkLimit, configs.cycleDuration("ProjectGrantMember"), errorCount}, userEvents: repos.UserEvents},
|
||||
&Application{handler: handler{view, bulkLimit, configs.cycleDuration("Application"), errorCount}},
|
||||
&User{handler: handler{view, bulkLimit, configs.cycleDuration("User"), errorCount}},
|
||||
&UserGrant{handler: handler{view, bulkLimit, configs.cycleDuration("UserGrant"), errorCount}, projectEvents: repos.ProjectEvents, userEvents: repos.UserEvents},
|
||||
@ -44,5 +45,5 @@ func (configs Configs) cycleDuration(viewModel string) time.Duration {
|
||||
if !ok {
|
||||
return 1 * time.Second
|
||||
}
|
||||
return time.Duration(c.MinimumCycleDurationMillisecond) * time.Millisecond
|
||||
return c.MinimumCycleDuration.Duration
|
||||
}
|
||||
|
@ -79,7 +79,7 @@ func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error)
|
||||
}
|
||||
org := es_org.StartOrg(es_org.OrgConfig{Eventstore: es})
|
||||
|
||||
eventstoreRepos := handler.EventstoreRepos{ProjectEvents: project}
|
||||
eventstoreRepos := handler.EventstoreRepos{ProjectEvents: project, UserEvents: user}
|
||||
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, eventstoreRepos)
|
||||
|
||||
return &EsRepository{
|
||||
|
@ -1,46 +1,19 @@
|
||||
package spooler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_locker "github.com/caos/zitadel/internal/eventstore/locker"
|
||||
"time"
|
||||
|
||||
"github.com/cockroachdb/cockroach-go/crdb"
|
||||
)
|
||||
|
||||
const (
|
||||
lockTable = "management.locks"
|
||||
lockedUntilKey = "locked_until"
|
||||
lockerIDKey = "locker_id"
|
||||
objectTypeKey = "object_type"
|
||||
lockTable = "management.locks"
|
||||
)
|
||||
|
||||
type locker struct {
|
||||
dbClient *sql.DB
|
||||
}
|
||||
|
||||
type lock struct {
|
||||
LockerID string `gorm:"column:locker_id;primary_key"`
|
||||
LockedUntil time.Time `gorm:"column:locked_until"`
|
||||
ViewName string `gorm:"column:object_type;primary_key"`
|
||||
}
|
||||
|
||||
func (l *locker) Renew(lockerID, viewModel string, waitTime time.Duration) error {
|
||||
return crdb.ExecuteTx(context.Background(), l.dbClient, nil, func(tx *sql.Tx) error {
|
||||
query := fmt.Sprintf("INSERT INTO %s (%s, %s, %s) VALUES ($1, $2, now()+$3) ON CONFLICT (%s) DO UPDATE SET %s = now()+$4, %s = $5 WHERE (locks.%s < now() OR locks.%s = $6) AND locks.%s = $7",
|
||||
lockTable, objectTypeKey, lockerIDKey, lockedUntilKey, objectTypeKey, lockedUntilKey, lockerIDKey, lockedUntilKey, lockerIDKey, objectTypeKey)
|
||||
|
||||
rs, err := tx.Exec(query, viewModel, lockerID, waitTime.Seconds(), waitTime.Seconds(), lockerID, lockerID, viewModel)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if rows, _ := rs.RowsAffected(); rows == 0 {
|
||||
tx.Rollback()
|
||||
return caos_errs.ThrowAlreadyExists(nil, "SPOOL-lso0e", "view already locked")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return es_locker.Renew(l.dbClient, lockTable, lockerID, viewModel, waitTime)
|
||||
}
|
||||
|
17
internal/notification/notification.go
Normal file
17
internal/notification/notification.go
Normal file
@ -0,0 +1,17 @@
|
||||
package notification
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/logging"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/notification/repository/eventsourcing"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Repository eventsourcing.Config
|
||||
}
|
||||
|
||||
func Start(ctx context.Context, config Config, systemDefaults sd.SystemDefaults) {
|
||||
_, err := eventsourcing.Start(config.Repository, systemDefaults)
|
||||
logging.Log("MAIN-9uBxp").OnError(err).Panic("unable to start app")
|
||||
}
|
6
internal/notification/providers/chat/config.go
Normal file
6
internal/notification/providers/chat/config.go
Normal file
@ -0,0 +1,6 @@
|
||||
package chat
|
||||
|
||||
type ChatConfig struct {
|
||||
Url string
|
||||
SplitCount int
|
||||
}
|
9
internal/notification/providers/chat/message.go
Normal file
9
internal/notification/providers/chat/message.go
Normal file
@ -0,0 +1,9 @@
|
||||
package chat
|
||||
|
||||
type ChatMessage struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
func (msg *ChatMessage) GetContent() string {
|
||||
return msg.Text
|
||||
}
|
75
internal/notification/providers/chat/provider.go
Normal file
75
internal/notification/providers/chat/provider.go
Normal file
@ -0,0 +1,75 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/notification/providers"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type Chat struct {
|
||||
URL *url.URL
|
||||
SplitCount int
|
||||
}
|
||||
|
||||
func InitChatProvider(config ChatConfig) (*Chat, error) {
|
||||
url, err := url.Parse(config.Url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Chat{
|
||||
URL: url,
|
||||
SplitCount: config.SplitCount,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (chat *Chat) CanHandleMessage(_ providers.Message) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (chat *Chat) HandleMessage(message providers.Message) error {
|
||||
contentText := message.GetContent()
|
||||
for _, splittedMsg := range splitMessage(contentText, chat.SplitCount) {
|
||||
chatMsg := &ChatMessage{Text: splittedMsg}
|
||||
if err := chat.SendMessage(chatMsg); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (chat *Chat) SendMessage(message providers.Message) error {
|
||||
chatMsg, ok := message.(*ChatMessage)
|
||||
if !ok {
|
||||
return caos_errs.ThrowInternal(nil, "EMAIL-s8JLs", "message is not ChatMessage")
|
||||
}
|
||||
req, err := json.Marshal(chatMsg)
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInternal(err, "PROVI-s8uie", "Could not unmarshal content")
|
||||
}
|
||||
|
||||
_, err = http.Post(chat.URL.String(), "application/json; charset=UTF-8", bytes.NewReader(req))
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInternal(err, "PROVI-si93s", "unable to send message")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func splitMessage(message string, count int) []string {
|
||||
if count == 0 {
|
||||
return []string{message}
|
||||
}
|
||||
var splits []string
|
||||
var l, r int
|
||||
for l, r = 0, count; r < len(message); l, r = r, r+count {
|
||||
for !utf8.RuneStart(message[r]) {
|
||||
r--
|
||||
}
|
||||
splits = append(splits, message[l:r])
|
||||
}
|
||||
splits = append(splits, message[l:])
|
||||
return splits
|
||||
}
|
18
internal/notification/providers/email/config.go
Normal file
18
internal/notification/providers/email/config.go
Normal file
@ -0,0 +1,18 @@
|
||||
package email
|
||||
|
||||
type EmailConfig struct {
|
||||
SMTP SMTP
|
||||
Tls bool
|
||||
From string
|
||||
FromName string
|
||||
}
|
||||
|
||||
type SMTP struct {
|
||||
Host string
|
||||
User string
|
||||
Password string
|
||||
}
|
||||
|
||||
func (smtp *SMTP) HasAuth() bool {
|
||||
return smtp.User != "" && smtp.Password != ""
|
||||
}
|
47
internal/notification/providers/email/message.go
Normal file
47
internal/notification/providers/email/message.go
Normal file
@ -0,0 +1,47 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
isHTMLRgx = regexp.MustCompile(`.*<html.*>.*`)
|
||||
lineBreak = "\r\n"
|
||||
)
|
||||
|
||||
type EmailMessage struct {
|
||||
Recipients []string
|
||||
BCC []string
|
||||
CC []string
|
||||
SenderEmail string
|
||||
Subject string
|
||||
Content string
|
||||
}
|
||||
|
||||
func (msg *EmailMessage) GetContent() string {
|
||||
headers := make(map[string]string)
|
||||
headers["From"] = msg.SenderEmail
|
||||
headers["To"] = strings.Join(msg.Recipients, ", ")
|
||||
headers["Cc"] = strings.Join(msg.CC, ", ")
|
||||
|
||||
message := ""
|
||||
for k, v := range headers {
|
||||
message += fmt.Sprintf("%s: %s"+lineBreak, k, v)
|
||||
}
|
||||
|
||||
//default mime-type is html
|
||||
mime := "MIME-version: 1.0;" + lineBreak + "Content-Type: text/html; charset=\"UTF-8\";" + lineBreak + lineBreak
|
||||
if !isHTML(msg.Content) {
|
||||
mime = "MIME-version: 1.0;" + lineBreak + "Content-Type: text/plain; charset=\"UTF-8\";" + lineBreak + lineBreak
|
||||
}
|
||||
subject := "Subject: " + msg.Subject + lineBreak
|
||||
message += subject + mime + lineBreak + msg.Content
|
||||
|
||||
return message
|
||||
}
|
||||
|
||||
func isHTML(input string) bool {
|
||||
return isHTMLRgx.MatchString(input)
|
||||
}
|
123
internal/notification/providers/email/provider.go
Normal file
123
internal/notification/providers/email/provider.go
Normal file
@ -0,0 +1,123 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/notification/providers"
|
||||
"net"
|
||||
"net/smtp"
|
||||
)
|
||||
|
||||
type Email struct {
|
||||
smtpClient *smtp.Client
|
||||
}
|
||||
|
||||
func InitEmailProvider(config EmailConfig) (*Email, error) {
|
||||
client, err := config.SMTP.connectToSMTP(config.Tls)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &Email{
|
||||
smtpClient: client,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (email *Email) CanHandleMessage(message providers.Message) bool {
|
||||
msg, ok := message.(*EmailMessage)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return msg.Content != "" && msg.Subject != "" && len(msg.Recipients) > 0
|
||||
}
|
||||
|
||||
func (email *Email) HandleMessage(message providers.Message) error {
|
||||
defer email.smtpClient.Close()
|
||||
emailMsg, ok := message.(*EmailMessage)
|
||||
if !ok {
|
||||
return caos_errs.ThrowInternal(nil, "EMAIL-s8JLs", "message is not EmailMessage")
|
||||
}
|
||||
// To && From
|
||||
if err := email.smtpClient.Mail(emailMsg.SenderEmail); err != nil {
|
||||
return caos_errs.ThrowInternalf(err, "EMAIL-s3is3", "could not set sender: %v", emailMsg.SenderEmail)
|
||||
}
|
||||
|
||||
for _, recp := range append(append(emailMsg.Recipients, emailMsg.CC...), emailMsg.BCC...) {
|
||||
if err := email.smtpClient.Rcpt(recp); err != nil {
|
||||
return caos_errs.ThrowInternalf(err, "EMAIL-s4is4", "could not set recipient: %v", recp)
|
||||
}
|
||||
}
|
||||
|
||||
// Data
|
||||
w, err := email.smtpClient.Data()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = w.Write([]byte(emailMsg.GetContent()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = w.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer logging.LogWithFields("EMAI-a1c87ec8").Debug("email sent")
|
||||
return email.smtpClient.Quit()
|
||||
}
|
||||
|
||||
func (smtpConfig SMTP) connectToSMTP(tlsRequired bool) (client *smtp.Client, err error) {
|
||||
host, _, err := net.SplitHostPort(smtpConfig.Host)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "EMAIL-spR56", "could not split host and port for connect to smtp")
|
||||
}
|
||||
|
||||
if !tlsRequired {
|
||||
client, err = smtpConfig.getSMPTClient()
|
||||
} else {
|
||||
client, err = smtpConfig.getSMPTClientWithTls(host)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = smtpConfig.smtpAuth(client, host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (smtpConfig SMTP) getSMPTClient() (*smtp.Client, error) {
|
||||
client, err := smtp.Dial(smtpConfig.Host)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "EMAIL-skwos", "Could not make smtp dial")
|
||||
}
|
||||
return client, nil
|
||||
}
|
||||
|
||||
func (smtpConfig SMTP) getSMPTClientWithTls(host string) (*smtp.Client, error) {
|
||||
conn, err := tls.Dial("tcp", smtpConfig.Host, &tls.Config{})
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "EMAIL-sl39s", "Could not make tls dial")
|
||||
}
|
||||
|
||||
client, err := smtp.NewClient(conn, host)
|
||||
if err != nil {
|
||||
return nil, caos_errs.ThrowInternal(err, "EMAIL-skwi4", "Could not create smtp client")
|
||||
}
|
||||
return client, err
|
||||
}
|
||||
|
||||
func (smtpConfig SMTP) smtpAuth(client *smtp.Client, host string) error {
|
||||
if !smtpConfig.HasAuth() {
|
||||
return nil
|
||||
}
|
||||
// Auth
|
||||
auth := smtp.PlainAuth("", smtpConfig.User, smtpConfig.Password, host)
|
||||
err := client.Auth(auth)
|
||||
logging.Log("EMAIL-s9kfs").WithField("smtp user", smtpConfig.User).OnError(err).Debug("Could not add smtp auth")
|
||||
return err
|
||||
}
|
4
internal/notification/providers/gen_mock.go
Normal file
4
internal/notification/providers/gen_mock.go
Normal file
@ -0,0 +1,4 @@
|
||||
package providers
|
||||
|
||||
//go:generate mockgen -package mock -destination ./mock/provider.mock.go github.com/caos/zitadel/internal/notification/providers NotificationProvider
|
||||
//go:generate mockgen -package mock -destination ./mock/message.mock.go github.com/caos/zitadel/internal/notification/providers Message
|
5
internal/notification/providers/message.go
Normal file
5
internal/notification/providers/message.go
Normal file
@ -0,0 +1,5 @@
|
||||
package providers
|
||||
|
||||
type Message interface {
|
||||
GetContent() string
|
||||
}
|
47
internal/notification/providers/mock/message.mock.go
Normal file
47
internal/notification/providers/mock/message.mock.go
Normal file
@ -0,0 +1,47 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/caos/zitadel/internal/notification/providers (interfaces: Message)
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockMessage is a mock of Message interface
|
||||
type MockMessage struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockMessageMockRecorder
|
||||
}
|
||||
|
||||
// MockMessageMockRecorder is the mock recorder for MockMessage
|
||||
type MockMessageMockRecorder struct {
|
||||
mock *MockMessage
|
||||
}
|
||||
|
||||
// NewMockMessage creates a new mock instance
|
||||
func NewMockMessage(ctrl *gomock.Controller) *MockMessage {
|
||||
mock := &MockMessage{ctrl: ctrl}
|
||||
mock.recorder = &MockMessageMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockMessage) EXPECT() *MockMessageMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// GetContent mocks base method
|
||||
func (m *MockMessage) GetContent() string {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetContent")
|
||||
ret0, _ := ret[0].(string)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// GetContent indicates an expected call of GetContent
|
||||
func (mr *MockMessageMockRecorder) GetContent() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetContent", reflect.TypeOf((*MockMessage)(nil).GetContent))
|
||||
}
|
61
internal/notification/providers/mock/provider.mock.go
Normal file
61
internal/notification/providers/mock/provider.mock.go
Normal file
@ -0,0 +1,61 @@
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: github.com/caos/zitadel/internal/notification/providers (interfaces: NotificationProvider)
|
||||
|
||||
// Package mock is a generated GoMock package.
|
||||
package mock
|
||||
|
||||
import (
|
||||
gomock "github.com/golang/mock/gomock"
|
||||
reflect "reflect"
|
||||
)
|
||||
|
||||
// MockNotificationProvider is a mock of NotificationProvider interface
|
||||
type MockNotificationProvider struct {
|
||||
ctrl *gomock.Controller
|
||||
recorder *MockNotificationProviderMockRecorder
|
||||
}
|
||||
|
||||
// MockNotificationProviderMockRecorder is the mock recorder for MockNotificationProvider
|
||||
type MockNotificationProviderMockRecorder struct {
|
||||
mock *MockNotificationProvider
|
||||
}
|
||||
|
||||
// NewMockNotificationProvider creates a new mock instance
|
||||
func NewMockNotificationProvider(ctrl *gomock.Controller) *MockNotificationProvider {
|
||||
mock := &MockNotificationProvider{ctrl: ctrl}
|
||||
mock.recorder = &MockNotificationProviderMockRecorder{mock}
|
||||
return mock
|
||||
}
|
||||
|
||||
// EXPECT returns an object that allows the caller to indicate expected use
|
||||
func (m *MockNotificationProvider) EXPECT() *MockNotificationProviderMockRecorder {
|
||||
return m.recorder
|
||||
}
|
||||
|
||||
// CanHandleMessage mocks base method
|
||||
func (m *MockNotificationProvider) CanHandleMessage() bool {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "CanHandleMessage")
|
||||
ret0, _ := ret[0].(bool)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// CanHandleMessage indicates an expected call of CanHandleMessage
|
||||
func (mr *MockNotificationProviderMockRecorder) CanHandleMessage() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CanHandleMessage", reflect.TypeOf((*MockNotificationProvider)(nil).CanHandleMessage))
|
||||
}
|
||||
|
||||
// HandleMessage mocks base method
|
||||
func (m *MockNotificationProvider) HandleMessage() error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "HandleMessage")
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// HandleMessage indicates an expected call of HandleMessage
|
||||
func (mr *MockNotificationProviderMockRecorder) HandleMessage() *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleMessage", reflect.TypeOf((*MockNotificationProvider)(nil).HandleMessage))
|
||||
}
|
6
internal/notification/providers/provider.go
Normal file
6
internal/notification/providers/provider.go
Normal file
@ -0,0 +1,6 @@
|
||||
package providers
|
||||
|
||||
type NotificationProvider interface {
|
||||
CanHandleMessage() bool
|
||||
HandleMessage() error
|
||||
}
|
7
internal/notification/providers/twilio/config.go
Normal file
7
internal/notification/providers/twilio/config.go
Normal file
@ -0,0 +1,7 @@
|
||||
package twilio
|
||||
|
||||
type TwilioConfig struct {
|
||||
SID string
|
||||
Token string
|
||||
From string
|
||||
}
|
11
internal/notification/providers/twilio/message.go
Normal file
11
internal/notification/providers/twilio/message.go
Normal file
@ -0,0 +1,11 @@
|
||||
package twilio
|
||||
|
||||
type TwilioMessage struct {
|
||||
SenderPhoneNumber string
|
||||
RecipientPhoneNumber string
|
||||
Content string
|
||||
}
|
||||
|
||||
func (msg *TwilioMessage) GetContent() string {
|
||||
return msg.Content
|
||||
}
|
39
internal/notification/providers/twilio/provider.go
Normal file
39
internal/notification/providers/twilio/provider.go
Normal file
@ -0,0 +1,39 @@
|
||||
package twilio
|
||||
|
||||
import (
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/notification/providers"
|
||||
twilio "github.com/kevinburke/twilio-go"
|
||||
)
|
||||
|
||||
type Twilio struct {
|
||||
client *twilio.Client
|
||||
}
|
||||
|
||||
func InitTwilioProvider(config TwilioConfig) *Twilio {
|
||||
return &Twilio{
|
||||
client: twilio.NewClient(config.SID, config.Token, nil),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Twilio) CanHandleMessage(message providers.Message) bool {
|
||||
twilioMsg, ok := message.(*TwilioMessage)
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
return twilioMsg.Content != "" && twilioMsg.RecipientPhoneNumber != "" && twilioMsg.SenderPhoneNumber != ""
|
||||
}
|
||||
|
||||
func (t *Twilio) HandleMessage(message providers.Message) error {
|
||||
twilioMsg, ok := message.(*TwilioMessage)
|
||||
if !ok {
|
||||
return caos_errs.ThrowInternal(nil, "TWILI-s0pLc", "message is not TwilioMessage")
|
||||
}
|
||||
m, err := t.client.Messages.SendMessage(twilioMsg.SenderPhoneNumber, twilioMsg.RecipientPhoneNumber, twilioMsg.GetContent(), nil)
|
||||
if err != nil {
|
||||
return caos_errs.ThrowInternal(err, "TWILI-osk3S", "could not send message")
|
||||
}
|
||||
logging.LogWithFields("SMS_-f335c523", "message_sid", m.Sid, "status", m.Status).Debug("sms sent")
|
||||
return nil
|
||||
}
|
@ -0,0 +1,49 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"github.com/caos/logging"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/view"
|
||||
usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Configs map[string]*Config
|
||||
|
||||
type Config struct {
|
||||
MinimumCycleDuration types.Duration
|
||||
}
|
||||
|
||||
type handler struct {
|
||||
view *view.View
|
||||
bulkLimit uint64
|
||||
cycleDuration time.Duration
|
||||
errorCountUntilSkip uint64
|
||||
}
|
||||
|
||||
type EventstoreRepos struct {
|
||||
UserEvents *usr_event.UserEventstore
|
||||
}
|
||||
|
||||
func Register(configs Configs, bulkLimit, errorCount uint64, view *view.View, eventstore eventstore.Eventstore, repos EventstoreRepos, systemDefaults sd.SystemDefaults) []spooler.Handler {
|
||||
aesCrypto, err := crypto.NewAESCrypto(systemDefaults.UserVerificationKey)
|
||||
if err != nil {
|
||||
logging.Log("HANDL-s90ew").WithError(err).Debug("error create new aes crypto")
|
||||
}
|
||||
return []spooler.Handler{
|
||||
&NotifyUser{handler: handler{view, bulkLimit, configs.cycleDuration("User"), errorCount}},
|
||||
&Notification{handler: handler{view, bulkLimit, configs.cycleDuration("Notification"), errorCount}, eventstore: eventstore, userEvents: repos.UserEvents, systemDefaults: systemDefaults, AesCrypto: aesCrypto},
|
||||
}
|
||||
}
|
||||
|
||||
func (configs Configs) cycleDuration(viewModel string) time.Duration {
|
||||
c, ok := configs[viewModel]
|
||||
if !ok {
|
||||
return 1 * time.Second
|
||||
}
|
||||
return c.MinimumCycleDuration.Duration
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"github.com/caos/zitadel/internal/api/auth"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/notification/types"
|
||||
usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
)
|
||||
|
||||
type Notification struct {
|
||||
handler
|
||||
eventstore eventstore.Eventstore
|
||||
userEvents *usr_event.UserEventstore
|
||||
systemDefaults sd.SystemDefaults
|
||||
AesCrypto crypto.EncryptionAlgorithm
|
||||
}
|
||||
|
||||
const (
|
||||
notificationTable = "notification.notifications"
|
||||
NOTIFY_USER = "NOTIFICATION"
|
||||
)
|
||||
|
||||
func (n *Notification) MinimumCycleDuration() time.Duration { return n.cycleDuration }
|
||||
|
||||
func (n *Notification) ViewModel() string {
|
||||
return notificationTable
|
||||
}
|
||||
|
||||
func (n *Notification) EventQuery() (*models.SearchQuery, error) {
|
||||
sequence, err := n.view.GetLatestNotificationSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return eventsourcing.UserQuery(sequence), nil
|
||||
}
|
||||
|
||||
func (n *Notification) Process(event *models.Event) (err error) {
|
||||
switch event.Type {
|
||||
case es_model.InitializedUserCodeAdded:
|
||||
err = n.handleInitUserCode(event)
|
||||
case es_model.UserEmailCodeAdded:
|
||||
err = n.handleEmailVerificationCode(event)
|
||||
case es_model.UserPhoneCodeAdded:
|
||||
err = n.handlePhoneVerificationCode(event)
|
||||
case es_model.UserPasswordCodeAdded:
|
||||
err = n.handlePasswordCode(event)
|
||||
default:
|
||||
return n.view.ProcessedNotificationSequence(event.Sequence)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.view.ProcessedNotificationSequence(event.Sequence)
|
||||
}
|
||||
|
||||
func (n *Notification) handleInitUserCode(event *models.Event) (err error) {
|
||||
alreadyHandled, err := n.checkIfCodeAlreadyHandled(event.AggregateID, event.Sequence, es_model.InitializedUserCodeAdded, es_model.InitializedUserCodeSent)
|
||||
if err != nil || alreadyHandled {
|
||||
return err
|
||||
}
|
||||
initCode := new(es_model.InitUserCode)
|
||||
initCode.SetData(event)
|
||||
user, err := n.view.NotifyUserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendUserInitCode(user, initCode, n.systemDefaults, n.AesCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.userEvents.InitCodeSent(getSetNotifyContextData(event.ResourceOwner), event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) handlePasswordCode(event *models.Event) (err error) {
|
||||
alreadyHandled, err := n.checkIfCodeAlreadyHandled(event.AggregateID, event.Sequence, es_model.UserPasswordCodeAdded, es_model.UserPasswordCodeSent)
|
||||
if err != nil || alreadyHandled {
|
||||
return err
|
||||
}
|
||||
pwCode := new(es_model.PasswordCode)
|
||||
pwCode.SetData(event)
|
||||
user, err := n.view.NotifyUserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendPasswordCode(user, pwCode, n.systemDefaults, n.AesCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.userEvents.PasswordCodeSent(getSetNotifyContextData(event.ResourceOwner), event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) handleEmailVerificationCode(event *models.Event) (err error) {
|
||||
alreadyHandled, err := n.checkIfCodeAlreadyHandled(event.AggregateID, event.Sequence, es_model.UserEmailCodeAdded, es_model.UserEmailCodeSent)
|
||||
if err != nil || alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
emailCode := new(es_model.EmailCode)
|
||||
emailCode.SetData(event)
|
||||
user, err := n.view.NotifyUserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendEmailVerificationCode(user, emailCode, n.systemDefaults, n.AesCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.userEvents.EmailVerificationCodeSent(getSetNotifyContextData(event.ResourceOwner), event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) handlePhoneVerificationCode(event *models.Event) (err error) {
|
||||
alreadyHandled, err := n.checkIfCodeAlreadyHandled(event.AggregateID, event.Sequence, es_model.UserPhoneCodeAdded, es_model.UserPhoneCodeSent)
|
||||
if err != nil || alreadyHandled {
|
||||
return nil
|
||||
}
|
||||
phoneCode := new(es_model.PhoneCode)
|
||||
phoneCode.SetData(event)
|
||||
user, err := n.view.NotifyUserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = types.SendPhoneVerificationCode(user, phoneCode, n.systemDefaults, n.AesCrypto)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return n.userEvents.PhoneVerificationCodeSent(getSetNotifyContextData(event.ResourceOwner), event.AggregateID)
|
||||
}
|
||||
|
||||
func (n *Notification) checkIfCodeAlreadyHandled(userID string, sequence uint64, addedType, sentType models.EventType) (bool, error) {
|
||||
events, err := n.getUserEvents(userID, sequence)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
for _, event := range events {
|
||||
if event.Type == addedType || event.Type == sentType {
|
||||
return true, nil
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (n *Notification) getUserEvents(userID string, sequence uint64) ([]*models.Event, error) {
|
||||
query, err := eventsourcing.UserByIDQuery(userID, sequence)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return n.eventstore.FilterEvents(context.Background(), query)
|
||||
}
|
||||
|
||||
func (n *Notification) OnError(event *models.Event, err error) error {
|
||||
logging.LogWithFields("SPOOL-s9opc", "id", event.AggregateID, "sequence", event.Sequence).WithError(err).Warn("something went wrong in notification handler")
|
||||
return spooler.HandleError(event, err, n.view.GetLatestNotificationFailedEvent, n.view.ProcessedNotificationFailedEvent, n.view.ProcessedNotificationSequence, n.errorCountUntilSkip)
|
||||
}
|
||||
|
||||
func getSetNotifyContextData(orgID string) context.Context {
|
||||
return auth.SetCtxData(context.Background(), auth.CtxData{UserID: NOTIFY_USER, OrgID: orgID})
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"time"
|
||||
|
||||
"github.com/caos/logging"
|
||||
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type NotifyUser struct {
|
||||
handler
|
||||
eventstore eventstore.Eventstore
|
||||
}
|
||||
|
||||
const (
|
||||
userTable = "notification.notify_users"
|
||||
)
|
||||
|
||||
func (p *NotifyUser) MinimumCycleDuration() time.Duration { return p.cycleDuration }
|
||||
|
||||
func (p *NotifyUser) ViewModel() string {
|
||||
return userTable
|
||||
}
|
||||
|
||||
func (p *NotifyUser) EventQuery() (*models.SearchQuery, error) {
|
||||
sequence, err := p.view.GetLatestNotifyUserSequence()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return eventsourcing.UserQuery(sequence), nil
|
||||
}
|
||||
|
||||
func (p *NotifyUser) Process(event *models.Event) (err error) {
|
||||
user := new(view_model.NotifyUser)
|
||||
switch event.Type {
|
||||
case es_model.UserAdded,
|
||||
es_model.UserRegistered:
|
||||
user.AppendEvent(event)
|
||||
case es_model.UserProfileChanged,
|
||||
es_model.UserEmailChanged,
|
||||
es_model.UserEmailVerified,
|
||||
es_model.UserPhoneChanged,
|
||||
es_model.UserPhoneVerified:
|
||||
user, err = p.view.NotifyUserByID(event.AggregateID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = user.AppendEvent(event)
|
||||
case es_model.UserDeleted:
|
||||
err = p.view.DeleteNotifyUser(event.AggregateID, event.Sequence)
|
||||
default:
|
||||
return p.view.ProcessedNotifyUserSequence(event.Sequence)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return p.view.PutNotifyUser(user)
|
||||
}
|
||||
|
||||
func (p *NotifyUser) OnError(event *models.Event, err error) error {
|
||||
logging.LogWithFields("SPOOL-9spwf", "id", event.AggregateID).WithError(err).Warn("something went wrong in notify user handler")
|
||||
return spooler.HandleError(event, err, p.view.GetLatestNotifyUserFailedEvent, p.view.ProcessedNotifyUserFailedEvent, p.view.ProcessedNotifyUserSequence, p.errorCountUntilSkip)
|
||||
}
|
56
internal/notification/repository/eventsourcing/repository.go
Normal file
56
internal/notification/repository/eventsourcing/repository.go
Normal file
@ -0,0 +1,56 @@
|
||||
package eventsourcing
|
||||
|
||||
import (
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/config/types"
|
||||
es_int "github.com/caos/zitadel/internal/eventstore"
|
||||
es_spol "github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/handler"
|
||||
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/spooler"
|
||||
noti_view "github.com/caos/zitadel/internal/notification/repository/eventsourcing/view"
|
||||
es_usr "github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Eventstore es_int.Config
|
||||
View types.SQL
|
||||
Spooler spooler.SpoolerConfig
|
||||
}
|
||||
|
||||
type EsRepository struct {
|
||||
spooler *es_spol.Spooler
|
||||
}
|
||||
|
||||
func Start(conf Config, systemDefaults sd.SystemDefaults) (*EsRepository, error) {
|
||||
es, err := es_int.Start(conf.Eventstore)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sqlClient, err := conf.View.Start()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
view, err := noti_view.StartView(sqlClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user, err := es_usr.StartUser(es_usr.UserConfig{
|
||||
Eventstore: es,
|
||||
Cache: conf.Eventstore.Cache,
|
||||
}, systemDefaults)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
eventstoreRepos := handler.EventstoreRepos{UserEvents: user}
|
||||
spool := spooler.StartSpooler(conf.Spooler, es, view, sqlClient, eventstoreRepos, systemDefaults)
|
||||
|
||||
return &EsRepository{
|
||||
spool,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (repo *EsRepository) Health() error {
|
||||
return nil
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package spooler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
es_locker "github.com/caos/zitadel/internal/eventstore/locker"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
lockTable = "notification.locks"
|
||||
)
|
||||
|
||||
type locker struct {
|
||||
dbClient *sql.DB
|
||||
}
|
||||
|
||||
func (l *locker) Renew(lockerID, viewModel string, waitTime time.Duration) error {
|
||||
return es_locker.Renew(l.dbClient, lockTable, lockerID, viewModel, waitTime)
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
package spooler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
)
|
||||
|
||||
type dbMock struct {
|
||||
db *sql.DB
|
||||
mock sqlmock.Sqlmock
|
||||
}
|
||||
|
||||
func mockDB(t *testing.T) *dbMock {
|
||||
mockDB := dbMock{}
|
||||
var err error
|
||||
mockDB.db, mockDB.mock, err = sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("error occured while creating stub db %v", err)
|
||||
}
|
||||
|
||||
mockDB.mock.MatchExpectationsInOrder(true)
|
||||
|
||||
return &mockDB
|
||||
}
|
||||
|
||||
func (db *dbMock) expectCommit() *dbMock {
|
||||
db.mock.ExpectCommit()
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectRollback() *dbMock {
|
||||
db.mock.ExpectRollback()
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectBegin() *dbMock {
|
||||
db.mock.ExpectBegin()
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectSavepoint() *dbMock {
|
||||
db.mock.ExpectExec("SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectReleaseSavepoint() *dbMock {
|
||||
db.mock.ExpectExec("RELEASE SAVEPOINT").WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func (db *dbMock) expectRenew(lockerID, view string, affectedRows int64) *dbMock {
|
||||
query := db.mock.
|
||||
ExpectExec(`INSERT INTO notification\.locks \(object_type, locker_id, locked_until\) VALUES \(\$1, \$2, now\(\)\+\$3\) ON CONFLICT \(object_type\) DO UPDATE SET locked_until = now\(\)\+\$4, locker_id = \$5 WHERE \(locks\.locked_until < now\(\) OR locks\.locker_id = \$6\) AND locks\.object_type = \$7`).
|
||||
WithArgs(view, lockerID, sqlmock.AnyArg(), sqlmock.AnyArg(), lockerID, lockerID, view).
|
||||
WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
if affectedRows == 0 {
|
||||
query.WillReturnResult(sqlmock.NewResult(0, 0))
|
||||
} else {
|
||||
query.WillReturnResult(sqlmock.NewResult(1, affectedRows))
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
|
||||
func Test_locker_Renew(t *testing.T) {
|
||||
type fields struct {
|
||||
db *dbMock
|
||||
}
|
||||
type args struct {
|
||||
lockerID string
|
||||
viewModel string
|
||||
waitTime time.Duration
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
fields fields
|
||||
args args
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "renew succeeded",
|
||||
fields: fields{
|
||||
db: mockDB(t).
|
||||
expectBegin().
|
||||
expectSavepoint().
|
||||
expectRenew("locker", "view", 1).
|
||||
expectReleaseSavepoint().
|
||||
expectCommit(),
|
||||
},
|
||||
args: args{lockerID: "locker", viewModel: "view", waitTime: 1 * time.Second},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "renew now rows updated",
|
||||
fields: fields{
|
||||
db: mockDB(t).
|
||||
expectBegin().
|
||||
expectSavepoint().
|
||||
expectRenew("locker", "view", 0).
|
||||
expectRollback(),
|
||||
},
|
||||
args: args{lockerID: "locker", viewModel: "view", waitTime: 1 * time.Second},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
l := &locker{
|
||||
dbClient: tt.fields.db.db,
|
||||
}
|
||||
if err := l.Renew(tt.args.lockerID, tt.args.viewModel, tt.args.waitTime); (err != nil) != tt.wantErr {
|
||||
t.Errorf("locker.Renew() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
if err := tt.fields.db.mock.ExpectationsWereMet(); err != nil {
|
||||
t.Errorf("not all database expectations met: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -0,0 +1,34 @@
|
||||
package spooler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
sd "github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/eventstore"
|
||||
"github.com/caos/zitadel/internal/eventstore/spooler"
|
||||
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/handler"
|
||||
"github.com/caos/zitadel/internal/notification/repository/eventsourcing/view"
|
||||
usr_event "github.com/caos/zitadel/internal/user/repository/eventsourcing"
|
||||
)
|
||||
|
||||
type SpoolerConfig struct {
|
||||
BulkLimit uint64
|
||||
FailureCountUntilSkip uint64
|
||||
ConcurrentTasks int
|
||||
Handlers handler.Configs
|
||||
}
|
||||
|
||||
type EventstoreRepos struct {
|
||||
UserEvents *usr_event.UserEventstore
|
||||
}
|
||||
|
||||
func StartSpooler(c SpoolerConfig, es eventstore.Eventstore, view *view.View, sql *sql.DB, eventstoreRepos handler.EventstoreRepos, systemDefaults sd.SystemDefaults) *spooler.Spooler {
|
||||
spoolerConfig := spooler.Config{
|
||||
Eventstore: es,
|
||||
Locker: &locker{dbClient: sql},
|
||||
ConcurrentTasks: c.ConcurrentTasks,
|
||||
ViewHandlers: handler.Register(c.Handlers, c.BulkLimit, c.FailureCountUntilSkip, view, es, eventstoreRepos, systemDefaults),
|
||||
}
|
||||
spool := spoolerConfig.New()
|
||||
spool.Start()
|
||||
return spool
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
const (
|
||||
errTable = "notification.failed_event"
|
||||
)
|
||||
|
||||
func (v *View) saveFailedEvent(failedEvent *view.FailedEvent) error {
|
||||
return view.SaveFailedEvent(v.Db, errTable, failedEvent)
|
||||
}
|
||||
|
||||
func (v *View) latestFailedEvent(viewName string, sequence uint64) (*view.FailedEvent, error) {
|
||||
return view.LatestFailedEvent(v.Db, errTable, viewName, sequence)
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
global_view "github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
const (
|
||||
notificationTable = "notification.notifications"
|
||||
)
|
||||
|
||||
func (v *View) GetLatestNotificationSequence() (uint64, error) {
|
||||
return v.latestSequence(notificationTable)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedNotificationSequence(eventSequence uint64) error {
|
||||
return v.saveCurrentSequence(notificationTable, eventSequence)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestNotificationFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
|
||||
return v.latestFailedEvent(notificationTable, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedNotificationFailedEvent(failedEvent *global_view.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/user/repository/view"
|
||||
"github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
global_view "github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
const (
|
||||
notifyUserTable = "notification.notify_users"
|
||||
)
|
||||
|
||||
func (v *View) NotifyUserByID(userID string) (*model.NotifyUser, error) {
|
||||
return view.NotifyUserByID(v.Db, notifyUserTable, userID)
|
||||
}
|
||||
|
||||
func (v *View) PutNotifyUser(user *model.NotifyUser) error {
|
||||
err := view.PutNotifyUser(v.Db, notifyUserTable, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return v.ProcessedNotifyUserSequence(user.Sequence)
|
||||
}
|
||||
|
||||
func (v *View) DeleteNotifyUser(userID string, eventSequence uint64) error {
|
||||
err := view.DeleteNotifyUser(v.Db, notifyUserTable, userID)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return v.ProcessedNotifyUserSequence(eventSequence)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestNotifyUserSequence() (uint64, error) {
|
||||
return v.latestSequence(notifyUserTable)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedNotifyUserSequence(eventSequence uint64) error {
|
||||
return v.saveCurrentSequence(notifyUserTable, eventSequence)
|
||||
}
|
||||
|
||||
func (v *View) GetLatestNotifyUserFailedEvent(sequence uint64) (*global_view.FailedEvent, error) {
|
||||
return v.latestFailedEvent(notifyUserTable, sequence)
|
||||
}
|
||||
|
||||
func (v *View) ProcessedNotifyUserFailedEvent(failedEvent *global_view.FailedEvent) error {
|
||||
return v.saveFailedEvent(failedEvent)
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/view"
|
||||
)
|
||||
|
||||
const (
|
||||
sequencesTable = "notification.current_sequences"
|
||||
)
|
||||
|
||||
func (v *View) saveCurrentSequence(viewName string, sequence uint64) error {
|
||||
return view.SaveCurrentSequence(v.Db, sequencesTable, viewName, sequence)
|
||||
}
|
||||
|
||||
func (v *View) latestSequence(viewName string) (uint64, error) {
|
||||
return view.LatestSequence(v.Db, sequencesTable, viewName)
|
||||
}
|
25
internal/notification/repository/eventsourcing/view/view.go
Normal file
25
internal/notification/repository/eventsourcing/view/view.go
Normal file
@ -0,0 +1,25 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
type View struct {
|
||||
Db *gorm.DB
|
||||
}
|
||||
|
||||
func StartView(sqlClient *sql.DB) (*View, error) {
|
||||
gorm, err := gorm.Open("postgres", sqlClient)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &View{
|
||||
Db: gorm,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (v *View) Health() (err error) {
|
||||
return v.Db.DB().Ping()
|
||||
}
|
5
internal/notification/repository/repository.go
Normal file
5
internal/notification/repository/repository.go
Normal file
@ -0,0 +1,5 @@
|
||||
package repository
|
||||
|
||||
type Repository interface {
|
||||
Health() error
|
||||
}
|
45
internal/notification/templates/template.go
Normal file
45
internal/notification/templates/template.go
Normal file
@ -0,0 +1,45 @@
|
||||
package templates
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
const (
|
||||
templateFileName = "template.html"
|
||||
)
|
||||
|
||||
func GetParsedTemplate(contentData interface{}) (string, error) {
|
||||
template, err := ParseTemplateFile("", contentData)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return ParseTemplateText(template, contentData)
|
||||
}
|
||||
|
||||
func ParseTemplateFile(fileName string, data interface{}) (string, error) {
|
||||
if fileName == "" {
|
||||
fileName = templateFileName
|
||||
}
|
||||
template, err := template.ParseFiles(fileName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return parseTemplate(template, data)
|
||||
}
|
||||
|
||||
func ParseTemplateText(text string, data interface{}) (string, error) {
|
||||
template, err := template.New("template").Parse(text)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return parseTemplate(template, data)
|
||||
}
|
||||
|
||||
func parseTemplate(template *template.Template, data interface{}) (string, error) {
|
||||
buf := new(bytes.Buffer)
|
||||
if err := template.Execute(buf, data); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return buf.String(), nil
|
||||
}
|
11
internal/notification/templates/templateData.go
Normal file
11
internal/notification/templates/templateData.go
Normal file
@ -0,0 +1,11 @@
|
||||
package templates
|
||||
|
||||
type TemplateData struct {
|
||||
Title string
|
||||
PreHeader string
|
||||
Subject string
|
||||
Greeting string
|
||||
Text string
|
||||
Href string
|
||||
ButtonText string
|
||||
}
|
34
internal/notification/types/email_verification_code.go
Normal file
34
internal/notification/types/email_verification_code.go
Normal file
@ -0,0 +1,34 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/notification/templates"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type EmailVerificationCodeData struct {
|
||||
templates.TemplateData
|
||||
FirstName string
|
||||
LastName string
|
||||
URL string
|
||||
}
|
||||
|
||||
func SendEmailVerificationCode(user *view_model.NotifyUser, code *es_model.EmailCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
|
||||
codeString, err := crypto.DecryptString(code.Code, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url, err := templates.ParseTemplateText(systemDefaults.Notifications.Endpoints.VerifyEmail, &UrlData{UserID: user.ID, Code: codeString})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
emailCodeData := &EmailVerificationCodeData{TemplateData: systemDefaults.Notifications.TemplateData.VerifyEmail, FirstName: user.FirstName, LastName: user.LastName, URL: url}
|
||||
|
||||
template, err := templates.GetParsedTemplate(emailCodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateEmail(user, template, systemDefaults.Notifications, true)
|
||||
}
|
39
internal/notification/types/init_code.go
Normal file
39
internal/notification/types/init_code.go
Normal file
@ -0,0 +1,39 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/notification/templates"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type InitCodeEmailData struct {
|
||||
templates.TemplateData
|
||||
FirstName string
|
||||
LastName string
|
||||
URL string
|
||||
}
|
||||
|
||||
type UrlData struct {
|
||||
UserID string
|
||||
Code string
|
||||
}
|
||||
|
||||
func SendUserInitCode(user *view_model.NotifyUser, code *es_model.InitUserCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
|
||||
codeString, err := crypto.DecryptString(code.Code, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url, err := templates.ParseTemplateText(systemDefaults.Notifications.Endpoints.InitCode, &UrlData{UserID: user.ID, Code: codeString})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
initCodeData := &InitCodeEmailData{TemplateData: systemDefaults.Notifications.TemplateData.InitCode, FirstName: user.FirstName, LastName: user.LastName, URL: url}
|
||||
|
||||
template, err := templates.GetParsedTemplate(initCodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateEmail(user, template, systemDefaults.Notifications, true)
|
||||
}
|
34
internal/notification/types/password_code.go
Normal file
34
internal/notification/types/password_code.go
Normal file
@ -0,0 +1,34 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/notification/templates"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type PasswordCodeData struct {
|
||||
templates.TemplateData
|
||||
FirstName string
|
||||
LastName string
|
||||
URL string
|
||||
}
|
||||
|
||||
func SendPasswordCode(user *view_model.NotifyUser, code *es_model.PasswordCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
|
||||
codeString, err := crypto.DecryptString(code.Code, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
url, err := templates.ParseTemplateText(systemDefaults.Notifications.Endpoints.PasswordReset, &UrlData{UserID: user.ID, Code: codeString})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
passwordCodeData := &PasswordCodeData{TemplateData: systemDefaults.Notifications.TemplateData.PasswordReset, FirstName: user.FirstName, LastName: user.LastName, URL: url}
|
||||
|
||||
template, err := templates.GetParsedTemplate(passwordCodeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateEmail(user, template, systemDefaults.Notifications, false)
|
||||
}
|
29
internal/notification/types/phone_verification_code.go
Normal file
29
internal/notification/types/phone_verification_code.go
Normal file
@ -0,0 +1,29 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
"github.com/caos/zitadel/internal/notification/templates"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
type PhoneVerificationCodeData struct {
|
||||
FirstName string
|
||||
LastName string
|
||||
Code string
|
||||
UserID string
|
||||
}
|
||||
|
||||
func SendPhoneVerificationCode(user *view_model.NotifyUser, code *es_model.PhoneCode, systemDefaults systemdefaults.SystemDefaults, alg crypto.EncryptionAlgorithm) error {
|
||||
codeString, err := crypto.DecryptString(code.Code, alg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
codeData := &PhoneVerificationCodeData{FirstName: user.FirstName, LastName: user.LastName, UserID: user.ID, Code: codeString}
|
||||
template, err := templates.ParseTemplateText(systemDefaults.Notifications.TemplateData.VerifyPhone.Text, codeData)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return generateSms(user, template, systemDefaults.Notifications, true)
|
||||
}
|
41
internal/notification/types/user_email.go
Normal file
41
internal/notification/types/user_email.go
Normal file
@ -0,0 +1,41 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/notification/providers"
|
||||
"github.com/caos/zitadel/internal/notification/providers/chat"
|
||||
"github.com/caos/zitadel/internal/notification/providers/email"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
func generateEmail(user *view_model.NotifyUser, content string, config systemdefaults.Notifications, lastEmail bool) error {
|
||||
provider, err := email.InitEmailProvider(config.Providers.Email)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
message := &email.EmailMessage{
|
||||
SenderEmail: config.Providers.Email.From,
|
||||
Recipients: []string{user.VerifiedEmail},
|
||||
Subject: config.TemplateData.InitCode.Subject,
|
||||
Content: content,
|
||||
}
|
||||
if lastEmail {
|
||||
message.Recipients = []string{user.LastEmail}
|
||||
}
|
||||
if provider.CanHandleMessage(message) {
|
||||
if config.DebugMode {
|
||||
return sendDebugEmail(message, config)
|
||||
}
|
||||
return provider.HandleMessage(message)
|
||||
}
|
||||
return caos_errs.ThrowInternalf(nil, "NOTIF-s8ipw", "Could not send init message: userid: %v", user.ID)
|
||||
}
|
||||
|
||||
func sendDebugEmail(message providers.Message, config systemdefaults.Notifications) error {
|
||||
provider, err := chat.InitChatProvider(config.Providers.Chat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return provider.HandleMessage(message)
|
||||
}
|
37
internal/notification/types/user_phone.go
Normal file
37
internal/notification/types/user_phone.go
Normal file
@ -0,0 +1,37 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/config/systemdefaults"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/notification/providers"
|
||||
"github.com/caos/zitadel/internal/notification/providers/chat"
|
||||
"github.com/caos/zitadel/internal/notification/providers/twilio"
|
||||
view_model "github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
)
|
||||
|
||||
func generateSms(user *view_model.NotifyUser, content string, config systemdefaults.Notifications, lastPhone bool) error {
|
||||
provider := twilio.InitTwilioProvider(config.Providers.Twilio)
|
||||
message := &twilio.TwilioMessage{
|
||||
SenderPhoneNumber: config.Providers.Twilio.From,
|
||||
RecipientPhoneNumber: user.VerifiedPhone,
|
||||
Content: content,
|
||||
}
|
||||
if lastPhone {
|
||||
message.RecipientPhoneNumber = user.LastPhone
|
||||
}
|
||||
if provider.CanHandleMessage(message) {
|
||||
if config.DebugMode {
|
||||
return sendDebugPhone(message, config)
|
||||
}
|
||||
return provider.HandleMessage(message)
|
||||
}
|
||||
return caos_errs.ThrowInternalf(nil, "NOTIF-s8ipw", "Could not send init message: userid: %v", user.ID)
|
||||
}
|
||||
|
||||
func sendDebugPhone(message providers.Message, config systemdefaults.Notifications) error {
|
||||
provider, err := chat.InitChatProvider(config.Providers.Chat)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return provider.HandleMessage(message)
|
||||
}
|
53
internal/user/model/notify_user.go
Normal file
53
internal/user/model/notify_user.go
Normal file
@ -0,0 +1,53 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/caos/zitadel/internal/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
type NotifyUser struct {
|
||||
ID string
|
||||
CreationDate time.Time
|
||||
ChangeDate time.Time
|
||||
ResourceOwner string
|
||||
UserName string
|
||||
FirstName string
|
||||
LastName string
|
||||
NickName string
|
||||
DisplayName string
|
||||
PreferredLanguage string
|
||||
Gender Gender
|
||||
LastEmail string
|
||||
VerifiedEmail string
|
||||
LastPhone string
|
||||
VerifiedPhone string
|
||||
Sequence uint64
|
||||
}
|
||||
|
||||
type NotifyUserSearchRequest struct {
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
SortingColumn NotifyUserSearchKey
|
||||
Asc bool
|
||||
Queries []*NotifyUserSearchQuery
|
||||
}
|
||||
|
||||
type NotifyUserSearchKey int32
|
||||
|
||||
const (
|
||||
NOTIFYUSERSEARCHKEY_UNSPECIFIED UserSearchKey = iota
|
||||
NOTIFYUSERSEARCHKEY_USER_ID
|
||||
)
|
||||
|
||||
type NotifyUserSearchQuery struct {
|
||||
Key NotifyUserSearchKey
|
||||
Method model.SearchMethod
|
||||
Value string
|
||||
}
|
||||
|
||||
type NotifyUserSearchResponse struct {
|
||||
Offset uint64
|
||||
Limit uint64
|
||||
TotalResult uint64
|
||||
Result []*UserView
|
||||
}
|
@ -1,13 +1,18 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
"github.com/caos/zitadel/internal/crypto"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/ttacon/libphonenumber"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
//TODO: How do we get region?
|
||||
defaultRegion = "CH"
|
||||
)
|
||||
|
||||
type Phone struct {
|
||||
es_models.ObjectRoot
|
||||
|
||||
@ -23,27 +28,16 @@ type PhoneCode struct {
|
||||
}
|
||||
|
||||
func (p *Phone) IsValid() bool {
|
||||
return p.PhoneNumber != ""
|
||||
err := p.formatPhone()
|
||||
return p.PhoneNumber != "" && err == nil
|
||||
}
|
||||
|
||||
func (u *User) appendUserPhoneChangedEvent(event *es_models.Event) error {
|
||||
u.Phone = new(Phone)
|
||||
u.Phone.setData(event)
|
||||
u.IsPhoneVerified = false
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) appendUserPhoneVerifiedEvent() error {
|
||||
u.IsPhoneVerified = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *Phone) setData(event *es_models.Event) error {
|
||||
p.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, p); err != nil {
|
||||
logging.Log("EVEN-dlo9s").WithError(err).Error("could not unmarshal event data")
|
||||
return err
|
||||
func (p *Phone) formatPhone() error {
|
||||
phoneNr, err := libphonenumber.Parse(p.PhoneNumber, defaultRegion)
|
||||
if err != nil {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-so0wa", "Phonenumber is invalid")
|
||||
}
|
||||
p.PhoneNumber = libphonenumber.Format(phoneNr, libphonenumber.E164)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
106
internal/user/model/phone_test.go
Normal file
106
internal/user/model/phone_test.go
Normal file
@ -0,0 +1,106 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFormatPhoneNumber(t *testing.T) {
|
||||
type args struct {
|
||||
phone *Phone
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *Phone
|
||||
errFunc func(err error) bool
|
||||
}{
|
||||
{
|
||||
name: "invalid phone number",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "PhoneNumber",
|
||||
},
|
||||
},
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
{
|
||||
name: "format phone 071...",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "0711234567",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format phone 0041...",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "0041711234567",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format phone 071 xxx xx xx",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "071 123 45 67",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format phone +4171 xxx xx xx",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "+4171 123 45 67",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format phone 004171 xxx xx xx",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "004171 123 45 67",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+41711234567",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "format non swiss phone 004371 xxx xx xx",
|
||||
args: args{
|
||||
phone: &Phone{
|
||||
PhoneNumber: "004371 123 45 67",
|
||||
},
|
||||
},
|
||||
result: &Phone{
|
||||
PhoneNumber: "+43711234567",
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.phone.formatPhone()
|
||||
|
||||
if tt.errFunc == nil && tt.result.PhoneNumber != tt.args.phone.PhoneNumber {
|
||||
t.Errorf("got wrong result: expected: %v, actual: %v ", tt.args.phone.PhoneNumber, tt.result.PhoneNumber)
|
||||
}
|
||||
if tt.errFunc != nil && !tt.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
@ -58,7 +58,7 @@ func (u *User) SetEmailAsUsername() {
|
||||
}
|
||||
|
||||
func (u *User) IsValid() bool {
|
||||
return u.Profile != nil && u.FirstName != "" && u.LastName != "" && u.UserName != "" && u.Email != nil && u.EmailAddress != ""
|
||||
return u.Profile != nil && u.FirstName != "" && u.LastName != "" && u.UserName != "" && u.Email != nil && u.Email.IsValid() && u.Phone == nil || (u.Phone != nil && u.Phone.IsValid())
|
||||
}
|
||||
|
||||
func (u *User) IsInitialState() bool {
|
||||
|
@ -96,7 +96,7 @@ func (es *UserEventstore) UserByID(ctx context.Context, id string) (*usr_model.U
|
||||
func (es *UserEventstore) PrepareCreateUser(ctx context.Context, user *usr_model.User, resourceOwner string) (*model.User, *es_models.Aggregate, error) {
|
||||
user.SetEmailAsUsername()
|
||||
if !user.IsValid() {
|
||||
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "Name is required")
|
||||
return nil, nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-9dk45", "User is invalid")
|
||||
}
|
||||
//TODO: Check Uniqueness
|
||||
id, err := es.idGenerator.NextID()
|
||||
@ -303,6 +303,25 @@ func (es *UserEventstore) CreateInitializeUserCodeByID(ctx context.Context, user
|
||||
return model.InitCodeToModel(repoUser.InitCode), nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) InitCodeSent(ctx context.Context, userID string) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-0posw", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
agg := UserInitCodeSentAggregate(es.AggregateCreator(), repoUser)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) SkipMfaInit(ctx context.Context, userID string) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-dic8s", "userID missing")
|
||||
@ -446,6 +465,25 @@ func (es *UserEventstore) RequestSetPassword(ctx context.Context, userID string,
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) PasswordCodeSent(ctx context.Context, userID string) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-s09ow", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
agg := PasswordCodeSentAggregate(es.AggregateCreator(), repoUser)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) ProfileByID(ctx context.Context, userID string) (*usr_model.Profile, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di834", "userID missing")
|
||||
@ -584,6 +622,25 @@ func (es *UserEventstore) CreateEmailVerificationCode(ctx context.Context, userI
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) EmailVerificationCodeSent(ctx context.Context, userID string) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-spo0w", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
agg := EmailCodeSentAggregate(es.AggregateCreator(), repoUser)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) PhoneByID(ctx context.Context, userID string) (*usr_model.Phone, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-do9se", "userID missing")
|
||||
@ -686,6 +743,25 @@ func (es *UserEventstore) CreatePhoneVerificationCode(ctx context.Context, userI
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) PhoneVerificationCodeSent(ctx context.Context, userID string) error {
|
||||
if userID == "" {
|
||||
return caos_errs.ThrowPreconditionFailed(nil, "EVENT-sp0wa", "userID missing")
|
||||
}
|
||||
user, err := es.UserByID(ctx, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
repoUser := model.UserFromModel(user)
|
||||
agg := PhoneCodeSentAggregate(es.AggregateCreator(), repoUser)
|
||||
err = es_sdk.Push(ctx, es.PushAggregates, repoUser.AppendEvents, agg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
es.userCache.cacheUser(repoUser)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (es *UserEventstore) AddressByID(ctx context.Context, userID string) (*usr_model.Address, error) {
|
||||
if userID == "" {
|
||||
return nil, caos_errs.ThrowPreconditionFailed(nil, "EVENT-di8ws", "userID missing")
|
||||
|
@ -161,7 +161,7 @@ func TestCreateUser(t *testing.T) {
|
||||
{
|
||||
name: "with verified phone number",
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithInitCodeGen(ctrl, repo_model.User{Profile: &repo_model.Profile{UserName: "EmailAddress", FirstName: "FirstName", LastName: "LastName"}, Email: &repo_model.Email{EmailAddress: "EmailAddress", IsEmailVerified: true}, Phone: &repo_model.Phone{PhoneNumber: "PhoneNumber", IsPhoneVerified: true}}),
|
||||
es: GetMockManipulateUserWithInitCodeGen(ctrl, repo_model.User{Profile: &repo_model.Profile{UserName: "EmailAddress", FirstName: "FirstName", LastName: "LastName"}, Email: &repo_model.Email{EmailAddress: "EmailAddress", IsEmailVerified: true}, Phone: &repo_model.Phone{PhoneNumber: "+41711234567", IsPhoneVerified: true}}),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
user: &model.User{ObjectRoot: es_models.ObjectRoot{Sequence: 1},
|
||||
Profile: &model.Profile{
|
||||
@ -174,7 +174,7 @@ func TestCreateUser(t *testing.T) {
|
||||
IsEmailVerified: true,
|
||||
},
|
||||
Phone: &model.Phone{
|
||||
PhoneNumber: "UserName",
|
||||
PhoneNumber: "+41711234567",
|
||||
IsPhoneVerified: true,
|
||||
},
|
||||
},
|
||||
@ -191,7 +191,7 @@ func TestCreateUser(t *testing.T) {
|
||||
IsEmailVerified: true,
|
||||
},
|
||||
Phone: &model.Phone{
|
||||
PhoneNumber: "UserName",
|
||||
PhoneNumber: "+41711234567",
|
||||
IsPhoneVerified: true,
|
||||
},
|
||||
},
|
||||
@ -824,6 +824,67 @@ func TestCreateInitCode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitCodeSent(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "sent init",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.es.InitCodeSent(tt.args.ctx, tt.args.existing.AggregateID)
|
||||
|
||||
if tt.res.errFunc == nil && err != nil {
|
||||
t.Errorf("rshould not get err")
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkipMfaInit(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
@ -1468,6 +1529,67 @@ func TestRequestSetPassword(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordCodeSent(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "sent password code",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.es.PasswordCodeSent(tt.args.ctx, tt.args.existing.AggregateID)
|
||||
|
||||
if tt.res.errFunc == nil && err != nil {
|
||||
t.Errorf("rshould not get err")
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileByID(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
@ -1907,6 +2029,67 @@ func TestCreateEmailVerificationCode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailVerificationCodeSent(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "sent email verify code",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.es.EmailVerificationCodeSent(tt.args.ctx, tt.args.existing.AggregateID)
|
||||
|
||||
if tt.res.errFunc == nil && err != nil {
|
||||
t.Errorf("rshould not get err")
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhoneByID(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
@ -1995,10 +2178,10 @@ func TestChangePhone(t *testing.T) {
|
||||
args: args{
|
||||
es: GetMockManipulateUserFull(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "PhoneNumberChanged", IsPhoneVerified: true},
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "0711234567", IsPhoneVerified: true},
|
||||
},
|
||||
res: res{
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "PhoneNumberChanged", IsPhoneVerified: true},
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "+41711234567", IsPhoneVerified: true},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -2006,10 +2189,10 @@ func TestChangePhone(t *testing.T) {
|
||||
args: args{
|
||||
es: GetMockManipulateUserWithPhoneCodeGen(ctrl, repo_model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, Profile: &repo_model.Profile{UserName: "UserName"}, Phone: &repo_model.Phone{PhoneNumber: "PhoneNumber"}}),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "PhoneNumberChanged", IsPhoneVerified: false},
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "+41711234567", IsPhoneVerified: false},
|
||||
},
|
||||
res: res{
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "PhoneNumberChanged", IsPhoneVerified: false},
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "+41711234567", IsPhoneVerified: false},
|
||||
},
|
||||
},
|
||||
{
|
||||
@ -2028,7 +2211,7 @@ func TestChangePhone(t *testing.T) {
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "PhoneNumberChanged", IsPhoneVerified: true},
|
||||
phone: &model.Phone{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}, PhoneNumber: "+41711234567", IsPhoneVerified: true},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
@ -2212,6 +2395,67 @@ func TestCreatePhoneVerificationCode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhoneVerificationCodeSent(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
es *UserEventstore
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
}
|
||||
type res struct {
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "sent phone verification code",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{},
|
||||
},
|
||||
{
|
||||
name: "empty userid",
|
||||
args: args{
|
||||
es: GetMockManipulateUser(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsPreconditionFailed,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "existing user not found",
|
||||
args: args{
|
||||
es: GetMockManipulateUserNoEvents(ctrl),
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: es_models.ObjectRoot{AggregateID: "AggregateID", Sequence: 1}},
|
||||
},
|
||||
res: res{
|
||||
errFunc: caos_errs.IsNotFound,
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.args.es.PhoneVerificationCodeSent(tt.args.ctx, tt.args.existing.AggregateID)
|
||||
|
||||
if tt.res.errFunc == nil && err != nil {
|
||||
t.Errorf("rshould not get err")
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressByID(t *testing.T) {
|
||||
ctrl := gomock.NewController(t)
|
||||
type args struct {
|
||||
|
@ -76,7 +76,7 @@ func (u *User) appendUserEmailChangedEvent(event *es_models.Event) error {
|
||||
|
||||
func (u *User) appendUserEmailCodeAddedEvent(event *es_models.Event) error {
|
||||
u.EmailCode = new(EmailCode)
|
||||
return u.EmailCode.setData(event)
|
||||
return u.EmailCode.SetData(event)
|
||||
}
|
||||
|
||||
func (u *User) appendUserEmailVerifiedEvent() {
|
||||
@ -92,7 +92,7 @@ func (a *Email) setData(event *es_models.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *EmailCode) setData(event *es_models.Event) error {
|
||||
func (a *EmailCode) SetData(event *es_models.Event) error {
|
||||
a.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, a); err != nil {
|
||||
logging.Log("EVEN-lo9s").WithError(err).Error("could not unmarshal event data")
|
||||
|
@ -62,7 +62,7 @@ func (u *User) appendUserPasswordChangedEvent(event *es_models.Event) error {
|
||||
|
||||
func (u *User) appendPasswordSetRequestedEvent(event *es_models.Event) error {
|
||||
u.PasswordCode = new(PasswordCode)
|
||||
return u.PasswordCode.setData(event)
|
||||
return u.PasswordCode.SetData(event)
|
||||
}
|
||||
|
||||
func (pw *Password) setData(event *es_models.Event) error {
|
||||
@ -74,7 +74,7 @@ func (pw *Password) setData(event *es_models.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *PasswordCode) setData(event *es_models.Event) error {
|
||||
func (a *PasswordCode) SetData(event *es_models.Event) error {
|
||||
a.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, a); err != nil {
|
||||
logging.Log("EVEN-lo0y2").WithError(err).Error("could not unmarshal event data")
|
||||
|
@ -74,7 +74,7 @@ func (u *User) appendUserPhoneChangedEvent(event *es_models.Event) error {
|
||||
|
||||
func (u *User) appendUserPhoneCodeAddedEvent(event *es_models.Event) error {
|
||||
u.PhoneCode = new(PhoneCode)
|
||||
return u.PhoneCode.setData(event)
|
||||
return u.PhoneCode.SetData(event)
|
||||
}
|
||||
|
||||
func (u *User) appendUserPhoneVerifiedEvent() {
|
||||
@ -90,7 +90,7 @@ func (p *Phone) setData(event *es_models.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *PhoneCode) setData(event *es_models.Event) error {
|
||||
func (a *PhoneCode) SetData(event *es_models.Event) error {
|
||||
a.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, a); err != nil {
|
||||
logging.Log("EVEN-sk8ws").WithError(err).Error("could not unmarshal event data")
|
||||
|
@ -228,7 +228,7 @@ func (u *User) appendUnlockedEvent() {
|
||||
|
||||
func (u *User) appendInitUsercodeCreatedEvent(event *es_models.Event) error {
|
||||
initCode := new(InitUserCode)
|
||||
err := initCode.setData(event)
|
||||
err := initCode.SetData(event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -237,7 +237,7 @@ func (u *User) appendInitUsercodeCreatedEvent(event *es_models.Event) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *InitUserCode) setData(event *es_models.Event) error {
|
||||
func (c *InitUserCode) SetData(event *es_models.Event) error {
|
||||
c.ObjectRoot.AppendEvent(event)
|
||||
if err := json.Unmarshal(event.Data, c); err != nil {
|
||||
logging.Log("EVEN-7duwe").WithError(err).Error("could not unmarshal event data")
|
||||
|
@ -68,12 +68,6 @@ func UserCreateAggregate(ctx context.Context, aggCreator *es_models.AggregateCre
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if user.Password != nil {
|
||||
agg, err = agg.AppendEvent(model.UserPasswordCodeAdded, user.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if initCode != nil {
|
||||
agg, err = agg.AppendEvent(model.InitializedUserCodeAdded, initCode)
|
||||
if err != nil {
|
||||
@ -145,6 +139,16 @@ func UserInitCodeAggregate(aggCreator *es_models.AggregateCreator, existing *mod
|
||||
}
|
||||
}
|
||||
|
||||
func UserInitCodeSentAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.InitializedUserCodeSent, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func SkipMfaAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
@ -200,6 +204,16 @@ func RequestSetPassword(aggCreator *es_models.AggregateCreator, existing *model.
|
||||
}
|
||||
}
|
||||
|
||||
func PasswordCodeSentAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserPasswordCodeSent, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func ProfileChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, profile *model.Profile) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if profile == nil {
|
||||
@ -210,6 +224,9 @@ func ProfileChangeAggregate(aggCreator *es_models.AggregateCreator, existing *mo
|
||||
return nil, err
|
||||
}
|
||||
changes := existing.Profile.Changes(profile)
|
||||
if len(changes) == 0 {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-0spow", "no changes found")
|
||||
}
|
||||
return agg.AppendEvent(model.UserProfileChanged, changes)
|
||||
}
|
||||
}
|
||||
@ -227,6 +244,9 @@ func EmailChangeAggregate(aggCreator *es_models.AggregateCreator, existing *mode
|
||||
return nil, err
|
||||
}
|
||||
changes := existing.Email.Changes(email)
|
||||
if len(changes) == 0 {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-s90pw", "no changes found")
|
||||
}
|
||||
agg, err = agg.AppendEvent(model.UserEmailChanged, changes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -266,6 +286,16 @@ func EmailVerificationCodeAggregate(aggCreator *es_models.AggregateCreator, exis
|
||||
}
|
||||
}
|
||||
|
||||
func EmailCodeSentAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserEmailCodeSent, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func PhoneChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, phone *model.Phone, code *model.PhoneCode) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if phone == nil {
|
||||
@ -282,6 +312,9 @@ func PhoneChangeAggregate(aggCreator *es_models.AggregateCreator, existing *mode
|
||||
existing.Phone = new(model.Phone)
|
||||
}
|
||||
changes := existing.Phone.Changes(phone)
|
||||
if len(changes) == 0 {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-sp0oc", "no changes found")
|
||||
}
|
||||
agg, err = agg.AppendEvent(model.UserPhoneChanged, changes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -318,6 +351,16 @@ func PhoneVerificationCodeAggregate(aggCreator *es_models.AggregateCreator, exis
|
||||
}
|
||||
}
|
||||
|
||||
func PhoneCodeSentAggregate(aggCreator *es_models.AggregateCreator, existing *model.User) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
agg, err := UserAggregate(ctx, aggCreator, existing)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return agg.AppendEvent(model.UserPhoneCodeSent, nil)
|
||||
}
|
||||
}
|
||||
|
||||
func AddressChangeAggregate(aggCreator *es_models.AggregateCreator, existing *model.User, address *model.Address) func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
return func(ctx context.Context) (*es_models.Aggregate, error) {
|
||||
if address == nil {
|
||||
@ -331,6 +374,9 @@ func AddressChangeAggregate(aggCreator *es_models.AggregateCreator, existing *mo
|
||||
existing.Address = new(model.Address)
|
||||
}
|
||||
changes := existing.Address.Changes(address)
|
||||
if len(changes) == 0 {
|
||||
return nil, errors.ThrowPreconditionFailed(nil, "EVENT-2tszw", "no changes found")
|
||||
}
|
||||
return agg.AppendEvent(model.UserAddressChanged, changes)
|
||||
}
|
||||
}
|
||||
|
@ -650,6 +650,54 @@ func TestUserInitCodeAggregate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitCodeSentAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
aggCreator *models.AggregateCreator
|
||||
}
|
||||
type res struct {
|
||||
eventLen int
|
||||
eventTypes []models.EventType
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "user init code sent aggregate ok",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: models.ObjectRoot{AggregateID: "ID"}},
|
||||
aggCreator: models.NewAggregateCreator("Test"),
|
||||
},
|
||||
res: res{
|
||||
eventLen: 1,
|
||||
eventTypes: []models.EventType{model.InitializedUserCodeSent},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
agg, err := UserInitCodeSentAggregate(tt.args.aggCreator, tt.args.existing)(tt.args.ctx)
|
||||
|
||||
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
|
||||
}
|
||||
for i := 0; i < tt.res.eventLen; i++ {
|
||||
if tt.res.errFunc == nil && agg.Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], agg.Events[i].Type.String())
|
||||
}
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkipMfaAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -824,6 +872,54 @@ func TestRequestSetPasswordAggregate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPasswordCodeSentAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
aggCreator *models.AggregateCreator
|
||||
}
|
||||
type res struct {
|
||||
eventLen int
|
||||
eventTypes []models.EventType
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "user password code sent aggregate ok",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: models.ObjectRoot{AggregateID: "ID"}},
|
||||
aggCreator: models.NewAggregateCreator("Test"),
|
||||
},
|
||||
res: res{
|
||||
eventLen: 1,
|
||||
eventTypes: []models.EventType{model.UserPasswordCodeSent},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
agg, err := PasswordCodeSentAggregate(tt.args.aggCreator, tt.args.existing)(tt.args.ctx)
|
||||
|
||||
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
|
||||
}
|
||||
for i := 0; i < tt.res.eventLen; i++ {
|
||||
if tt.res.errFunc == nil && agg.Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], agg.Events[i].Type.String())
|
||||
}
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeProfileAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -846,9 +942,9 @@ func TestChangeProfileAggregate(t *testing.T) {
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
|
||||
Profile: &model.Profile{UserName: "UserName"},
|
||||
Profile: &model.Profile{FirstName: "FirstName"},
|
||||
},
|
||||
profile: &model.Profile{FirstName: ""},
|
||||
profile: &model.Profile{FirstName: "FirstNameChanged"},
|
||||
aggCreator: models.NewAggregateCreator("Test"),
|
||||
},
|
||||
res: res{
|
||||
@ -1114,6 +1210,55 @@ func TestCreateEmailCodeAggregate(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEmailCodeSentAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
aggCreator *models.AggregateCreator
|
||||
}
|
||||
type res struct {
|
||||
eventLen int
|
||||
eventTypes []models.EventType
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "user email code sent aggregate ok",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: models.ObjectRoot{AggregateID: "ID"}},
|
||||
aggCreator: models.NewAggregateCreator("Test"),
|
||||
},
|
||||
res: res{
|
||||
eventLen: 1,
|
||||
eventTypes: []models.EventType{model.UserEmailCodeSent},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
agg, err := EmailCodeSentAggregate(tt.args.aggCreator, tt.args.existing)(tt.args.ctx)
|
||||
|
||||
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
|
||||
}
|
||||
for i := 0; i < tt.res.eventLen; i++ {
|
||||
if tt.res.errFunc == nil && agg.Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], agg.Events[i].Type.String())
|
||||
}
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangePhoneAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
@ -1137,9 +1282,9 @@ func TestChangePhoneAggregate(t *testing.T) {
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: models.ObjectRoot{AggregateID: "ID"},
|
||||
Phone: &model.Phone{PhoneNumber: "PhoneNumber"},
|
||||
Phone: &model.Phone{PhoneNumber: "+41791234567"},
|
||||
},
|
||||
phone: &model.Phone{PhoneNumber: "Changed", IsPhoneVerified: true},
|
||||
phone: &model.Phone{PhoneNumber: "+41799876543", IsPhoneVerified: true},
|
||||
aggCreator: models.NewAggregateCreator("Test"),
|
||||
},
|
||||
res: res{
|
||||
@ -1346,6 +1491,54 @@ func TestCreatePhoneCodeAggregate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPhoneCodeSentAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
existing *model.User
|
||||
aggCreator *models.AggregateCreator
|
||||
}
|
||||
type res struct {
|
||||
eventLen int
|
||||
eventTypes []models.EventType
|
||||
errFunc func(err error) bool
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
res res
|
||||
}{
|
||||
{
|
||||
name: "user phone code sent aggregate ok",
|
||||
args: args{
|
||||
ctx: auth.NewMockContext("orgID", "userID"),
|
||||
existing: &model.User{ObjectRoot: models.ObjectRoot{AggregateID: "ID"}},
|
||||
aggCreator: models.NewAggregateCreator("Test"),
|
||||
},
|
||||
res: res{
|
||||
eventLen: 1,
|
||||
eventTypes: []models.EventType{model.UserPhoneCodeSent},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
agg, err := PhoneCodeSentAggregate(tt.args.aggCreator, tt.args.existing)(tt.args.ctx)
|
||||
|
||||
if tt.res.errFunc == nil && len(agg.Events) != tt.res.eventLen {
|
||||
t.Errorf("got wrong event len: expected: %v, actual: %v ", tt.res.eventLen, len(agg.Events))
|
||||
}
|
||||
for i := 0; i < tt.res.eventLen; i++ {
|
||||
if tt.res.errFunc == nil && agg.Events[i].Type != tt.res.eventTypes[i] {
|
||||
t.Errorf("got wrong event type: expected: %v, actual: %v ", tt.res.eventTypes[i], agg.Events[i].Type.String())
|
||||
}
|
||||
}
|
||||
if tt.res.errFunc != nil && !tt.res.errFunc(err) {
|
||||
t.Errorf("got wrong err: %v ", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestChangeAddressAggregate(t *testing.T) {
|
||||
type args struct {
|
||||
ctx context.Context
|
||||
|
112
internal/user/repository/view/model/notify_user.go
Normal file
112
internal/user/repository/view/model/notify_user.go
Normal file
@ -0,0 +1,112 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/caos/logging"
|
||||
caos_errs "github.com/caos/zitadel/internal/errors"
|
||||
"github.com/caos/zitadel/internal/eventstore/models"
|
||||
"github.com/caos/zitadel/internal/user/model"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
NotifyUserKeyUserID = "id"
|
||||
)
|
||||
|
||||
type NotifyUser struct {
|
||||
ID string `json:"-" gorm:"column:id;primary_key"`
|
||||
CreationDate time.Time `json:"-" gorm:"column:creation_date"`
|
||||
ChangeDate time.Time `json:"-" gorm:"column:change_date"`
|
||||
ResourceOwner string `json:"-" gorm:"column:resource_owner"`
|
||||
UserName string `json:"userName" gorm:"column:user_name"`
|
||||
FirstName string `json:"firstName" gorm:"column:first_name"`
|
||||
LastName string `json:"lastName" gorm:"column:last_name"`
|
||||
NickName string `json:"nickName" gorm:"column:nick_name"`
|
||||
DisplayName string `json:"displayName" gorm:"column:display_name"`
|
||||
PreferredLanguage string `json:"preferredLanguage" gorm:"column:preferred_language"`
|
||||
Gender int32 `json:"gender" gorm:"column:gender"`
|
||||
LastEmail string `json:"email" gorm:"column:last_email"`
|
||||
VerifiedEmail string `json:"-" gorm:"column:verified_email"`
|
||||
LastPhone string `json:"phone" gorm:"column:last_phone"`
|
||||
VerifiedPhone string `json:"-" gorm:"column:verified_phone"`
|
||||
Sequence uint64 `json:"-" gorm:"column:sequence"`
|
||||
}
|
||||
|
||||
func NotifyUserFromModel(user *model.NotifyUser) *NotifyUser {
|
||||
return &NotifyUser{
|
||||
ID: user.ID,
|
||||
ChangeDate: user.ChangeDate,
|
||||
CreationDate: user.CreationDate,
|
||||
ResourceOwner: user.ResourceOwner,
|
||||
UserName: user.UserName,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
NickName: user.NickName,
|
||||
DisplayName: user.DisplayName,
|
||||
PreferredLanguage: user.PreferredLanguage,
|
||||
Gender: int32(user.Gender),
|
||||
LastEmail: user.LastEmail,
|
||||
VerifiedEmail: user.VerifiedEmail,
|
||||
LastPhone: user.LastPhone,
|
||||
VerifiedPhone: user.VerifiedPhone,
|
||||
Sequence: user.Sequence,
|
||||
}
|
||||
}
|
||||
|
||||
func NotifyUserToModel(user *NotifyUser) *model.NotifyUser {
|
||||
return &model.NotifyUser{
|
||||
ID: user.ID,
|
||||
ChangeDate: user.ChangeDate,
|
||||
CreationDate: user.CreationDate,
|
||||
ResourceOwner: user.ResourceOwner,
|
||||
UserName: user.UserName,
|
||||
FirstName: user.FirstName,
|
||||
LastName: user.LastName,
|
||||
NickName: user.NickName,
|
||||
DisplayName: user.DisplayName,
|
||||
PreferredLanguage: user.PreferredLanguage,
|
||||
Gender: model.Gender(user.Gender),
|
||||
LastEmail: user.LastEmail,
|
||||
VerifiedEmail: user.VerifiedEmail,
|
||||
LastPhone: user.LastPhone,
|
||||
VerifiedPhone: user.VerifiedPhone,
|
||||
Sequence: user.Sequence,
|
||||
}
|
||||
}
|
||||
|
||||
func (u *NotifyUser) AppendEvent(event *models.Event) (err error) {
|
||||
u.ChangeDate = event.CreationDate
|
||||
u.Sequence = event.Sequence
|
||||
switch event.Type {
|
||||
case es_model.UserAdded,
|
||||
es_model.UserRegistered:
|
||||
u.CreationDate = event.CreationDate
|
||||
u.setRootData(event)
|
||||
err = u.setData(event)
|
||||
case es_model.UserProfileChanged:
|
||||
err = u.setData(event)
|
||||
case es_model.UserEmailChanged:
|
||||
err = u.setData(event)
|
||||
case es_model.UserEmailVerified:
|
||||
u.VerifiedEmail = u.LastEmail
|
||||
case es_model.UserPhoneChanged:
|
||||
err = u.setData(event)
|
||||
case es_model.UserPhoneVerified:
|
||||
u.VerifiedPhone = u.LastPhone
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (u *NotifyUser) setRootData(event *models.Event) {
|
||||
u.ID = event.AggregateID
|
||||
u.ResourceOwner = event.ResourceOwner
|
||||
}
|
||||
|
||||
func (u *NotifyUser) setData(event *models.Event) error {
|
||||
if err := json.Unmarshal(event.Data, u); err != nil {
|
||||
logging.Log("EVEN-lso9e").WithError(err).Error("could not unmarshal event data")
|
||||
return caos_errs.ThrowInternal(nil, "MODEL-8iows", "could not unmarshal data")
|
||||
}
|
||||
return nil
|
||||
}
|
113
internal/user/repository/view/model/notify_user_test.go
Normal file
113
internal/user/repository/view/model/notify_user_test.go
Normal file
@ -0,0 +1,113 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
es_models "github.com/caos/zitadel/internal/eventstore/models"
|
||||
es_model "github.com/caos/zitadel/internal/user/repository/eventsourcing/model"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNotifyUserAppendEvent(t *testing.T) {
|
||||
type args struct {
|
||||
event *es_models.Event
|
||||
user *NotifyUser
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
result *NotifyUser
|
||||
}{
|
||||
{
|
||||
name: "append added user event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserAdded, ResourceOwner: "OrgID", Data: mockUserData(getFullUser(nil))},
|
||||
user: &NotifyUser{},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
{
|
||||
name: "append change user profile event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserProfileChanged, ResourceOwner: "OrgID", Data: mockProfileData(&es_model.Profile{FirstName: "FirstNameChanged"})},
|
||||
user: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstNameChanged", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
{
|
||||
name: "append change user email event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserEmailChanged, ResourceOwner: "OrgID", Data: mockEmailData(&es_model.Email{EmailAddress: "EmailChanged"})},
|
||||
user: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "EmailChanged", LastPhone: "Phone"},
|
||||
},
|
||||
{
|
||||
name: "append change user email event, existing email",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserEmailChanged, ResourceOwner: "OrgID", Data: mockEmailData(&es_model.Email{EmailAddress: "EmailChanged"})},
|
||||
user: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", VerifiedEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "EmailChanged", VerifiedEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
{
|
||||
name: "append verify user email event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserEmailVerified, ResourceOwner: "OrgID"},
|
||||
user: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", VerifiedEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
{
|
||||
name: "append change user phone event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserPhoneChanged, ResourceOwner: "OrgID", Data: mockPhoneData(&es_model.Phone{PhoneNumber: "PhoneChanged"})},
|
||||
user: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "PhoneChanged"},
|
||||
},
|
||||
{
|
||||
name: "append change user phone event, existing phone",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserPhoneChanged, ResourceOwner: "OrgID", Data: mockPhoneData(&es_model.Phone{PhoneNumber: "PhoneChanged"})},
|
||||
user: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone", VerifiedPhone: "Phone"},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "PhoneChanged", VerifiedPhone: "Phone"},
|
||||
},
|
||||
{
|
||||
name: "append verify user phone event",
|
||||
args: args{
|
||||
event: &es_models.Event{AggregateID: "AggregateID", Sequence: 1, Type: es_model.UserPhoneVerified, ResourceOwner: "OrgID"},
|
||||
user: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone"},
|
||||
},
|
||||
result: &NotifyUser{ID: "AggregateID", ResourceOwner: "OrgID", UserName: "UserName", FirstName: "FirstName", LastName: "LastName", LastEmail: "Email", LastPhone: "Phone", VerifiedPhone: "Phone"},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tt.args.user.AppendEvent(tt.args.event)
|
||||
if tt.args.user.ID != tt.result.ID {
|
||||
t.Errorf("got wrong result ID: expected: %v, actual: %v ", tt.result.ID, tt.args.user.ID)
|
||||
}
|
||||
if tt.args.user.FirstName != tt.result.FirstName {
|
||||
t.Errorf("got wrong result FirstName: expected: %v, actual: %v ", tt.result.FirstName, tt.args.user.FirstName)
|
||||
}
|
||||
if tt.args.user.LastName != tt.result.LastName {
|
||||
t.Errorf("got wrong result FirstName: expected: %v, actual: %v ", tt.result.FirstName, tt.args.user.FirstName)
|
||||
}
|
||||
if tt.args.user.ResourceOwner != tt.result.ResourceOwner {
|
||||
t.Errorf("got wrong result ResourceOwner: expected: %v, actual: %v ", tt.result.ResourceOwner, tt.args.user.ResourceOwner)
|
||||
}
|
||||
if tt.args.user.LastEmail != tt.result.LastEmail {
|
||||
t.Errorf("got wrong result LastEmail: expected: %v, actual: %v ", tt.result.LastEmail, tt.args.user.LastEmail)
|
||||
}
|
||||
if tt.args.user.VerifiedEmail != tt.result.VerifiedEmail {
|
||||
t.Errorf("got wrong result VerifiedEmail: expected: %v, actual: %v ", tt.result.VerifiedEmail, tt.args.user.VerifiedEmail)
|
||||
}
|
||||
if tt.args.user.LastPhone != tt.result.LastPhone {
|
||||
t.Errorf("got wrong result LastPhone: expected: %v, actual: %v ", tt.result.LastPhone, tt.args.user.LastPhone)
|
||||
}
|
||||
if tt.args.user.VerifiedPhone != tt.result.VerifiedPhone {
|
||||
t.Errorf("got wrong result VerifiedPhone: expected: %v, actual: %v ", tt.result.VerifiedPhone, tt.args.user.VerifiedPhone)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
25
internal/user/repository/view/notify_user.go
Normal file
25
internal/user/repository/view/notify_user.go
Normal file
@ -0,0 +1,25 @@
|
||||
package view
|
||||
|
||||
import (
|
||||
usr_model "github.com/caos/zitadel/internal/user/model"
|
||||
"github.com/caos/zitadel/internal/user/repository/view/model"
|
||||
"github.com/caos/zitadel/internal/view"
|
||||
"github.com/jinzhu/gorm"
|
||||
)
|
||||
|
||||
func NotifyUserByID(db *gorm.DB, table, userID string) (*model.NotifyUser, error) {
|
||||
user := new(model.NotifyUser)
|
||||
query := view.PrepareGetByKey(table, model.UserSearchKey(usr_model.NOTIFYUSERSEARCHKEY_USER_ID), userID)
|
||||
err := query(db, user)
|
||||
return user, err
|
||||
}
|
||||
|
||||
func PutNotifyUser(db *gorm.DB, table string, project *model.NotifyUser) error {
|
||||
save := view.PrepareSave(table)
|
||||
return save(db, project)
|
||||
}
|
||||
|
||||
func DeleteNotifyUser(db *gorm.DB, table, userID string) error {
|
||||
delete := view.PrepareDeleteByKey(table, model.UserSearchKey(usr_model.NOTIFYUSERSEARCHKEY_USER_ID), userID)
|
||||
return delete(db)
|
||||
}
|
@ -76,7 +76,7 @@ func LatestFailedEvent(db *gorm.DB, table, viewName string, sequence uint64) (*F
|
||||
query := PrepareGetByQuery(table, queries...)
|
||||
err := query(db, failedEvent)
|
||||
|
||||
if err == nil {
|
||||
if err == nil && failedEvent.ViewName != "" {
|
||||
return failedEvent, nil
|
||||
}
|
||||
|
||||
|
69
migrations/cockroach/V1.7__notification.sql
Normal file
69
migrations/cockroach/V1.7__notification.sql
Normal file
@ -0,0 +1,69 @@
|
||||
BEGIN;
|
||||
|
||||
CREATE DATABASE notification;
|
||||
|
||||
|
||||
COMMIT;
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE USER notification;
|
||||
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON DATABASE notification TO notification;
|
||||
GRANT SELECT, INSERT, UPDATE ON DATABASE eventstore TO notification;
|
||||
GRANT SELECT, INSERT, UPDATE ON TABLE eventstore.* TO notification;
|
||||
|
||||
COMMIT;
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE notification.locks (
|
||||
locker_id TEXT,
|
||||
locked_until TIMESTAMPTZ,
|
||||
object_type TEXT,
|
||||
|
||||
PRIMARY KEY (object_type)
|
||||
);
|
||||
|
||||
CREATE TABLE notification.current_sequences (
|
||||
view_name TEXT,
|
||||
|
||||
current_sequence BIGINT,
|
||||
|
||||
PRIMARY KEY (view_name)
|
||||
);
|
||||
|
||||
CREATE TABLE notification.failed_event (
|
||||
view_name TEXT,
|
||||
failed_sequence BIGINT,
|
||||
failure_count SMALLINT,
|
||||
err_msg TEXT,
|
||||
|
||||
PRIMARY KEY (view_name, failed_sequence)
|
||||
);
|
||||
|
||||
CREATE TABLE notification.notify_users (
|
||||
id TEXT,
|
||||
|
||||
creation_date TIMESTAMPTZ,
|
||||
change_date TIMESTAMPTZ,
|
||||
|
||||
resource_owner TEXT,
|
||||
user_name TEXT,
|
||||
first_name TEXT,
|
||||
last_name TEXT,
|
||||
nick_Name TEXT,
|
||||
display_name TEXT,
|
||||
preferred_language TEXT,
|
||||
gender SMALLINT,
|
||||
last_email TEXT,
|
||||
verified_email TEXT,
|
||||
last_phone TEXT,
|
||||
verified_phone TEXT,
|
||||
sequence BIGINT,
|
||||
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
|
||||
|
||||
COMMIT;
|
Loading…
Reference in New Issue
Block a user