Skip to content

Commit 60efd8d

Browse files
committed
Merge branch 'main' into feature/user-profile
2 parents e4b3a80 + 0ee5639 commit 60efd8d

File tree

31 files changed

+449
-84
lines changed

31 files changed

+449
-84
lines changed

.git-blame-ignore-revs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
# Apply blank line linting rules
22
2bbe554f470c5b7a0d59927dd3b9783314a0d805
3+
4+
# Linting commits
5+
6efc953b9f7f648f2be59295a78ce1180f12b32d

NoteBlockWorld.code-workspace

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,19 @@
1818
}
1919
],
2020
"settings": {
21+
"editor.formatOnSave": true,
22+
"eslint.validate": ["typescript"],
23+
"eslint.run": "onType",
24+
"eslint.format.enable": true,
2125
"mdx.server.enable": true,
22-
"jest.disabledWorkspaceFolders": ["Root", "Frontend"]
26+
"editor.codeActionsOnSave": {
27+
"source.fixAll": "explicit"
28+
},
29+
"jest.disabledWorkspaceFolders": ["Root", "Frontend"],
30+
"search.exclude": {
31+
"**/.git": true,
32+
"**/node_modules": true,
33+
"**/dist": true,
34+
}
2335
}
2436
}

pnpm-lock.yaml

Lines changed: 4 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

server/src/auth/auth.module.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,6 @@ export class AuthModule {
6262
useFactory: (configService: ConfigService) =>
6363
configService.getOrThrow<string>('SERVER_URL'),
6464
},
65-
{
66-
inject: [ConfigService],
67-
provide: 'MAGIC_LINK_SECRET',
68-
useFactory: (configService: ConfigService) =>
69-
configService.getOrThrow<string>('MAGIC_LINK_SECRET'),
70-
},
7165
{
7266
inject: [ConfigService],
7367
provide: 'FRONTEND_URL',

server/src/user/dto/user.dto.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { User } from '../entity/user.entity';
2+
3+
export class UserDto {
4+
username: string;
5+
publicName: string;
6+
email: string;
7+
static fromEntity(user: User): UserDto {
8+
const userDto: UserDto = {
9+
username: user.username,
10+
publicName: user.publicName,
11+
email: user.email,
12+
};
13+
14+
return userDto;
15+
}
16+
}

server/src/user/entity/user.entity.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ export class User {
1919
lastEdited: Date;
2020

2121
@Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now })
22-
lastLogin: Date;
22+
lastSeen: Date;
23+
24+
@Prop({ type: Number, required: true, default: 0 })
25+
loginCount: number;
2326

2427
@Prop({ type: Number, required: true, default: 0 })
2528
loginStreak: number;
2629

2730
@Prop({ type: Number, required: true, default: 0 })
28-
loginCount: number;
31+
maxLoginStreak: number;
2932

3033
@Prop({ type: Number, required: true, default: 0 })
3134
playCount: number;

server/src/user/user.service.spec.ts

Lines changed: 162 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ describe('UserService', () => {
200200
describe('getSelfUserData', () => {
201201
it('should return self user data', async () => {
202202
const user = { _id: 'test-id' } as UserDocument;
203-
const userData = { ...user } as UserDocument;
203+
const userData = { ...user, lastSeen: new Date() } as UserDocument;
204204

205205
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
206206

@@ -219,6 +219,161 @@ describe('UserService', () => {
219219
new HttpException('user not found', HttpStatus.NOT_FOUND),
220220
);
221221
});
222+
223+
it('should update lastSeen and increment loginStreak if lastSeen is before today', async () => {
224+
const user = { _id: 'test-id' } as UserDocument;
225+
const yesterday = new Date();
226+
yesterday.setDate(yesterday.getDate() - 1);
227+
yesterday.setHours(0, 0, 0, 0);
228+
229+
const userData = {
230+
...user,
231+
lastSeen: yesterday,
232+
loginStreak: 1,
233+
save: jest.fn().mockResolvedValue(true),
234+
} as unknown as UserDocument;
235+
236+
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
237+
238+
const result = await service.getSelfUserData(user);
239+
240+
expect(result.lastSeen).toBeInstanceOf(Date);
241+
expect(result.loginStreak).toBe(2);
242+
expect(userData.save).toHaveBeenCalled();
243+
});
244+
245+
it('should not update lastSeen or increment loginStreak if lastSeen is today', async () => {
246+
const user = { _id: 'test-id' } as UserDocument;
247+
const today = new Date();
248+
today.setHours(0, 0, 0, 0);
249+
250+
const userData = {
251+
...user,
252+
lastSeen: today,
253+
loginStreak: 1,
254+
save: jest.fn().mockResolvedValue(true),
255+
} as unknown as UserDocument;
256+
257+
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
258+
259+
const result = await service.getSelfUserData(user);
260+
261+
expect(result.lastSeen).toEqual(today);
262+
expect(result.loginStreak).toBe(1);
263+
expect(userData.save).not.toHaveBeenCalled();
264+
});
265+
266+
it('should reset loginStreak if lastSeen is not yesterday', async () => {
267+
const user = { _id: 'test-id' } as UserDocument;
268+
const twoDaysAgo = new Date();
269+
twoDaysAgo.setDate(twoDaysAgo.getDate() - 2);
270+
twoDaysAgo.setHours(0, 0, 0, 0);
271+
272+
const userData = {
273+
...user,
274+
lastSeen: twoDaysAgo,
275+
loginStreak: 5,
276+
save: jest.fn().mockResolvedValue(true),
277+
} as unknown as UserDocument;
278+
279+
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
280+
281+
const result = await service.getSelfUserData(user);
282+
283+
expect(result.lastSeen).toBeInstanceOf(Date);
284+
expect(result.loginStreak).toBe(1);
285+
expect(userData.save).toHaveBeenCalled();
286+
});
287+
288+
it('should increment loginCount if lastSeen is not today', async () => {
289+
const user = { _id: 'test-id' } as UserDocument;
290+
const yesterday = new Date();
291+
yesterday.setDate(yesterday.getDate() - 1);
292+
yesterday.setHours(0, 0, 0, 0);
293+
294+
const userData = {
295+
...user,
296+
lastSeen: yesterday,
297+
loginCount: 5,
298+
save: jest.fn().mockResolvedValue(true),
299+
} as unknown as UserDocument;
300+
301+
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
302+
303+
const result = await service.getSelfUserData(user);
304+
305+
expect(result.lastSeen).toBeInstanceOf(Date);
306+
expect(result.loginCount).toBe(6);
307+
expect(userData.save).toHaveBeenCalled();
308+
});
309+
310+
it('should not increment loginCount if lastSeen is today', async () => {
311+
const user = { _id: 'test-id' } as UserDocument;
312+
313+
const today = new Date();
314+
today.setHours(0, 0, 0, 0);
315+
316+
const userData = {
317+
...user,
318+
lastSeen: today,
319+
loginCount: 5,
320+
save: jest.fn().mockResolvedValue(true),
321+
} as unknown as UserDocument;
322+
323+
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
324+
325+
const result = await service.getSelfUserData(user);
326+
327+
expect(result.lastSeen).toEqual(today);
328+
expect(result.loginCount).toBe(5);
329+
expect(userData.save).not.toHaveBeenCalled();
330+
});
331+
332+
it('should increment maxLoginStreak if login streak exceeds max', async () => {
333+
const user = { _id: 'test-id' } as UserDocument;
334+
335+
const yesterday = new Date();
336+
yesterday.setDate(yesterday.getDate() - 1);
337+
yesterday.setHours(0, 0, 0, 0);
338+
339+
const userData = {
340+
...user,
341+
lastSeen: yesterday,
342+
loginStreak: 8,
343+
maxLoginStreak: 8,
344+
save: jest.fn().mockResolvedValue(true),
345+
} as unknown as UserDocument;
346+
347+
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
348+
349+
const result = await service.getSelfUserData(user);
350+
351+
expect(result.maxLoginStreak).toBe(9);
352+
expect(userData.save).toHaveBeenCalled();
353+
});
354+
355+
it('should not increment maxLoginStreak if login streak is less than the max', async () => {
356+
const user = { _id: 'test-id' } as UserDocument;
357+
358+
const yesterday = new Date();
359+
yesterday.setDate(yesterday.getDate() - 1);
360+
yesterday.setHours(0, 0, 0, 0);
361+
362+
const userData = {
363+
...user,
364+
lastSeen: yesterday,
365+
loginStreak: 4,
366+
maxLoginStreak: 8,
367+
save: jest.fn().mockResolvedValue(true),
368+
} as unknown as UserDocument;
369+
370+
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
371+
372+
const result = await service.getSelfUserData(user);
373+
374+
expect(result.maxLoginStreak).toBe(8);
375+
expect(userData.save).toHaveBeenCalled();
376+
});
222377
});
223378

224379
describe('usernameExists', () => {
@@ -341,7 +496,12 @@ describe('UserService', () => {
341496

342497
const result = await service.updateUsername(user, body);
343498

344-
expect(result).toEqual(user);
499+
expect(result).toEqual({
500+
username: 'newuser',
501+
publicName: undefined,
502+
email: undefined,
503+
});
504+
345505
expect(user.username).toBe(body.username);
346506
expect(service.usernameExists).toHaveBeenCalledWith(body.username);
347507
});

server/src/user/user.service.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { UpdateUserProfileDto } from '@shared/validation/user/dto/UpdateUserProf
99
import { validate } from 'class-validator';
1010
import { Model } from 'mongoose';
1111

12+
import { UserDto } from './dto/user.dto';
1213
import { User, UserDocument } from './entity/user.entity';
1314

1415
@Injectable()
@@ -165,11 +166,36 @@ export class UserService {
165166
}
166167

167168
public async getSelfUserData(user: UserDocument) {
168-
const usedData = await this.findByID(user._id.toString());
169-
if (!usedData)
169+
const userData = await this.findByID(user._id.toString());
170+
if (!userData)
170171
throw new HttpException('user not found', HttpStatus.NOT_FOUND);
171172

172-
return usedData;
173+
const today = new Date();
174+
today.setHours(0, 0, 0, 0); // Set the time to the start of the day
175+
176+
const lastSeenDate = new Date(userData.lastSeen);
177+
lastSeenDate.setHours(0, 0, 0, 0); // Set the time to the start of the day
178+
179+
if (lastSeenDate < today) {
180+
userData.lastSeen = new Date();
181+
182+
// if the last seen date is not yesterday, reset the login streak
183+
const yesterday = new Date(today);
184+
yesterday.setDate(today.getDate() - 1);
185+
186+
if (lastSeenDate < yesterday) userData.loginStreak = 1;
187+
else {
188+
userData.loginStreak += 1;
189+
if (userData.loginStreak > userData.maxLoginStreak)
190+
userData.maxLoginStreak = userData.loginStreak;
191+
}
192+
193+
userData.loginCount++;
194+
195+
userData.save(); // no need to await this, we already have the data to send back
196+
} // if equal or greater, do nothing about the login streak
197+
198+
return userData;
173199
}
174200

175201
public async usernameExists(username: string) {
@@ -220,8 +246,11 @@ export class UserService {
220246
}
221247

222248
user.username = username;
249+
user.lastEdited = new Date();
223250

224-
return await user.save();
251+
await user.save();
252+
253+
return UserDto.fromEntity(user);
225254
}
226255

227256
public async updateProfile(user: UserDocument, body: UpdateUserProfileDto) {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { deepFreeze } from '../common/deepFreeze';
2+
3+
export const UserConst = deepFreeze({
4+
USERNAME_MIN_LENGTH: 3,
5+
USERNAME_MAX_LENGTH: 32,
6+
ALLOWED_REGEXP: /^[a-zA-Z0-9-_.]*$/,
7+
});

0 commit comments

Comments
 (0)