Skip to content

Commit 9491151

Browse files
committed
Merge branch 'develop' of https://github.com/OpenNBS/NoteBlockWorld into feature/login-by-email
2 parents bbfd4b1 + 0ee5639 commit 9491151

File tree

30 files changed

+439
-78
lines changed

30 files changed

+439
-78
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
@@ -66,12 +66,6 @@ export class AuthModule {
6666
useFactory: (configService: ConfigService) =>
6767
configService.getOrThrow<string>('SERVER_URL'),
6868
},
69-
{
70-
inject: [ConfigService],
71-
provide: 'MAGIC_LINK_SECRET',
72-
useFactory: (configService: ConfigService) =>
73-
configService.getOrThrow<string>('MAGIC_LINK_SECRET'),
74-
},
7569
{
7670
inject: [ConfigService],
7771
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
@@ -39,13 +39,16 @@ export class User {
3939
lastEdited: Date;
4040

4141
@Prop({ type: MongooseSchema.Types.Date, required: true, default: Date.now })
42-
lastLogin: Date;
42+
lastSeen: Date;
43+
44+
@Prop({ type: Number, required: true, default: 0 })
45+
loginCount: number;
4346

4447
@Prop({ type: Number, required: true, default: 0 })
4548
loginStreak: number;
4649

4750
@Prop({ type: Number, required: true, default: 0 })
48-
loginCount: number;
51+
maxLoginStreak: number;
4952

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

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

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

203203
jest.spyOn(service, 'findByID').mockResolvedValue(userData);
204204

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

222377
describe('usernameExists', () => {
@@ -339,7 +494,12 @@ describe('UserService', () => {
339494

340495
const result = await service.updateUsername(user, body);
341496

342-
expect(result).toEqual(user);
497+
expect(result).toEqual({
498+
username: 'newuser',
499+
publicName: undefined,
500+
email: undefined,
501+
});
502+
343503
expect(user.username).toBe(body.username);
344504
expect(service.usernameExists).toHaveBeenCalledWith(body.username);
345505
});

server/src/user/user.service.ts

Lines changed: 33 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { UpdateUsernameDto } from '@shared/validation/user/dto/UpdateUsername.dt
77
import { validate } from 'class-validator';
88
import { Model } from 'mongoose';
99

10+
import { UserDto } from './dto/user.dto';
1011
import { User, UserDocument } from './entity/user.entity';
1112

1213
@Injectable()
@@ -139,11 +140,36 @@ export class UserService {
139140
}
140141

141142
public async getSelfUserData(user: UserDocument) {
142-
const usedData = await this.findByID(user._id.toString());
143-
if (!usedData)
143+
const userData = await this.findByID(user._id.toString());
144+
if (!userData)
144145
throw new HttpException('user not found', HttpStatus.NOT_FOUND);
145146

146-
return usedData;
147+
const today = new Date();
148+
today.setHours(0, 0, 0, 0); // Set the time to the start of the day
149+
150+
const lastSeenDate = new Date(userData.lastSeen);
151+
lastSeenDate.setHours(0, 0, 0, 0); // Set the time to the start of the day
152+
153+
if (lastSeenDate < today) {
154+
userData.lastSeen = new Date();
155+
156+
// if the last seen date is not yesterday, reset the login streak
157+
const yesterday = new Date(today);
158+
yesterday.setDate(today.getDate() - 1);
159+
160+
if (lastSeenDate < yesterday) userData.loginStreak = 1;
161+
else {
162+
userData.loginStreak += 1;
163+
if (userData.loginStreak > userData.maxLoginStreak)
164+
userData.maxLoginStreak = userData.loginStreak;
165+
}
166+
167+
userData.loginCount++;
168+
169+
userData.save(); // no need to await this, we already have the data to send back
170+
} // if equal or greater, do nothing about the login streak
171+
172+
return userData;
147173
}
148174

149175
public async usernameExists(username: string) {
@@ -194,7 +220,10 @@ export class UserService {
194220
}
195221

196222
user.username = username;
223+
user.lastEdited = new Date();
197224

198-
return await user.save();
225+
await user.save();
226+
227+
return UserDto.fromEntity(user);
199228
}
200229
}
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)