Skip to content

Commit 0460934

Browse files
authored
feat: support phone templates (#1224)
* feat: add support for phone templates - src/context/defaults.ts: add phoneTemplatesDefaults function to strip read-only fields - src/context/directory/handlers/index.ts: import phoneTemplates handler - src/context/yaml/handlers/index.ts: import phoneTemplates handler - src/tools/auth0/handlers/index.ts: import phoneTemplates handler - src/tools/constants.ts: add PHONE_TEMPLATES_DIRECTORY constant - src/types.ts: add PhoneTemplate type and update Assets type - src/context/directory/handlers/phoneTemplates.ts: implement phoneTemplatesHandler for directory context - src/context/yaml/handlers/phoneTemplates.ts: implement phoneTemplatesHandler for YAML context - src/tools/auth0/handlers/phoneTemplates.ts: implement PhoneTemplatesHandler class for managing phone templates - .gitignore: ignore .github/agents directory * feat: add support for phone templates - docs/resource-specific-documentation.md: Added documentation for phone templates. - examples/yaml/tenant.yaml: Included phone templates configuration example. - src/context/directory/handlers/phoneTemplates.ts: Updated to return null for missing templates. - src/context/yaml/handlers/phoneTemplates.ts: Updated to return null for missing templates. - test/context/yaml/context.test.js: Added tests for phone templates processing. - test/utils.js: Mocked phone templates management functions. - examples/directory/phone-templates/otp_enroll.json: Created OTP enrollment template. - examples/directory/phone-templates/otp_verify.json: Created OTP verification template. - test/context/directory/phoneTemplates.test.ts: Added tests for directory context phone templates. - test/context/yaml/phoneTemplates.test.ts: Added tests for YAML context phone templates. - test/tools/auth0/handlers/phoneTemplates.test.ts: Added tests for phone templates handler. * feat: update phone templates to use keyword markers - src/context/directory/handlers/phoneTemplates.ts: remove comment on read-only fields - test/context/directory/phoneTemplates.test.ts: update verification and enrollment code texts to use keyword markers - test/context/yaml/phoneTemplates.test.ts: update verification and enrollment code texts to use keyword markers - test/tools/auth0/handlers/phoneTemplates.test.ts: update verification and enrollment code texts to use keyword markers ---------
1 parent 6038014 commit 0460934

23 files changed

+9841
-2888
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,4 @@ Makefile
1616
.github/copilot-instructions.md
1717
.github/chatmodes/*
1818
.github/prompts/*
19+
.github/agents/*

docs/resource-specific-documentation.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -575,3 +575,56 @@ phoneProviders:
575575
}
576576
]
577577
```
578+
579+
## PhoneTemplates
580+
581+
Phone templates allow you to customize the SMS and voice messages sent to users for phone-based authentication.
582+
Refer to the [Management API](https://auth0.com/docs/api/management/v2/branding/get-phone-templates) for more details.
583+
584+
### YAML Example
585+
586+
```yaml
587+
# Contents of ./tenant.yaml
588+
phoneTemplates:
589+
- type: otp_verify
590+
disabled: false
591+
content:
592+
from: '+12341234567'
593+
body:
594+
text: 'Your verification code is {{ code }}'
595+
voice: 'Your verification code is {{ code }}'
596+
- type: otp_enroll
597+
disabled: false
598+
content:
599+
from: '+12341234567'
600+
body:
601+
text: 'Your enrollment code is {{ code }}'
602+
```
603+
604+
### Directory Example
605+
606+
Create individual JSON files for each template in the `phone-templates` directory:
607+
608+
```text
609+
phone-templates/
610+
├── otp_verify.json
611+
├── otp_enroll.json
612+
├── change_password.json
613+
└── ...
614+
```
615+
616+
Example `phone-templates/otp_verify.json`:
617+
618+
```json
619+
{
620+
"type": "otp_verify",
621+
"disabled": false,
622+
"content": {
623+
"from": "+12341234567",
624+
"body": {
625+
"text": "Your verification code is {{ code }}",
626+
"voice": "Your verification code is {{ code }}"
627+
}
628+
}
629+
}
630+
```
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"type": "otp_enroll",
3+
"disabled": false,
4+
"content": {
5+
"syntax": "liquid",
6+
"from": "+12341234567",
7+
"body": {
8+
"text": "Your enrollment code is {{ code }}",
9+
"voice": "Your enrollment code is {{ code }}"
10+
}
11+
}
12+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"type": "otp_verify",
3+
"disabled": false,
4+
"content": {
5+
"syntax": "liquid",
6+
"from": "+12341234567",
7+
"body": {
8+
"text": "Your verification code is {{ code }}",
9+
"voice": "Your verification code is {{ code }}"
10+
}
11+
}
12+
}

examples/yaml/tenant.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,21 @@ phoneProviders:
111111
credentials:
112112
auth_token: "some_auth_token"
113113

114+
phoneTemplates:
115+
- type: otp_verify
116+
disabled: false
117+
content:
118+
from: "+12341234567"
119+
body:
120+
text: "Your verification code is {{ code }}"
121+
voice: "Your verification code is {{ code }}"
122+
- type: otp_enroll
123+
disabled: false
124+
content:
125+
from: "+12341234567"
126+
body:
127+
text: "Your enrollment code is {{ code }}"
128+
114129
emailProvider:
115130
name: "smtp"
116131
enabled: true

src/context/defaults.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,27 @@ export function phoneProviderDefaults(phoneProvider) {
9090
return updated;
9191
}
9292

93+
export function phoneTemplatesDefaults(phoneTemplate) {
94+
const updated = { ...phoneTemplate };
95+
96+
// Strip read-only fields that are returned by the API but should not be included in exported config
97+
const removeKeysFromOutput = [
98+
'id',
99+
'channel',
100+
'customizable',
101+
'tenant',
102+
'created_at',
103+
'updated_at',
104+
];
105+
removeKeysFromOutput.forEach((key) => {
106+
if (key in updated) {
107+
delete updated[key];
108+
}
109+
});
110+
111+
return updated;
112+
}
113+
93114
export function connectionDefaults(connection) {
94115
if (connection.options) {
95116
// Mask secret for key: connection.options.client_secret

src/context/directory/handlers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import triggers from './triggers';
2020
import attackProtection from './attackProtection';
2121
import branding from './branding';
2222
import phoneProviders from './phoneProvider';
23+
import phoneTemplates from './phoneTemplates';
2324
import logStreams from './logStreams';
2425
import prompts from './prompts';
2526
import customDomains from './customDomains';
@@ -70,6 +71,7 @@ const directoryHandlers: {
7071
attackProtection,
7172
branding,
7273
phoneProviders,
74+
phoneTemplates,
7375
logStreams,
7476
prompts,
7577
customDomains,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import path from 'path';
2+
import fs from 'fs-extra';
3+
import { constants } from '../../../tools';
4+
5+
import { existsMustBeDir, getFiles, dumpJSON, loadJSON } from '../../../utils';
6+
import { DirectoryHandler } from '.';
7+
import DirectoryContext from '..';
8+
import { ParsedAsset } from '../../../types';
9+
import { PhoneTemplate } from '../../../tools/auth0/handlers/phoneTemplates';
10+
import { phoneTemplatesDefaults } from '../../defaults';
11+
12+
type ParsedPhoneTemplates = ParsedAsset<'phoneTemplates', PhoneTemplate[]>;
13+
14+
function parse(context: DirectoryContext): ParsedPhoneTemplates {
15+
const phoneTemplatesFolder = path.join(context.filePath, constants.PHONE_TEMPLATES_DIRECTORY);
16+
if (!existsMustBeDir(phoneTemplatesFolder)) return { phoneTemplates: null }; // Skip
17+
18+
const files = getFiles(phoneTemplatesFolder, ['.json']);
19+
20+
const phoneTemplates = files.map((f) =>
21+
loadJSON(f, {
22+
mappings: context.mappings,
23+
disableKeywordReplacement: context.disableKeywordReplacement,
24+
})
25+
);
26+
27+
return { phoneTemplates };
28+
}
29+
30+
async function dump(context: DirectoryContext): Promise<void> {
31+
const { phoneTemplates } = context.assets;
32+
33+
if (!phoneTemplates) {
34+
return;
35+
} // Skip, nothing to dump
36+
37+
const phoneTemplatesFolder = path.join(context.filePath, constants.PHONE_TEMPLATES_DIRECTORY);
38+
fs.ensureDirSync(phoneTemplatesFolder);
39+
40+
phoneTemplates.forEach((template) => {
41+
const templateWithDefaults = phoneTemplatesDefaults(template);
42+
const templateFile = path.join(phoneTemplatesFolder, `${template.type}.json`);
43+
dumpJSON(templateFile, templateWithDefaults);
44+
});
45+
}
46+
47+
const phoneTemplatesHandler: DirectoryHandler<ParsedPhoneTemplates> = {
48+
parse,
49+
dump,
50+
};
51+
52+
export default phoneTemplatesHandler;

src/context/yaml/handlers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import triggers from './triggers';
2020
import attackProtection from './attackProtection';
2121
import branding from './branding';
2222
import phoneProviders from './phoneProvider';
23+
import phoneTemplates from './phoneTemplates';
2324
import logStreams from './logStreams';
2425
import prompts from './prompts';
2526
import customDomains from './customDomains';
@@ -68,6 +69,7 @@ const yamlHandlers: { [key in AssetTypes]: YAMLHandler<{ [key: string]: unknown
6869
attackProtection,
6970
branding,
7071
phoneProviders,
72+
phoneTemplates,
7173
logStreams,
7274
prompts,
7375
customDomains,
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import { YAMLHandler } from '.';
2+
import YAMLContext from '..';
3+
import { PhoneTemplate } from '../../../tools/auth0/handlers/phoneTemplates';
4+
import { ParsedAsset } from '../../../types';
5+
import { phoneTemplatesDefaults } from '../../defaults';
6+
7+
type ParsedPhoneTemplates = ParsedAsset<'phoneTemplates', PhoneTemplate[]>;
8+
9+
async function parse(context: YAMLContext): Promise<ParsedPhoneTemplates> {
10+
const { phoneTemplates } = context.assets;
11+
12+
if (!phoneTemplates) return { phoneTemplates: null };
13+
14+
return {
15+
phoneTemplates,
16+
};
17+
}
18+
19+
async function dump(context: YAMLContext): Promise<ParsedPhoneTemplates> {
20+
const { phoneTemplates } = context.assets;
21+
22+
if (!phoneTemplates) return { phoneTemplates: null };
23+
24+
const processedTemplates = phoneTemplates.map((template) => phoneTemplatesDefaults(template));
25+
26+
return {
27+
phoneTemplates: processedTemplates,
28+
};
29+
}
30+
31+
const phoneTemplatesHandler: YAMLHandler<ParsedPhoneTemplates> = {
32+
parse,
33+
dump,
34+
};
35+
36+
export default phoneTemplatesHandler;

0 commit comments

Comments
 (0)