Skip to content

Commit 335c43a

Browse files
committed
feat(pkg): add new Otp helper
1 parent 62dad99 commit 335c43a

File tree

6 files changed

+157
-26
lines changed

6 files changed

+157
-26
lines changed

src/helpers/Otp.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
/**
2+
* @athenna/common
3+
*
4+
* (c) João Lenon <lenon@athenna.io>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import otpGenerator from 'otp-generator'
11+
12+
export class Otp {
13+
/**
14+
* Generate an OTP token.
15+
*/
16+
public static generate(prefix?: string) {
17+
if (prefix) {
18+
return `${prefix}::${otpGenerator.generate(6, {
19+
digits: true,
20+
specialChars: false,
21+
upperCaseAlphabets: true,
22+
lowerCaseAlphabets: false
23+
})}`
24+
}
25+
26+
return otpGenerator.generate(6, {
27+
digits: true,
28+
specialChars: false,
29+
upperCaseAlphabets: true,
30+
lowerCaseAlphabets: false
31+
})
32+
}
33+
34+
/**
35+
* Return the token without his prefix.
36+
*/
37+
public static getToken(token: string): string {
38+
const prefix = Otp.getPrefix(token)
39+
40+
if (!prefix) {
41+
return token
42+
}
43+
44+
return token.split(`${prefix}::`)[1]
45+
}
46+
47+
/**
48+
* Return the prefix without his token.
49+
*/
50+
public static getPrefix(token: string): string | null {
51+
const prefix = token.split('::')[0]
52+
53+
/**
54+
* Means that the "::" char has not been
55+
* found. So there is no prefix in the token.
56+
*/
57+
if (prefix === token) {
58+
return null
59+
}
60+
61+
return prefix
62+
}
63+
64+
/**
65+
* Inject a prefix in the uuid token.
66+
*/
67+
public static injectPrefix(prefix: string, token: string): string {
68+
return `${prefix}::${token}`
69+
}
70+
71+
/**
72+
* Change the prefix of an OTP token.
73+
*/
74+
public static changePrefix(newPrefix: string, token: string): string {
75+
const otp = this.getToken(token)
76+
77+
return `${newPrefix}::${otp}`
78+
}
79+
80+
/**
81+
* Change the token prefix or generate a new one.
82+
*/
83+
public static changeOrGenerate(prefix: string, token?: string): string {
84+
if (token) {
85+
return this.changePrefix(prefix, token)
86+
}
87+
88+
return this.generate(prefix)
89+
}
90+
}

src/helpers/String.ts

Lines changed: 8 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
*/
99

1010
import pluralize from 'pluralize'
11-
import otpGenerator from 'otp-generator'
1211
import * as changeCase from 'change-case'
1312

1413
import { crc32 } from 'crc'
@@ -55,37 +54,23 @@ export class String {
5554
* @example
5655
* ```ts
5756
* const random = String.random(10) // '1bibr3zxdA'
58-
* const random = String.random(10, { otp: true }) // '41NPH'
5957
* const random = String.random(10, { suffixCRC: true }) // '9-EdWM9OV53876186015'
6058
* ```
6159
*/
6260
public static random(
6361
size: number,
64-
options?: { otp?: boolean; suffixCRC?: boolean }
62+
options?: { suffixCRC?: boolean }
6563
): string {
6664
options = Options.create(options, {
67-
otp: false,
6865
suffixCRC: false
6966
})
7067

71-
let random = ''
72-
73-
if (options.otp) {
74-
random = otpGenerator.generate(size, {
75-
digits: true,
76-
specialChars: false,
77-
upperCaseAlphabets: true,
78-
lowerCaseAlphabets: false
79-
})
80-
} else {
81-
const bits = (size + 1) * 6
82-
const buffer = randomBytes(Math.ceil(bits / 8))
83-
84-
random = String.normalizeBase64(buffer.toString('base64'))
85-
.replace(/-/g, '')
86-
.replace(/_/g, '')
87-
.slice(0, size)
88-
}
68+
const bits = (size + 1) * 6
69+
const buffer = randomBytes(Math.ceil(bits / 8))
70+
const random = String.normalizeBase64(buffer.toString('base64'))
71+
.replace(/-/g, '')
72+
.replace(/_/g, '')
73+
.slice(0, size)
8974

9075
if (options.suffixCRC) {
9176
const crc = crc32(random).toString(30)
@@ -103,7 +88,7 @@ export class String {
10388
* will be removed in the next major version.
10489
*/
10590
public static generateRandom(size: number): string {
106-
return this.random(size, { otp: false, suffixCRC: false })
91+
return this.random(size, { suffixCRC: false })
10792
}
10893

10994
/**

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export * from '#src/helpers/Json'
2727
export * from '#src/helpers/Module'
2828
export * from '#src/helpers/Number'
2929
export * from '#src/helpers/Options'
30+
export * from '#src/helpers/Otp'
3031
export * from '#src/helpers/Parser'
3132
export * from '#src/helpers/Path'
3233
export * from '#src/helpers/Route'

tests/unit/helpers/ModuleTest.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,15 +67,15 @@ export default class ModuleTest {
6767
public async shouldBeAbleToGetAllModulesFirstExportMatchOrDefaultFromAnyPath({ assert }: Context) {
6868
const modules = await Module.getAllFrom(Path.src('helpers'))
6969

70-
assert.lengthOf(modules, 22)
70+
assert.lengthOf(modules, 23)
7171
assert.equal(modules[0].name, 'Clean')
7272
}
7373

7474
@Test()
7575
public async shouldBeAbleToGetAllModulesFirstExportMatchOrDefaultFromAnyPathWithAlias({ assert }: Context) {
7676
const modules = await Module.getAllFromWithAlias(Path.src('helpers'), 'App/Helpers')
7777

78-
assert.lengthOf(modules, 22)
78+
assert.lengthOf(modules, 23)
7979
assert.equal(modules[0].module.name, 'Clean')
8080
assert.equal(modules[0].alias, 'App/Helpers/Clean')
8181
}

tests/unit/helpers/OtpTest.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/**
2+
* @athenna/common
3+
*
4+
* (c) João Lenon <lenon@athenna.io>
5+
*
6+
* For the full copyright and license information, please view the LICENSE
7+
* file that was distributed with this source code.
8+
*/
9+
10+
import otpGenerator from 'otp-generator'
11+
12+
import { Otp } from '#src'
13+
import { Test, type Context } from '@athenna/test'
14+
15+
export default class OtpTest {
16+
private otp = otpGenerator.generate(6, {
17+
digits: true,
18+
specialChars: false,
19+
upperCaseAlphabets: true,
20+
lowerCaseAlphabets: false
21+
})
22+
23+
@Test()
24+
public shouldGetOnlyTheTokenFromPrefixedOtp({ assert }: Context) {
25+
const tokenOtp = Otp.generate('tkn')
26+
27+
assert.equal(Otp.getToken(tokenOtp), tokenOtp.replace('tkn::', ''))
28+
}
29+
30+
@Test()
31+
public shouldGetOnlyThePrefixFromPrefixedOtp({ assert }: Context) {
32+
const tokenOtp = Otp.generate('tkn')
33+
34+
assert.isNull(Otp.getPrefix(this.otp), null)
35+
assert.equal(Otp.getPrefix(tokenOtp), 'tkn')
36+
}
37+
38+
@Test()
39+
public shouldInjectThePrefixInTheToken({ assert }: Context) {
40+
const tokenOtp = Otp.generate()
41+
const injectedPrefix = Otp.injectPrefix('tkn', tokenOtp)
42+
const tokenPrefixedChange = Otp.changePrefix('any', injectedPrefix)
43+
44+
assert.equal(injectedPrefix, `tkn::${tokenOtp}`)
45+
assert.equal(tokenPrefixedChange, `any::${tokenOtp}`)
46+
}
47+
48+
@Test()
49+
public shouldChangeOrGenerateANewToken({ assert }: Context) {
50+
const tokenGenerated = Otp.changeOrGenerate('tkn', undefined)
51+
const tokenChanged = Otp.changeOrGenerate('tkn', `ooo::${this.otp}`)
52+
53+
assert.isDefined(tokenGenerated)
54+
assert.equal(tokenChanged, `tkn::${this.otp}`)
55+
}
56+
}

tests/unit/helpers/StringTest.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ export default class StringTest {
4747
assert.lengthOf(String.random(10), 10)
4848
assert.lengthOf(String.random(20), 20)
4949

50-
assert.lengthOf(String.random(5, { otp: true }), 5)
5150
assert.lengthOf(String.random(40, { suffixCRC: true }), 40)
5251

5352
assert.lengthOf(String.generateRandom(10), 10)

0 commit comments

Comments
 (0)