@flusys/nestjs-auth 7.0.1 → 7.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +67 -0
- package/cjs/config/auth.constants.js +8 -0
- package/cjs/config/event-actions.js +4 -2
- package/cjs/config/message-keys.js +27 -0
- package/cjs/controllers/index.js +2 -0
- package/cjs/controllers/social-auth-config.controller.js +107 -0
- package/cjs/controllers/social-auth.controller.js +175 -0
- package/cjs/controllers/social-auth.controller.spec.js +79 -0
- package/cjs/docs/auth-swagger.config.js +9 -1
- package/cjs/dtos/index.js +1 -0
- package/cjs/dtos/social-auth.dto.js +318 -0
- package/cjs/entities/index.js +3 -1
- package/cjs/entities/index.spec.js +12 -1
- package/cjs/entities/social-auth-config.entity.js +102 -0
- package/cjs/enums/index.js +1 -0
- package/cjs/enums/social-provider-type.enum.js +17 -0
- package/cjs/index.js +1 -0
- package/cjs/interfaces/index.js +1 -0
- package/cjs/interfaces/social-auth.interface.js +4 -0
- package/cjs/modules/auth.module.js +6 -0
- package/cjs/modules/auth.module.spec.js +15 -0
- package/cjs/providers/facebook.social-provider.js +73 -0
- package/cjs/providers/google.social-provider.js +68 -0
- package/cjs/providers/index.js +18 -0
- package/cjs/providers/linkedin.social-provider.js +66 -0
- package/cjs/providers/microsoft.social-provider.js +78 -0
- package/cjs/providers/oauth-http.util.js +107 -0
- package/cjs/providers/social-provider.registry.js +24 -0
- package/cjs/providers/social-provider.registry.spec.js +135 -0
- package/cjs/services/auth-config.service.js +3 -0
- package/cjs/services/auth-config.service.spec.js +3 -0
- package/cjs/services/auth-datasource.provider.js +4 -0
- package/cjs/services/auth-datasource.provider.spec.js +30 -0
- package/cjs/services/authentication.service.js +38 -8
- package/cjs/services/authentication.service.spec.js +58 -0
- package/cjs/services/index.js +2 -0
- package/cjs/services/social-auth-config.service.js +103 -0
- package/cjs/services/social-auth-config.service.spec.js +71 -0
- package/cjs/services/social-auth.service.js +310 -0
- package/cjs/services/social-auth.service.spec.js +310 -0
- package/config/auth.constants.d.ts +2 -0
- package/config/event-actions.d.ts +2 -0
- package/config/message-keys.d.ts +21 -0
- package/controllers/index.d.ts +2 -0
- package/controllers/social-auth-config.controller.d.ts +22 -0
- package/controllers/social-auth.controller.d.ts +11 -0
- package/dtos/index.d.ts +1 -0
- package/dtos/social-auth.dto.d.ts +42 -0
- package/entities/index.d.ts +3 -1
- package/entities/social-auth-config.entity.d.ts +9 -0
- package/enums/index.d.ts +1 -0
- package/enums/social-provider-type.enum.d.ts +6 -0
- package/fesm/config/auth.constants.js +3 -0
- package/fesm/config/event-actions.js +4 -2
- package/fesm/config/message-keys.js +21 -0
- package/fesm/controllers/index.js +2 -0
- package/fesm/controllers/social-auth-config.controller.js +97 -0
- package/fesm/controllers/social-auth.controller.js +165 -0
- package/fesm/controllers/social-auth.controller.spec.js +75 -0
- package/fesm/docs/auth-swagger.config.js +9 -1
- package/fesm/dtos/index.js +1 -0
- package/fesm/dtos/social-auth.dto.js +282 -0
- package/fesm/entities/index.js +4 -1
- package/fesm/entities/index.spec.js +12 -1
- package/fesm/entities/social-auth-config.entity.js +92 -0
- package/fesm/enums/index.js +1 -0
- package/fesm/enums/social-provider-type.enum.js +7 -0
- package/fesm/index.js +1 -0
- package/fesm/interfaces/index.js +1 -0
- package/fesm/interfaces/social-auth.interface.js +1 -0
- package/fesm/modules/auth.module.js +8 -2
- package/fesm/modules/auth.module.spec.js +17 -2
- package/fesm/providers/facebook.social-provider.js +63 -0
- package/fesm/providers/google.social-provider.js +58 -0
- package/fesm/providers/index.js +1 -0
- package/fesm/providers/linkedin.social-provider.js +56 -0
- package/fesm/providers/microsoft.social-provider.js +68 -0
- package/fesm/providers/oauth-http.util.js +77 -0
- package/fesm/providers/social-provider.registry.js +14 -0
- package/fesm/providers/social-provider.registry.spec.js +131 -0
- package/fesm/services/auth-config.service.js +3 -0
- package/fesm/services/auth-config.service.spec.js +3 -0
- package/fesm/services/auth-datasource.provider.js +4 -0
- package/fesm/services/auth-datasource.provider.spec.js +30 -0
- package/fesm/services/authentication.service.js +38 -8
- package/fesm/services/authentication.service.spec.js +58 -0
- package/fesm/services/index.js +2 -0
- package/fesm/services/social-auth-config.service.js +93 -0
- package/fesm/services/social-auth-config.service.spec.js +67 -0
- package/fesm/services/social-auth.service.js +300 -0
- package/fesm/services/social-auth.service.spec.js +265 -0
- package/index.d.ts +1 -0
- package/interfaces/index.d.ts +1 -0
- package/interfaces/social-auth.interface.d.ts +43 -0
- package/package.json +3 -3
- package/providers/facebook.social-provider.d.ts +10 -0
- package/providers/google.social-provider.d.ts +10 -0
- package/providers/index.d.ts +1 -0
- package/providers/linkedin.social-provider.d.ts +10 -0
- package/providers/microsoft.social-provider.d.ts +10 -0
- package/providers/oauth-http.util.d.ts +10 -0
- package/providers/social-provider.registry.d.ts +3 -0
- package/services/auth-config.service.d.ts +1 -0
- package/services/auth-datasource.provider.d.ts +1 -0
- package/services/authentication.service.d.ts +4 -0
- package/services/index.d.ts +2 -0
- package/services/social-auth-config.service.d.ts +21 -0
- package/services/social-auth.service.d.ts +24 -0
|
@@ -131,6 +131,7 @@ describe('AuthenticationService', ()=>{
|
|
|
131
131
|
let mockUserEnricher;
|
|
132
132
|
let companyFeatureEnabled;
|
|
133
133
|
let emailVerificationEnabled;
|
|
134
|
+
let signUpEnabled;
|
|
134
135
|
function buildService(overrides = {}) {
|
|
135
136
|
return new _authenticationservice.AuthenticationService(mockJwtService, mockAuthConfig, 'cacheManager' in overrides ? overrides.cacheManager : mockCacheManager, 'dataSourceProvider' in overrides ? overrides.dataSourceProvider : mockDataSourceProvider, 'sessionService' in overrides ? overrides.sessionService : mockSessionService, 'authEmailService' in overrides ? overrides.authEmailService : mockAuthEmailService, 'userEnricher' in overrides ? overrides.userEnricher : mockUserEnricher, 'notificationAdapter' in overrides ? overrides.notificationAdapter : undefined);
|
|
136
137
|
}
|
|
@@ -147,6 +148,7 @@ describe('AuthenticationService', ()=>{
|
|
|
147
148
|
});
|
|
148
149
|
companyFeatureEnabled = false;
|
|
149
150
|
emailVerificationEnabled = false;
|
|
151
|
+
signUpEnabled = true;
|
|
150
152
|
mockJwtService = {
|
|
151
153
|
sign: jest.fn().mockReturnValue('signed-token'),
|
|
152
154
|
verify: jest.fn()
|
|
@@ -154,6 +156,7 @@ describe('AuthenticationService', ()=>{
|
|
|
154
156
|
mockAuthConfig = {
|
|
155
157
|
isCompanyFeatureEnabled: jest.fn().mockImplementation(()=>companyFeatureEnabled),
|
|
156
158
|
isEmailVerificationEnabled: jest.fn().mockImplementation(()=>emailVerificationEnabled),
|
|
159
|
+
isSignUpEnabled: jest.fn().mockImplementation(()=>signUpEnabled),
|
|
157
160
|
getJwtSecret: jest.fn().mockReturnValue('jwt-secret'),
|
|
158
161
|
getJwtExpiration: jest.fn().mockReturnValue('1h'),
|
|
159
162
|
getRefreshTokenSecret: jest.fn().mockReturnValue('refresh-secret'),
|
|
@@ -167,6 +170,7 @@ describe('AuthenticationService', ()=>{
|
|
|
167
170
|
mockDataSourceProvider = {
|
|
168
171
|
isCompanyFeatureEnabled: jest.fn().mockImplementation(()=>companyFeatureEnabled),
|
|
169
172
|
isEmailVerificationEnabled: jest.fn().mockImplementation(()=>emailVerificationEnabled),
|
|
173
|
+
isSignUpEnabled: jest.fn().mockImplementation(()=>signUpEnabled),
|
|
170
174
|
getRepository: jest.fn().mockImplementation(async (entity)=>{
|
|
171
175
|
if (entity === _appuserentity.AppUser) return userRepo;
|
|
172
176
|
if (entity === _companyentity.Company) return companyRepo;
|
|
@@ -341,6 +345,20 @@ describe('AuthenticationService', ()=>{
|
|
|
341
345
|
password: 'Password1!'
|
|
342
346
|
})).rejects.toThrow(_common.BadRequestException);
|
|
343
347
|
});
|
|
348
|
+
it('throws BadRequestException when the email belongs to a permanently deleted account', async ()=>{
|
|
349
|
+
userRepo.findOne.mockResolvedValue(buildUser({
|
|
350
|
+
deletedAt: new Date()
|
|
351
|
+
}));
|
|
352
|
+
const service = buildService();
|
|
353
|
+
await expect(service.register({
|
|
354
|
+
email: 'user@example.com',
|
|
355
|
+
name: 'Test',
|
|
356
|
+
password: 'Password1!'
|
|
357
|
+
})).rejects.toThrow(_common.BadRequestException);
|
|
358
|
+
expect(userRepo.findOne).toHaveBeenCalledWith(expect.objectContaining({
|
|
359
|
+
withDeleted: true
|
|
360
|
+
}));
|
|
361
|
+
});
|
|
344
362
|
it('routes existing users to company registration when the company feature is enabled', async ()=>{
|
|
345
363
|
companyFeatureEnabled = true;
|
|
346
364
|
userRepo.findOne.mockResolvedValue(buildUser());
|
|
@@ -362,6 +380,16 @@ describe('AuthenticationService', ()=>{
|
|
|
362
380
|
accessToken: expect.any(String)
|
|
363
381
|
}));
|
|
364
382
|
});
|
|
383
|
+
it('throws BadRequestException for a new user when sign-up is disabled', async ()=>{
|
|
384
|
+
signUpEnabled = false;
|
|
385
|
+
userRepo.findOne.mockResolvedValue(null);
|
|
386
|
+
const service = buildService();
|
|
387
|
+
await expect(service.register({
|
|
388
|
+
email: 'new@example.com',
|
|
389
|
+
name: 'New User',
|
|
390
|
+
password: 'Password1!'
|
|
391
|
+
})).rejects.toThrow(_common.BadRequestException);
|
|
392
|
+
});
|
|
365
393
|
it('sends a verification email and returns requiresEmailVerification when email verification is enabled', async ()=>{
|
|
366
394
|
emailVerificationEnabled = true;
|
|
367
395
|
userRepo.findOne.mockResolvedValue(null);
|
|
@@ -868,6 +896,36 @@ describe('AuthenticationService', ()=>{
|
|
|
868
896
|
})).rejects.toThrow(_common.BadRequestException);
|
|
869
897
|
expect(queryRunner.rollbackTransaction).toHaveBeenCalled();
|
|
870
898
|
});
|
|
899
|
+
it('throws BadRequestException when the email belongs to a permanently deleted account', async ()=>{
|
|
900
|
+
const queryRunner = {
|
|
901
|
+
connect: jest.fn(),
|
|
902
|
+
startTransaction: jest.fn(),
|
|
903
|
+
commitTransaction: jest.fn(),
|
|
904
|
+
rollbackTransaction: jest.fn(),
|
|
905
|
+
release: jest.fn(),
|
|
906
|
+
manager: {
|
|
907
|
+
findOne: jest.fn().mockResolvedValue(buildUser({
|
|
908
|
+
deletedAt: new Date()
|
|
909
|
+
})),
|
|
910
|
+
create: jest.fn(),
|
|
911
|
+
save: jest.fn()
|
|
912
|
+
}
|
|
913
|
+
};
|
|
914
|
+
mockDataSourceProvider.getDataSource.mockResolvedValue({
|
|
915
|
+
createQueryRunner: ()=>queryRunner
|
|
916
|
+
});
|
|
917
|
+
const service = buildService();
|
|
918
|
+
await expect(service.createUserFromPendingRegistration({
|
|
919
|
+
email: 'user@example.com',
|
|
920
|
+
name: 'Test',
|
|
921
|
+
passwordHash: 'hash'
|
|
922
|
+
})).rejects.toThrow(_common.BadRequestException);
|
|
923
|
+
expect(queryRunner.manager.findOne).toHaveBeenCalledWith(_appuserentity.AppUser, expect.objectContaining({
|
|
924
|
+
withDeleted: true
|
|
925
|
+
}));
|
|
926
|
+
expect(queryRunner.manager.create).not.toHaveBeenCalled();
|
|
927
|
+
expect(queryRunner.rollbackTransaction).toHaveBeenCalled();
|
|
928
|
+
});
|
|
871
929
|
it('creates the user with the email marked as verified', async ()=>{
|
|
872
930
|
const queryRunner = {
|
|
873
931
|
connect: jest.fn(),
|
package/cjs/services/index.js
CHANGED
|
@@ -5,6 +5,8 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
_export_star(require("./authentication.service"), exports);
|
|
6
6
|
_export_star(require("./auth-email.service"), exports);
|
|
7
7
|
_export_star(require("./company-selection-session.service"), exports);
|
|
8
|
+
_export_star(require("./social-auth.service"), exports);
|
|
9
|
+
_export_star(require("./social-auth-config.service"), exports);
|
|
8
10
|
_export_star(require("./user.service"), exports);
|
|
9
11
|
_export_star(require("./company.service"), exports);
|
|
10
12
|
_export_star(require("./branch.service"), exports);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "SocialAuthConfigService", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return SocialAuthConfigService;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _classes = require("@flusys/nestjs-shared/classes");
|
|
12
|
+
const _modules = require("@flusys/nestjs-shared/modules");
|
|
13
|
+
const _common = require("@nestjs/common");
|
|
14
|
+
const _typeorm = require("typeorm");
|
|
15
|
+
const _config = require("../config");
|
|
16
|
+
const _socialauthconfigentity = require("../entities/social-auth-config.entity");
|
|
17
|
+
const _authdatasourceprovider = require("./auth-datasource.provider");
|
|
18
|
+
function _define_property(obj, key, value) {
|
|
19
|
+
if (key in obj) {
|
|
20
|
+
Object.defineProperty(obj, key, {
|
|
21
|
+
value: value,
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
writable: true
|
|
25
|
+
});
|
|
26
|
+
} else {
|
|
27
|
+
obj[key] = value;
|
|
28
|
+
}
|
|
29
|
+
return obj;
|
|
30
|
+
}
|
|
31
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
32
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
33
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
34
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
35
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
36
|
+
}
|
|
37
|
+
function _ts_metadata(k, v) {
|
|
38
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
39
|
+
}
|
|
40
|
+
function _ts_param(paramIndex, decorator) {
|
|
41
|
+
return function(target, key) {
|
|
42
|
+
decorator(target, key, paramIndex);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
let SocialAuthConfigService = class SocialAuthConfigService extends _classes.ApiService {
|
|
46
|
+
async beforeInsertOperation(dto, _user, _queryRunner) {
|
|
47
|
+
for (const entry of Array.isArray(dto) ? dto : [
|
|
48
|
+
dto
|
|
49
|
+
]){
|
|
50
|
+
await this.ensureProviderUnclaimed(entry.provider);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async beforeUpdateOperation(dto, _user, _queryRunner) {
|
|
54
|
+
for (const entry of Array.isArray(dto) ? dto : [
|
|
55
|
+
dto
|
|
56
|
+
]){
|
|
57
|
+
if (entry.provider) await this.ensureProviderUnclaimed(entry.provider, entry.id);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async ensureProviderUnclaimed(provider, exceptId) {
|
|
61
|
+
await this.ensureDataSourceRepository();
|
|
62
|
+
const existing = await this.repository.findOne({
|
|
63
|
+
where: {
|
|
64
|
+
provider,
|
|
65
|
+
deletedAt: (0, _typeorm.IsNull)(),
|
|
66
|
+
...exceptId ? {
|
|
67
|
+
id: (0, _typeorm.Not)(exceptId)
|
|
68
|
+
} : {}
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
if (existing) {
|
|
72
|
+
throw new _common.ConflictException({
|
|
73
|
+
message: `Social provider "${provider}" is already configured`,
|
|
74
|
+
messageKey: _config.SOCIAL_AUTH_CONFIG_MESSAGES.PROVIDER_ALREADY_CONFIGURED,
|
|
75
|
+
messageVariables: {
|
|
76
|
+
provider
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
async getExtraManipulateQuery(query, filterDto, user) {
|
|
82
|
+
const result = await super.getExtraManipulateQuery(query, filterDto, user);
|
|
83
|
+
query.orderBy(`${this.entityName}.provider`, 'ASC');
|
|
84
|
+
return result;
|
|
85
|
+
}
|
|
86
|
+
constructor(cacheManager, utilsService, dataSourceProvider){
|
|
87
|
+
super(_config.AUTH_EVENT_ENTITIES.SOCIAL_AUTH_CONFIG, cacheManager, utilsService, SocialAuthConfigService.name, true, _config.AUTH_EVENT_MODULE, dataSourceProvider, _socialauthconfigentity.SocialAuthConfig), _define_property(this, "cacheManager", void 0), _define_property(this, "utilsService", void 0), this.cacheManager = cacheManager, this.utilsService = utilsService;
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
SocialAuthConfigService = _ts_decorate([
|
|
91
|
+
(0, _common.Injectable)({
|
|
92
|
+
scope: _common.Scope.REQUEST
|
|
93
|
+
}),
|
|
94
|
+
_ts_param(0, (0, _common.Inject)('CACHE_INSTANCE')),
|
|
95
|
+
_ts_param(1, (0, _common.Inject)(_modules.UtilsService)),
|
|
96
|
+
_ts_param(2, (0, _common.Inject)(_authdatasourceprovider.AuthDataSourceProvider)),
|
|
97
|
+
_ts_metadata("design:type", Function),
|
|
98
|
+
_ts_metadata("design:paramtypes", [
|
|
99
|
+
typeof _classes.HybridCache === "undefined" ? Object : _classes.HybridCache,
|
|
100
|
+
typeof _modules.UtilsService === "undefined" ? Object : _modules.UtilsService,
|
|
101
|
+
typeof _authdatasourceprovider.AuthDataSourceProvider === "undefined" ? Object : _authdatasourceprovider.AuthDataSourceProvider
|
|
102
|
+
])
|
|
103
|
+
], SocialAuthConfigService);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
const _common = require("@nestjs/common");
|
|
6
|
+
const _repositorymock = require("@test-utils/mocks/repository.mock");
|
|
7
|
+
const _typeorm = require("typeorm");
|
|
8
|
+
const _socialprovidertypeenum = require("../enums/social-provider-type.enum");
|
|
9
|
+
const _socialauthconfigservice = require("./social-auth-config.service");
|
|
10
|
+
describe('SocialAuthConfigService', ()=>{
|
|
11
|
+
let repo;
|
|
12
|
+
let service;
|
|
13
|
+
beforeEach(()=>{
|
|
14
|
+
repo = (0, _repositorymock.createMockRepository)();
|
|
15
|
+
const dataSourceProvider = {
|
|
16
|
+
getRepository: jest.fn().mockResolvedValue(repo)
|
|
17
|
+
};
|
|
18
|
+
service = new _socialauthconfigservice.SocialAuthConfigService({
|
|
19
|
+
get: jest.fn(),
|
|
20
|
+
set: jest.fn(),
|
|
21
|
+
del: jest.fn()
|
|
22
|
+
}, {}, dataSourceProvider);
|
|
23
|
+
});
|
|
24
|
+
const beforeInsert = (dto)=>service.beforeInsertOperation(dto, null, {});
|
|
25
|
+
const beforeUpdate = (dto)=>service.beforeUpdateOperation(dto, null, {});
|
|
26
|
+
it('allows the first configuration of a provider', async ()=>{
|
|
27
|
+
repo.findOne.mockResolvedValue(null);
|
|
28
|
+
await expect(beforeInsert({
|
|
29
|
+
provider: _socialprovidertypeenum.SocialProviderTypeEnum.GOOGLE
|
|
30
|
+
})).resolves.toBeUndefined();
|
|
31
|
+
});
|
|
32
|
+
it('rejects a second row for a provider that is already configured', async ()=>{
|
|
33
|
+
repo.findOne.mockResolvedValue({
|
|
34
|
+
id: 'existing'
|
|
35
|
+
});
|
|
36
|
+
await expect(beforeInsert({
|
|
37
|
+
provider: _socialprovidertypeenum.SocialProviderTypeEnum.GOOGLE
|
|
38
|
+
})).rejects.toThrow(_common.ConflictException);
|
|
39
|
+
});
|
|
40
|
+
it('checks every provider in a batch insert', async ()=>{
|
|
41
|
+
repo.findOne.mockResolvedValue(null);
|
|
42
|
+
await beforeInsert([
|
|
43
|
+
{
|
|
44
|
+
provider: _socialprovidertypeenum.SocialProviderTypeEnum.GOOGLE
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
provider: _socialprovidertypeenum.SocialProviderTypeEnum.FACEBOOK
|
|
48
|
+
}
|
|
49
|
+
]);
|
|
50
|
+
expect(repo.findOne).toHaveBeenCalledTimes(2);
|
|
51
|
+
});
|
|
52
|
+
it('lets a row keep its own provider on update', async ()=>{
|
|
53
|
+
repo.findOne.mockResolvedValue(null);
|
|
54
|
+
await beforeUpdate({
|
|
55
|
+
id: 'config-1',
|
|
56
|
+
provider: _socialprovidertypeenum.SocialProviderTypeEnum.GOOGLE
|
|
57
|
+
});
|
|
58
|
+
expect(repo.findOne).toHaveBeenCalledWith(expect.objectContaining({
|
|
59
|
+
where: expect.objectContaining({
|
|
60
|
+
id: (0, _typeorm.Not)('config-1')
|
|
61
|
+
})
|
|
62
|
+
}));
|
|
63
|
+
});
|
|
64
|
+
it('skips the check for an update that does not touch the provider', async ()=>{
|
|
65
|
+
await beforeUpdate({
|
|
66
|
+
id: 'config-1',
|
|
67
|
+
isEnabled: false
|
|
68
|
+
});
|
|
69
|
+
expect(repo.findOne).not.toHaveBeenCalled();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", {
|
|
3
|
+
value: true
|
|
4
|
+
});
|
|
5
|
+
Object.defineProperty(exports, "SocialAuthService", {
|
|
6
|
+
enumerable: true,
|
|
7
|
+
get: function() {
|
|
8
|
+
return SocialAuthService;
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
const _nestjsshared = require("@flusys/nestjs-shared");
|
|
12
|
+
const _classes = require("@flusys/nestjs-shared/classes");
|
|
13
|
+
const _constants = require("@flusys/nestjs-shared/constants");
|
|
14
|
+
const _common = require("@nestjs/common");
|
|
15
|
+
const _crypto = require("crypto");
|
|
16
|
+
const _typeorm = require("typeorm");
|
|
17
|
+
const _config = require("../config");
|
|
18
|
+
const _appuserentity = require("../entities/app-user.entity");
|
|
19
|
+
const _socialauthconfigentity = require("../entities/social-auth-config.entity");
|
|
20
|
+
const _socialprovidertypeenum = require("../enums/social-provider-type.enum");
|
|
21
|
+
const _socialproviderregistry = require("../providers/social-provider.registry");
|
|
22
|
+
const _authdatasourceprovider = require("./auth-datasource.provider");
|
|
23
|
+
const _authenticationservice = require("./authentication.service");
|
|
24
|
+
function _define_property(obj, key, value) {
|
|
25
|
+
if (key in obj) {
|
|
26
|
+
Object.defineProperty(obj, key, {
|
|
27
|
+
value: value,
|
|
28
|
+
enumerable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true
|
|
31
|
+
});
|
|
32
|
+
} else {
|
|
33
|
+
obj[key] = value;
|
|
34
|
+
}
|
|
35
|
+
return obj;
|
|
36
|
+
}
|
|
37
|
+
function _ts_decorate(decorators, target, key, desc) {
|
|
38
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
39
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
40
|
+
else for(var i = decorators.length - 1; i >= 0; i--)if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
41
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
42
|
+
}
|
|
43
|
+
function _ts_metadata(k, v) {
|
|
44
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
45
|
+
}
|
|
46
|
+
function _ts_param(paramIndex, decorator) {
|
|
47
|
+
return function(target, key) {
|
|
48
|
+
decorator(target, key, paramIndex);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
let SocialAuthService = class SocialAuthService {
|
|
52
|
+
async listEnabledProviders(redirectUri) {
|
|
53
|
+
const repository = await this.dataSourceProvider.getRepository(_socialauthconfigentity.SocialAuthConfig);
|
|
54
|
+
const configs = await repository.find({
|
|
55
|
+
where: {
|
|
56
|
+
isEnabled: true,
|
|
57
|
+
deletedAt: (0, _typeorm.IsNull)()
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
return configs.filter((config)=>!!config.clientId && !!config.clientSecret).map((config)=>({
|
|
61
|
+
provider: config.provider,
|
|
62
|
+
redirectUri
|
|
63
|
+
}));
|
|
64
|
+
}
|
|
65
|
+
async createAuthorization(provider, redirectUri) {
|
|
66
|
+
const config = await this.getUsableConfig(provider);
|
|
67
|
+
const state = (0, _crypto.randomBytes)(24).toString('hex');
|
|
68
|
+
await this.getCache().set(this.stateKey(state), {
|
|
69
|
+
provider,
|
|
70
|
+
redirectUri
|
|
71
|
+
}, _config.SOCIAL_STATE_TTL_MS);
|
|
72
|
+
return {
|
|
73
|
+
provider,
|
|
74
|
+
authorizationUrl: (0, _socialproviderregistry.getSocialProvider)(provider).buildAuthorizationUrl(this.toCredentials(config, redirectUri), state),
|
|
75
|
+
state
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
async completeLogin(code, state, redirectUri) {
|
|
79
|
+
const provider = await this.consumeState(state, redirectUri);
|
|
80
|
+
const config = await this.getUsableConfig(provider);
|
|
81
|
+
const profile = await (0, _socialproviderregistry.getSocialProvider)(provider).fetchProfile(this.toCredentials(config, redirectUri), code);
|
|
82
|
+
if (!profile.providerUserId) {
|
|
83
|
+
throw new _common.UnauthorizedException({
|
|
84
|
+
message: `${provider} did not return an account identifier`,
|
|
85
|
+
messageKey: _config.SOCIAL_AUTH_MESSAGES.PROFILE_INCOMPLETE,
|
|
86
|
+
messageVariables: {
|
|
87
|
+
provider
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
const user = await this.resolveUser(provider, profile);
|
|
92
|
+
const response = await this.authService.issueLoginForUser(user);
|
|
93
|
+
this.publishSocialEvent(_config.AUTH_EVENT_ENTITIES.SESSION, _config.AUTH_EVENT_ACTIONS.SOCIAL_LOGGED_IN, user, provider);
|
|
94
|
+
return response;
|
|
95
|
+
}
|
|
96
|
+
async resolveUser(provider, profile) {
|
|
97
|
+
this.ensureVerifiedEmail(provider, profile);
|
|
98
|
+
const userRepository = await this.dataSourceProvider.getRepository(_appuserentity.AppUser);
|
|
99
|
+
const existing = await userRepository.findOne({
|
|
100
|
+
where: {
|
|
101
|
+
email: profile.email
|
|
102
|
+
},
|
|
103
|
+
withDeleted: true
|
|
104
|
+
});
|
|
105
|
+
if (existing?.deletedAt) {
|
|
106
|
+
throw new _common.UnauthorizedException({
|
|
107
|
+
message: 'This account has been permanently deleted. Please contact your system administrator.',
|
|
108
|
+
messageKey: _config.SOCIAL_AUTH_MESSAGES.ACCOUNT_DELETED
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
if (existing) {
|
|
112
|
+
// The provider already proved ownership of this email (ensureVerifiedEmail above),
|
|
113
|
+
// so a social login carries at least as much proof as clicking a verification link.
|
|
114
|
+
if (!existing.emailVerified) {
|
|
115
|
+
existing.emailVerified = true;
|
|
116
|
+
await userRepository.save(existing);
|
|
117
|
+
}
|
|
118
|
+
return existing;
|
|
119
|
+
}
|
|
120
|
+
const user = await this.registerUser(provider, userRepository, profile);
|
|
121
|
+
this.publishSocialEvent(_config.AUTH_EVENT_ENTITIES.USER, _config.AUTH_EVENT_ACTIONS.REGISTERED, user, provider);
|
|
122
|
+
return user;
|
|
123
|
+
}
|
|
124
|
+
ensureVerifiedEmail(provider, profile) {
|
|
125
|
+
if (!profile.email) {
|
|
126
|
+
throw new _common.UnauthorizedException({
|
|
127
|
+
message: `${provider} did not share an email address`,
|
|
128
|
+
messageKey: _config.SOCIAL_AUTH_MESSAGES.EMAIL_REQUIRED,
|
|
129
|
+
messageVariables: {
|
|
130
|
+
provider
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (!profile.emailVerified) {
|
|
135
|
+
throw new _common.UnauthorizedException({
|
|
136
|
+
message: `${provider} has not verified this email address`,
|
|
137
|
+
messageKey: _config.SOCIAL_AUTH_MESSAGES.EMAIL_NOT_VERIFIED,
|
|
138
|
+
messageVariables: {
|
|
139
|
+
provider
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
async registerUser(provider, userRepository, profile) {
|
|
145
|
+
if (!this.dataSourceProvider.isSignUpEnabled()) {
|
|
146
|
+
throw new _common.UnauthorizedException({
|
|
147
|
+
message: `No account is linked to this ${provider} profile`,
|
|
148
|
+
messageKey: _config.SOCIAL_AUTH_MESSAGES.ACCOUNT_NOT_LINKED,
|
|
149
|
+
messageVariables: {
|
|
150
|
+
provider
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
if (this.dataSourceProvider.isCompanyFeatureEnabled()) {
|
|
155
|
+
throw new _common.UnauthorizedException({
|
|
156
|
+
message: `No account is linked to this ${provider} profile. Please sign up manually and join or create a company first, then social sign-in will work.`,
|
|
157
|
+
messageKey: _config.SOCIAL_AUTH_MESSAGES.COMPANY_REQUIRED,
|
|
158
|
+
messageVariables: {
|
|
159
|
+
provider
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return userRepository.save(userRepository.create({
|
|
164
|
+
email: profile.email,
|
|
165
|
+
name: profile.name,
|
|
166
|
+
password: null,
|
|
167
|
+
isActive: true,
|
|
168
|
+
emailVerified: true
|
|
169
|
+
}));
|
|
170
|
+
}
|
|
171
|
+
async getUsableConfig(provider) {
|
|
172
|
+
const repository = await this.dataSourceProvider.getRepository(_socialauthconfigentity.SocialAuthConfig);
|
|
173
|
+
const config = await repository.findOne({
|
|
174
|
+
where: {
|
|
175
|
+
provider,
|
|
176
|
+
deletedAt: (0, _typeorm.IsNull)()
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
if (!config?.isEnabled) {
|
|
180
|
+
throw new _common.BadRequestException({
|
|
181
|
+
message: `Social sign-in with ${provider} is not enabled`,
|
|
182
|
+
messageKey: _config.SOCIAL_AUTH_MESSAGES.PROVIDER_DISABLED,
|
|
183
|
+
messageVariables: {
|
|
184
|
+
provider
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (!config.clientId || !config.clientSecret) {
|
|
189
|
+
throw new _common.BadRequestException({
|
|
190
|
+
message: `Social sign-in with ${provider} is missing its client credentials`,
|
|
191
|
+
messageKey: _config.SOCIAL_AUTH_MESSAGES.PROVIDER_NOT_CONFIGURED,
|
|
192
|
+
messageVariables: {
|
|
193
|
+
provider
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return config;
|
|
198
|
+
}
|
|
199
|
+
toCredentials(config, redirectUri) {
|
|
200
|
+
return {
|
|
201
|
+
clientId: config.clientId,
|
|
202
|
+
clientSecret: config.clientSecret,
|
|
203
|
+
redirectUri,
|
|
204
|
+
scopes: config.scopes,
|
|
205
|
+
extraConfig: config.extraConfig
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async consumeState(state, redirectUri) {
|
|
209
|
+
const key = this.stateKey(state);
|
|
210
|
+
const stored = await this.getCache().get(key);
|
|
211
|
+
await this.getCache().del(key);
|
|
212
|
+
if (!stored) {
|
|
213
|
+
throw new _common.UnauthorizedException({
|
|
214
|
+
message: 'Social sign-in session is invalid or has expired',
|
|
215
|
+
messageKey: _config.SOCIAL_AUTH_MESSAGES.STATE_INVALID
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
if (stored.redirectUri !== redirectUri) {
|
|
219
|
+
throw new _common.UnauthorizedException({
|
|
220
|
+
message: 'Redirect URI does not match the one the sign-in started with',
|
|
221
|
+
messageKey: _config.SOCIAL_AUTH_MESSAGES.REDIRECT_URI_MISMATCH
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return stored.provider;
|
|
225
|
+
}
|
|
226
|
+
stateKey(state) {
|
|
227
|
+
return `${_config.SOCIAL_STATE_PREFIX}:${state}`;
|
|
228
|
+
}
|
|
229
|
+
getCache() {
|
|
230
|
+
if (!this.cacheManager) {
|
|
231
|
+
throw new _common.InternalServerErrorException({
|
|
232
|
+
message: 'Cache is required to hold the social sign-in state',
|
|
233
|
+
messageKey: _constants.SYSTEM_MESSAGES.SERVICE_NOT_AVAILABLE,
|
|
234
|
+
messageVariables: {
|
|
235
|
+
provider: 'CACHE_INSTANCE',
|
|
236
|
+
available: ''
|
|
237
|
+
}
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return this.cacheManager;
|
|
241
|
+
}
|
|
242
|
+
publishSocialEvent(entity, action, user, provider) {
|
|
243
|
+
void (0, _classes.publishDomainEvent)({
|
|
244
|
+
module: _config.AUTH_EVENT_MODULE,
|
|
245
|
+
entity,
|
|
246
|
+
action,
|
|
247
|
+
ids: [
|
|
248
|
+
user.id
|
|
249
|
+
],
|
|
250
|
+
metadata: {
|
|
251
|
+
email: user.email,
|
|
252
|
+
provider
|
|
253
|
+
},
|
|
254
|
+
user: {
|
|
255
|
+
id: user.id,
|
|
256
|
+
email: user.email
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
constructor(authService, dataSourceProvider, cacheManager){
|
|
261
|
+
_define_property(this, "authService", void 0);
|
|
262
|
+
_define_property(this, "dataSourceProvider", void 0);
|
|
263
|
+
_define_property(this, "cacheManager", void 0);
|
|
264
|
+
this.authService = authService;
|
|
265
|
+
this.dataSourceProvider = dataSourceProvider;
|
|
266
|
+
this.cacheManager = cacheManager;
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
_ts_decorate([
|
|
270
|
+
(0, _nestjsshared.LogAction)({
|
|
271
|
+
action: 'auth.social.authorize',
|
|
272
|
+
module: 'auth',
|
|
273
|
+
includeParams: false
|
|
274
|
+
}),
|
|
275
|
+
_ts_metadata("design:type", Function),
|
|
276
|
+
_ts_metadata("design:paramtypes", [
|
|
277
|
+
typeof _socialprovidertypeenum.SocialProviderTypeEnum === "undefined" ? Object : _socialprovidertypeenum.SocialProviderTypeEnum,
|
|
278
|
+
String
|
|
279
|
+
]),
|
|
280
|
+
_ts_metadata("design:returntype", Promise)
|
|
281
|
+
], SocialAuthService.prototype, "createAuthorization", null);
|
|
282
|
+
_ts_decorate([
|
|
283
|
+
(0, _nestjsshared.LogAction)({
|
|
284
|
+
action: 'auth.social.login',
|
|
285
|
+
module: 'auth',
|
|
286
|
+
includeParams: false
|
|
287
|
+
}),
|
|
288
|
+
_ts_metadata("design:type", Function),
|
|
289
|
+
_ts_metadata("design:paramtypes", [
|
|
290
|
+
String,
|
|
291
|
+
String,
|
|
292
|
+
String
|
|
293
|
+
]),
|
|
294
|
+
_ts_metadata("design:returntype", Promise)
|
|
295
|
+
], SocialAuthService.prototype, "completeLogin", null);
|
|
296
|
+
SocialAuthService = _ts_decorate([
|
|
297
|
+
(0, _common.Injectable)({
|
|
298
|
+
scope: _common.Scope.REQUEST
|
|
299
|
+
}),
|
|
300
|
+
_ts_param(0, (0, _common.Inject)(_authenticationservice.AuthenticationService)),
|
|
301
|
+
_ts_param(1, (0, _common.Inject)(_authdatasourceprovider.AuthDataSourceProvider)),
|
|
302
|
+
_ts_param(2, (0, _common.Optional)()),
|
|
303
|
+
_ts_param(2, (0, _common.Inject)('CACHE_INSTANCE')),
|
|
304
|
+
_ts_metadata("design:type", Function),
|
|
305
|
+
_ts_metadata("design:paramtypes", [
|
|
306
|
+
typeof _authenticationservice.AuthenticationService === "undefined" ? Object : _authenticationservice.AuthenticationService,
|
|
307
|
+
typeof _authdatasourceprovider.AuthDataSourceProvider === "undefined" ? Object : _authdatasourceprovider.AuthDataSourceProvider,
|
|
308
|
+
typeof _classes.HybridCache === "undefined" ? Object : _classes.HybridCache
|
|
309
|
+
])
|
|
310
|
+
], SocialAuthService);
|