Skip to content

Commit a8591fd

Browse files
committed
feat(kratos-cli): add verification command
1 parent f66d2d0 commit a8591fd

File tree

3 files changed

+160
-0
lines changed

3 files changed

+160
-0
lines changed

packages/kratos-cli/src/app/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
RegistrationCommand,
1212
RegistrationQuestions,
1313
} from './registration.command';
14+
import { VerificationCommand } from './verification.command';
1415

1516
@Module({
1617
imports: [
@@ -33,6 +34,7 @@ import {
3334
LoginQuestions,
3435
RegistrationCommand,
3536
RegistrationQuestions,
37+
VerificationCommand,
3638
],
3739
})
3840
export class AppModule {}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import { OryFrontendService } from '@getlarge/kratos-client-wrapper';
2+
import { TestingModule } from '@nestjs/testing';
3+
import { Configuration } from '@ory/client';
4+
import { CommandTestFactory } from 'nest-commander-testing';
5+
6+
import { AppModule } from './app.module';
7+
8+
class MockOryFrontendService {
9+
createNativeVerificationFlow = jest.fn();
10+
updateVerificationFlow = jest.fn();
11+
configuration = new Configuration({
12+
basePath: 'http://localhost',
13+
});
14+
get config(): Configuration {
15+
return this.configuration;
16+
}
17+
set config(config: Configuration) {
18+
this.configuration = config;
19+
}
20+
}
21+
22+
describe('VerificationCommand', () => {
23+
let oryFrontendService: OryFrontendService;
24+
let app: TestingModule;
25+
26+
beforeAll(async () => {
27+
app = await CommandTestFactory.createTestingCommand({
28+
imports: [AppModule],
29+
providers: [],
30+
})
31+
.overrideProvider(OryFrontendService)
32+
.useClass(MockOryFrontendService)
33+
.compile();
34+
35+
oryFrontendService = app.get(OryFrontendService);
36+
});
37+
38+
describe('run', () => {
39+
it('should trigger verification flow with code method', async () => {
40+
const email = 'test@test.it';
41+
const method = 'code';
42+
oryFrontendService.createNativeVerificationFlow = jest
43+
.fn()
44+
.mockResolvedValue({
45+
data: {
46+
id: 'flow-id',
47+
},
48+
});
49+
expect(oryFrontendService.config.basePath).toBe('http://localhost');
50+
await expect(
51+
CommandTestFactory.run(app, [
52+
'verify',
53+
'--email',
54+
email,
55+
'--method',
56+
method,
57+
'--basePath',
58+
'http://localhost:4433',
59+
])
60+
).resolves.toBeUndefined();
61+
62+
expect(oryFrontendService.config.basePath).toBe('http://localhost:4433');
63+
expect(
64+
oryFrontendService.createNativeVerificationFlow
65+
).toHaveBeenCalled();
66+
expect(oryFrontendService.updateVerificationFlow).toHaveBeenCalledWith({
67+
flow: 'flow-id',
68+
updateVerificationFlowBody: {
69+
email,
70+
method: 'code',
71+
},
72+
});
73+
});
74+
});
75+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { OryFrontendService } from '@getlarge/kratos-client-wrapper';
2+
import { Logger } from '@nestjs/common';
3+
import { Configuration } from '@ory/client';
4+
import { isEmail } from 'class-validator';
5+
import { Command, CommandRunner, Option } from 'nest-commander';
6+
7+
interface CommandOptions extends Pick<Configuration, 'basePath'> {
8+
email: string;
9+
method: 'code' | 'link';
10+
}
11+
12+
@Command({
13+
name: 'verify',
14+
description: 'Verify email address via Ory Kratos self-service API',
15+
})
16+
export class VerificationCommand extends CommandRunner {
17+
readonly logger = new Logger(VerificationCommand.name);
18+
19+
constructor(private readonly oryFrontendService: OryFrontendService) {
20+
super();
21+
}
22+
23+
async run(inputs: string[], options: CommandOptions): Promise<void> {
24+
const { email, method } = options;
25+
if (options.basePath) {
26+
this.oryFrontendService.config = new Configuration({
27+
...this.oryFrontendService.config,
28+
...options,
29+
});
30+
}
31+
32+
this.logger.debug('init verification flow');
33+
const {
34+
data: { id: flowId },
35+
} = await this.oryFrontendService.createNativeVerificationFlow();
36+
37+
this.logger.debug('complete verification flow');
38+
await this.oryFrontendService.updateVerificationFlow({
39+
flow: flowId,
40+
updateVerificationFlowBody: {
41+
email,
42+
method,
43+
},
44+
});
45+
this.logger.debug(`verification flow ${flowId} completed`);
46+
}
47+
48+
@Option({
49+
flags: '-e, --email <string>',
50+
description: 'Email address to login with',
51+
required: true,
52+
})
53+
parseEmail(val: string): string {
54+
if (isEmail(val)) {
55+
return val;
56+
}
57+
throw new TypeError('Invalid email address');
58+
}
59+
60+
@Option({
61+
flags: '-m, --method [string]',
62+
description: 'Verification method',
63+
required: false,
64+
})
65+
parseVerificationMethod(val: string): string {
66+
if (!val) {
67+
return 'code';
68+
}
69+
if (['code', 'link'].includes(val)) {
70+
return val;
71+
}
72+
throw new TypeError('Invalid verification method');
73+
}
74+
75+
@Option({
76+
flags: '-b, --basePath [string]',
77+
description: 'Ory Kratos Public API URL',
78+
required: false,
79+
})
80+
parseBasePath(val: string): string {
81+
return val;
82+
}
83+
}

0 commit comments

Comments
 (0)