HSB-445 feature: storing user last login timestamp (#4074)
* feat: lastLoggedOn added in schema and service function * feat: add lastLoggedOn logic for magic link * test: update test cases * feat: add lastLoggedOn in gql model * fix: nullable allowed in model attribute * fix: resolve feedback * feat: user last login interceptor added
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "User" ADD COLUMN "lastLoggedOn" TIMESTAMP(3);
|
||||||
@@ -41,31 +41,31 @@ model TeamInvitation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model TeamCollection {
|
model TeamCollection {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
parentID String?
|
parentID String?
|
||||||
data Json?
|
data Json?
|
||||||
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
|
parent TeamCollection? @relation("TeamCollectionChildParent", fields: [parentID], references: [id])
|
||||||
children TeamCollection[] @relation("TeamCollectionChildParent")
|
children TeamCollection[] @relation("TeamCollectionChildParent")
|
||||||
requests TeamRequest[]
|
requests TeamRequest[]
|
||||||
teamID String
|
teamID String
|
||||||
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
||||||
title String
|
title String
|
||||||
orderIndex Int
|
orderIndex Int
|
||||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||||
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
model TeamRequest {
|
model TeamRequest {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
collectionID String
|
collectionID String
|
||||||
collection TeamCollection @relation(fields: [collectionID], references: [id], onDelete: Cascade)
|
collection TeamCollection @relation(fields: [collectionID], references: [id], onDelete: Cascade)
|
||||||
teamID String
|
teamID String
|
||||||
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
team Team @relation(fields: [teamID], references: [id], onDelete: Cascade)
|
||||||
title String
|
title String
|
||||||
request Json
|
request Json
|
||||||
orderIndex Int
|
orderIndex Int
|
||||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||||
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
updatedOn DateTime @updatedAt @db.Timestamp(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
model Shortcode {
|
model Shortcode {
|
||||||
@@ -104,6 +104,7 @@ model User {
|
|||||||
userRequests UserRequest[]
|
userRequests UserRequest[]
|
||||||
currentRESTSession Json?
|
currentRESTSession Json?
|
||||||
currentGQLSession Json?
|
currentGQLSession Json?
|
||||||
|
lastLoggedOn DateTime?
|
||||||
createdOn DateTime @default(now()) @db.Timestamp(3)
|
createdOn DateTime @default(now()) @db.Timestamp(3)
|
||||||
invitedUsers InvitedUsers[]
|
invitedUsers InvitedUsers[]
|
||||||
shortcodes Shortcode[]
|
shortcodes Shortcode[]
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ const dbAdminUsers: DbUser[] = [
|
|||||||
refreshToken: 'refreshToken',
|
refreshToken: 'refreshToken',
|
||||||
currentRESTSession: '',
|
currentRESTSession: '',
|
||||||
currentGQLSession: '',
|
currentGQLSession: '',
|
||||||
|
lastLoggedOn: new Date(),
|
||||||
createdOn: new Date(),
|
createdOn: new Date(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -85,20 +86,10 @@ const dbAdminUsers: DbUser[] = [
|
|||||||
refreshToken: 'refreshToken',
|
refreshToken: 'refreshToken',
|
||||||
currentRESTSession: '',
|
currentRESTSession: '',
|
||||||
currentGQLSession: '',
|
currentGQLSession: '',
|
||||||
|
lastLoggedOn: new Date(),
|
||||||
createdOn: new Date(),
|
createdOn: new Date(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
const dbNonAminUser: DbUser = {
|
|
||||||
uid: 'uid 3',
|
|
||||||
displayName: 'displayName',
|
|
||||||
email: 'email@email.com',
|
|
||||||
photoURL: 'photoURL',
|
|
||||||
isAdmin: false,
|
|
||||||
refreshToken: 'refreshToken',
|
|
||||||
currentRESTSession: '',
|
|
||||||
currentGQLSession: '',
|
|
||||||
createdOn: new Date(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('AdminService', () => {
|
describe('AdminService', () => {
|
||||||
describe('fetchInvitedUsers', () => {
|
describe('fetchInvitedUsers', () => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Request,
|
Request,
|
||||||
Res,
|
Res,
|
||||||
UseGuards,
|
UseGuards,
|
||||||
|
UseInterceptors,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { SignInMagicDto } from './dto/signin-magic.dto';
|
import { SignInMagicDto } from './dto/signin-magic.dto';
|
||||||
@@ -27,6 +28,7 @@ import { SkipThrottle } from '@nestjs/throttler';
|
|||||||
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
import { AUTH_PROVIDER_NOT_SPECIFIED } from 'src/errors';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { throwHTTPErr } from 'src/utils';
|
import { throwHTTPErr } from 'src/utils';
|
||||||
|
import { UserLastLoginInterceptor } from 'src/interceptors/user-last-login.interceptor';
|
||||||
|
|
||||||
@UseGuards(ThrottlerBehindProxyGuard)
|
@UseGuards(ThrottlerBehindProxyGuard)
|
||||||
@Controller({ path: 'auth', version: '1' })
|
@Controller({ path: 'auth', version: '1' })
|
||||||
@@ -110,6 +112,7 @@ export class AuthController {
|
|||||||
@Get('google/callback')
|
@Get('google/callback')
|
||||||
@SkipThrottle()
|
@SkipThrottle()
|
||||||
@UseGuards(GoogleSSOGuard)
|
@UseGuards(GoogleSSOGuard)
|
||||||
|
@UseInterceptors(UserLastLoginInterceptor)
|
||||||
async googleAuthRedirect(@Request() req, @Res() res) {
|
async googleAuthRedirect(@Request() req, @Res() res) {
|
||||||
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
|
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
|
||||||
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
|
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
|
||||||
@@ -135,6 +138,7 @@ export class AuthController {
|
|||||||
@Get('github/callback')
|
@Get('github/callback')
|
||||||
@SkipThrottle()
|
@SkipThrottle()
|
||||||
@UseGuards(GithubSSOGuard)
|
@UseGuards(GithubSSOGuard)
|
||||||
|
@UseInterceptors(UserLastLoginInterceptor)
|
||||||
async githubAuthRedirect(@Request() req, @Res() res) {
|
async githubAuthRedirect(@Request() req, @Res() res) {
|
||||||
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
|
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
|
||||||
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
|
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
|
||||||
@@ -160,6 +164,7 @@ export class AuthController {
|
|||||||
@Get('microsoft/callback')
|
@Get('microsoft/callback')
|
||||||
@SkipThrottle()
|
@SkipThrottle()
|
||||||
@UseGuards(MicrosoftSSOGuard)
|
@UseGuards(MicrosoftSSOGuard)
|
||||||
|
@UseInterceptors(UserLastLoginInterceptor)
|
||||||
async microsoftAuthRedirect(@Request() req, @Res() res) {
|
async microsoftAuthRedirect(@Request() req, @Res() res) {
|
||||||
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
|
const authTokens = await this.authService.generateAuthTokens(req.user.uid);
|
||||||
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
|
if (E.isLeft(authTokens)) throwHTTPErr(authTokens.left);
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ const user: AuthUser = {
|
|||||||
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
@@ -172,9 +173,11 @@ describe('verifyMagicLinkTokens', () => {
|
|||||||
// generateAuthTokens
|
// generateAuthTokens
|
||||||
mockJWT.sign.mockReturnValue(user.refreshToken);
|
mockJWT.sign.mockReturnValue(user.refreshToken);
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
||||||
// deletePasswordlessVerificationToken
|
// deletePasswordlessVerificationToken
|
||||||
mockPrisma.verificationToken.delete.mockResolvedValueOnce(passwordlessData);
|
mockPrisma.verificationToken.delete.mockResolvedValueOnce(passwordlessData);
|
||||||
|
// usersService.updateUserLastLoggedOn
|
||||||
|
mockUser.updateUserLastLoggedOn.mockResolvedValue(E.right(true));
|
||||||
|
|
||||||
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
|
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight({
|
||||||
@@ -197,9 +200,11 @@ describe('verifyMagicLinkTokens', () => {
|
|||||||
// generateAuthTokens
|
// generateAuthTokens
|
||||||
mockJWT.sign.mockReturnValue(user.refreshToken);
|
mockJWT.sign.mockReturnValue(user.refreshToken);
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
||||||
// deletePasswordlessVerificationToken
|
// deletePasswordlessVerificationToken
|
||||||
mockPrisma.verificationToken.delete.mockResolvedValueOnce(passwordlessData);
|
mockPrisma.verificationToken.delete.mockResolvedValueOnce(passwordlessData);
|
||||||
|
// usersService.updateUserLastLoggedOn
|
||||||
|
mockUser.updateUserLastLoggedOn.mockResolvedValue(E.right(true));
|
||||||
|
|
||||||
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
|
const result = await authService.verifyMagicLinkTokens(magicLinkVerify);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight({
|
||||||
@@ -239,7 +244,7 @@ describe('verifyMagicLinkTokens', () => {
|
|||||||
// generateAuthTokens
|
// generateAuthTokens
|
||||||
mockJWT.sign.mockReturnValue(user.refreshToken);
|
mockJWT.sign.mockReturnValue(user.refreshToken);
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(
|
||||||
E.left(USER_NOT_FOUND),
|
E.left(USER_NOT_FOUND),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -264,7 +269,7 @@ describe('verifyMagicLinkTokens', () => {
|
|||||||
// generateAuthTokens
|
// generateAuthTokens
|
||||||
mockJWT.sign.mockReturnValue(user.refreshToken);
|
mockJWT.sign.mockReturnValue(user.refreshToken);
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
||||||
// deletePasswordlessVerificationToken
|
// deletePasswordlessVerificationToken
|
||||||
mockPrisma.verificationToken.delete.mockRejectedValueOnce('RecordNotFound');
|
mockPrisma.verificationToken.delete.mockRejectedValueOnce('RecordNotFound');
|
||||||
|
|
||||||
@@ -280,7 +285,7 @@ describe('generateAuthTokens', () => {
|
|||||||
test('Should successfully generate tokens with valid inputs', async () => {
|
test('Should successfully generate tokens with valid inputs', async () => {
|
||||||
mockJWT.sign.mockReturnValue(user.refreshToken);
|
mockJWT.sign.mockReturnValue(user.refreshToken);
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(E.right(user));
|
||||||
|
|
||||||
const result = await authService.generateAuthTokens(user.uid);
|
const result = await authService.generateAuthTokens(user.uid);
|
||||||
expect(result).toEqualRight({
|
expect(result).toEqualRight({
|
||||||
@@ -292,7 +297,7 @@ describe('generateAuthTokens', () => {
|
|||||||
test('Should throw USER_NOT_FOUND when updating refresh tokens fails', async () => {
|
test('Should throw USER_NOT_FOUND when updating refresh tokens fails', async () => {
|
||||||
mockJWT.sign.mockReturnValue(user.refreshToken);
|
mockJWT.sign.mockReturnValue(user.refreshToken);
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(
|
||||||
E.left(USER_NOT_FOUND),
|
E.left(USER_NOT_FOUND),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -319,7 +324,7 @@ describe('refreshAuthTokens', () => {
|
|||||||
// generateAuthTokens
|
// generateAuthTokens
|
||||||
mockJWT.sign.mockReturnValue(user.refreshToken);
|
mockJWT.sign.mockReturnValue(user.refreshToken);
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(
|
||||||
E.left(USER_NOT_FOUND),
|
E.left(USER_NOT_FOUND),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -348,7 +353,7 @@ describe('refreshAuthTokens', () => {
|
|||||||
// generateAuthTokens
|
// generateAuthTokens
|
||||||
mockJWT.sign.mockReturnValue('sdhjcbjsdhcbshjdcb');
|
mockJWT.sign.mockReturnValue('sdhjcbjsdhcbshjdcb');
|
||||||
// UpdateUserRefreshToken
|
// UpdateUserRefreshToken
|
||||||
mockUser.UpdateUserRefreshToken.mockResolvedValueOnce(
|
mockUser.updateUserRefreshToken.mockResolvedValueOnce(
|
||||||
E.right({
|
E.right({
|
||||||
...user,
|
...user,
|
||||||
refreshToken: 'sdhjcbjsdhcbshjdcb',
|
refreshToken: 'sdhjcbjsdhcbshjdcb',
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ export class AuthService {
|
|||||||
|
|
||||||
const refreshTokenHash = await argon2.hash(refreshToken);
|
const refreshTokenHash = await argon2.hash(refreshToken);
|
||||||
|
|
||||||
const updatedUser = await this.usersService.UpdateUserRefreshToken(
|
const updatedUser = await this.usersService.updateUserRefreshToken(
|
||||||
refreshTokenHash,
|
refreshTokenHash,
|
||||||
userUid,
|
userUid,
|
||||||
);
|
);
|
||||||
@@ -320,6 +320,8 @@ export class AuthService {
|
|||||||
statusCode: HttpStatus.NOT_FOUND,
|
statusCode: HttpStatus.NOT_FOUND,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.usersService.updateUserLastLoggedOn(passwordlessTokens.value.userUid);
|
||||||
|
|
||||||
return E.right(tokens.right);
|
return E.right(tokens.right);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import {
|
||||||
|
Injectable,
|
||||||
|
NestInterceptor,
|
||||||
|
ExecutionContext,
|
||||||
|
CallHandler,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { tap } from 'rxjs/operators';
|
||||||
|
import { AuthUser } from 'src/types/AuthUser';
|
||||||
|
import { UserService } from 'src/user/user.service';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class UserLastLoginInterceptor implements NestInterceptor {
|
||||||
|
constructor(private userService: UserService) {}
|
||||||
|
|
||||||
|
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
|
||||||
|
const user: AuthUser = context.switchToHttp().getRequest().user;
|
||||||
|
|
||||||
|
const now = Date.now();
|
||||||
|
return next.handle().pipe(
|
||||||
|
tap(() => {
|
||||||
|
this.userService.updateUserLastLoggedOn(user.uid);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,6 +48,7 @@ const user: AuthUser = {
|
|||||||
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: createdOn,
|
||||||
createdOn: createdOn,
|
createdOn: createdOn,
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ const user: AuthUser = {
|
|||||||
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ const user: AuthUser = {
|
|||||||
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
photoURL: 'https://en.wikipedia.org/wiki/Dwight_Schrute',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ const user: AuthUser = {
|
|||||||
photoURL: 'https://example.com/photo.png',
|
photoURL: 'https://example.com/photo.png',
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
refreshToken: null,
|
refreshToken: null,
|
||||||
|
lastLoggedOn: new Date(),
|
||||||
createdOn: new Date(),
|
createdOn: new Date(),
|
||||||
currentGQLSession: null,
|
currentGQLSession: null,
|
||||||
currentRESTSession: null,
|
currentRESTSession: null,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const user: AuthUser = {
|
|||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -30,6 +30,12 @@ export class User {
|
|||||||
})
|
})
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
|
||||||
|
@Field({
|
||||||
|
nullable: true,
|
||||||
|
description: 'Date when the user last logged in',
|
||||||
|
})
|
||||||
|
lastLoggedOn: Date;
|
||||||
|
|
||||||
@Field({
|
@Field({
|
||||||
description: 'Date when the user account was created',
|
description: 'Date when the user account was created',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ const user: AuthUser = {
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ const adminUser: AuthUser = {
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -67,6 +69,7 @@ const users: AuthUser[] = [
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -78,6 +81,7 @@ const users: AuthUser[] = [
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -89,6 +93,7 @@ const users: AuthUser[] = [
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -103,6 +108,7 @@ const adminUsers: AuthUser[] = [
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -114,6 +120,7 @@ const adminUsers: AuthUser[] = [
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -125,6 +132,7 @@ const adminUsers: AuthUser[] = [
|
|||||||
currentRESTSession: {},
|
currentRESTSession: {},
|
||||||
currentGQLSession: {},
|
currentGQLSession: {},
|
||||||
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
refreshToken: 'hbfvdkhjbvkdvdfjvbnkhjb',
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
createdOn: currentTime,
|
createdOn: currentTime,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -495,6 +503,26 @@ describe('UserService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('updateUserLastLoggedOn', () => {
|
||||||
|
test('should resolve right and update user last logged on', async () => {
|
||||||
|
const currentTime = new Date();
|
||||||
|
mockPrisma.user.update.mockResolvedValueOnce({
|
||||||
|
...user,
|
||||||
|
lastLoggedOn: currentTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await userService.updateUserLastLoggedOn(user.uid);
|
||||||
|
expect(result).toEqualRight(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should resolve left and error when invalid user uid is passed', async () => {
|
||||||
|
mockPrisma.user.update.mockRejectedValueOnce('NotFoundError');
|
||||||
|
|
||||||
|
const result = await userService.updateUserLastLoggedOn('invalidUserUid');
|
||||||
|
expect(result).toEqualLeft(USER_NOT_FOUND);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('fetchAllUsers', () => {
|
describe('fetchAllUsers', () => {
|
||||||
test('should resolve right and return 20 users when cursor is null', async () => {
|
test('should resolve right and return 20 users when cursor is null', async () => {
|
||||||
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
mockPrisma.user.findMany.mockResolvedValueOnce(users);
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export class UserService {
|
|||||||
* @param userUid User uid
|
* @param userUid User uid
|
||||||
* @returns Either of User with updated refreshToken
|
* @returns Either of User with updated refreshToken
|
||||||
*/
|
*/
|
||||||
async UpdateUserRefreshToken(refreshTokenHash: string, userUid: string) {
|
async updateUserRefreshToken(refreshTokenHash: string, userUid: string) {
|
||||||
try {
|
try {
|
||||||
const user = await this.prisma.user.update({
|
const user = await this.prisma.user.update({
|
||||||
where: {
|
where: {
|
||||||
@@ -174,6 +174,7 @@ export class UserService {
|
|||||||
displayName: userDisplayName,
|
displayName: userDisplayName,
|
||||||
email: profile.emails[0].value,
|
email: profile.emails[0].value,
|
||||||
photoURL: userPhotoURL,
|
photoURL: userPhotoURL,
|
||||||
|
lastLoggedOn: new Date(),
|
||||||
providerAccounts: {
|
providerAccounts: {
|
||||||
create: {
|
create: {
|
||||||
provider: profile.provider,
|
provider: profile.provider,
|
||||||
@@ -221,7 +222,7 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update User displayName and photoURL
|
* Update User displayName and photoURL when logged in via a SSO provider
|
||||||
*
|
*
|
||||||
* @param user User object
|
* @param user User object
|
||||||
* @param profile Data received from SSO provider on the users account
|
* @param profile Data received from SSO provider on the users account
|
||||||
@@ -236,6 +237,7 @@ export class UserService {
|
|||||||
data: {
|
data: {
|
||||||
displayName: !profile.displayName ? null : profile.displayName,
|
displayName: !profile.displayName ? null : profile.displayName,
|
||||||
photoURL: !profile.photos ? null : profile.photos[0].value,
|
photoURL: !profile.photos ? null : profile.photos[0].value,
|
||||||
|
lastLoggedOn: new Date(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return E.right(updatedUser);
|
return E.right(updatedUser);
|
||||||
@@ -289,7 +291,7 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update a user's data
|
* Update a user's displayName
|
||||||
* @param userUID User UID
|
* @param userUID User UID
|
||||||
* @param displayName User's displayName
|
* @param displayName User's displayName
|
||||||
* @returns a Either of User or error
|
* @returns a Either of User or error
|
||||||
@@ -316,6 +318,22 @@ export class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update user's lastLoggedOn timestamp
|
||||||
|
* @param userUID User UID
|
||||||
|
*/
|
||||||
|
async updateUserLastLoggedOn(userUid: string) {
|
||||||
|
try {
|
||||||
|
await this.prisma.user.update({
|
||||||
|
where: { uid: userUid },
|
||||||
|
data: { lastLoggedOn: new Date() },
|
||||||
|
});
|
||||||
|
return E.right(true);
|
||||||
|
} catch (e) {
|
||||||
|
return E.left(USER_NOT_FOUND);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Validate and parse currentRESTSession and currentGQLSession
|
* Validate and parse currentRESTSession and currentGQLSession
|
||||||
* @param sessionData string of the session
|
* @param sessionData string of the session
|
||||||
|
|||||||
Reference in New Issue
Block a user