Skip to content

Commit adf9929

Browse files
authored
Merge pull request #33 from CodeLog-Development/dev
Merge dev into main for v0.1.2
2 parents ec49489 + 8d90af7 commit adf9929

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+910
-365
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import { Module } from '@nestjs/common';
22
import { FirebaseService } from './firebase.service';
3+
import { UserModule } from '../user/user.module';
34

45
@Module({
6+
imports: [UserModule],
57
providers: [FirebaseService],
68
})
79
export class FirebaseModule { }

api/src/lib/firebase/firebase.service.ts

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,24 @@ import { Firestore, getFirestore } from 'firebase-admin/firestore';
33
import { credential } from 'firebase-admin';
44
import { ServiceAccount, initializeApp } from 'firebase-admin/app';
55
import { ConfigService } from '@nestjs/config';
6+
import { GatekeeperNotification } from './notification.interface';
7+
import {
8+
ConditionMessage,
9+
getMessaging,
10+
Messaging,
11+
TopicMessage,
12+
} from 'firebase-admin/messaging';
13+
import { UserService } from '../user/user.service';
614

715
@Injectable()
816
export class FirebaseService {
917
private readonly firestore?: Firestore;
10-
constructor(private configService: ConfigService) {
18+
private readonly messaging?: Messaging;
19+
20+
constructor(
21+
private configService: ConfigService,
22+
private userService: UserService,
23+
) {
1124
const serviceAccount =
1225
this.configService.get<ServiceAccount>('serviceAccount');
1326

@@ -20,6 +33,7 @@ export class FirebaseService {
2033
credential: credential.cert(serviceAccount),
2134
});
2235
this.firestore = getFirestore(app);
36+
this.messaging = getMessaging(app);
2337
} catch (e) {
2438
console.info('Firebase app already initialized');
2539
}
@@ -28,4 +42,25 @@ export class FirebaseService {
2842
getFirestore(): Firestore | undefined {
2943
return this.firestore;
3044
}
45+
46+
async sendNotification(
47+
notification: GatekeeperNotification,
48+
): Promise<boolean> {
49+
try {
50+
const tokens: string[] =
51+
await this.userService.getAllNotificationTokens();
52+
53+
await this.messaging?.sendEachForMulticast({
54+
tokens,
55+
notification: {
56+
title: notification.title,
57+
body: notification.message,
58+
},
59+
});
60+
61+
return true;
62+
} catch (_e) {
63+
return false;
64+
}
65+
}
3166
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface GatekeeperNotification {
2+
title: string;
3+
message: string;
4+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { ServerController } from './server.controller';
2+
import {
3+
StartInstanceRequest,
4+
StartServerResponse,
5+
StopServerResponse,
6+
} from './server.interface';
7+
import { ServerService, ServerStatus } from './server.service';
8+
import { Test } from '@nestjs/testing';
9+
10+
describe('ServerController', () => {
11+
let serverController: ServerController;
12+
let serverService: ServerService;
13+
14+
beforeEach(async () => {
15+
const moduleRef = await Test.createTestingModule({
16+
controllers: [ServerController],
17+
providers: [ServerService],
18+
}).compile();
19+
20+
serverService = moduleRef.get<ServerService>(ServerService);
21+
serverController = new ServerController(serverService);
22+
});
23+
24+
describe('getStatus', () => {
25+
it('should return the server status', async () => {
26+
const result: ServerStatus = {
27+
success: true,
28+
message: 'Lorem ipsum',
29+
running: [],
30+
};
31+
32+
jest
33+
.spyOn(serverService, 'getServerStatus')
34+
.mockImplementation(async () => {
35+
return result;
36+
});
37+
38+
expect(await serverController.getStatus()).toBe(result);
39+
});
40+
});
41+
42+
describe('startServer', () => {
43+
it('should start the server', async () => {
44+
const result: StartServerResponse = {
45+
success: true,
46+
message: 'Lorem ipsum',
47+
started: ['1234'],
48+
};
49+
50+
jest
51+
.spyOn(serverService, 'startInstance')
52+
.mockImplementation(async (id: string) => {
53+
return {
54+
success: true,
55+
message: 'Lorem ipsum',
56+
started: [id],
57+
};
58+
});
59+
60+
expect(
61+
await serverController.startServer({ instanceId: '1234' }),
62+
).toStrictEqual(result);
63+
});
64+
});
65+
66+
describe('stopServer', () => {
67+
it('should stop the server', async () => {
68+
const result: StopServerResponse = {
69+
success: true,
70+
message: 'Lorem ipsum',
71+
stopped: ['1234'],
72+
};
73+
74+
jest.spyOn(serverService, 'stopServer').mockImplementation(async () => {
75+
return result;
76+
});
77+
78+
expect(
79+
await serverController.stopServer({ instanceId: '1234' }),
80+
).toStrictEqual(result);
81+
});
82+
});
83+
});

api/src/lib/server/server.controller.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,14 +18,14 @@ export class ServerController {
1818

1919
@Post('start')
2020
async startServer(
21-
@Body() startRequest: StartInstanceRequest
21+
@Body() startRequest: StartInstanceRequest,
2222
): Promise<StartServerResponse> {
2323
return await this.serverService.startInstance(startRequest.instanceId);
2424
}
2525

2626
@Post('stop')
2727
async stopServer(
28-
@Body() stopRequest: StopServerRequest
28+
@Body() stopRequest: StopServerRequest,
2929
): Promise<StopServerResponse> {
3030
return await this.serverService.stopServer(stopRequest.instanceId);
3131
}

api/src/lib/server/server.service.ts

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Injectable, OnModuleInit } from '@nestjs/common';
22
import { Ec2Service } from '../aws/ec2/ec2.service';
33
import { ModuleRef } from '@nestjs/core';
44
import { StartServerResponse, StopServerResponse } from './server.interface';
5+
import { FirebaseService } from '../firebase/firebase.service';
56

67
export interface ServerStatus {
78
success: boolean;
@@ -12,10 +13,14 @@ export interface ServerStatus {
1213
@Injectable()
1314
export class ServerService implements OnModuleInit {
1415
private ec2Service?: Ec2Service;
16+
private firebaseService?: FirebaseService;
1517
constructor(private moduleRef: ModuleRef) { }
1618

1719
onModuleInit() {
1820
this.ec2Service = this.moduleRef.get(Ec2Service, { strict: false });
21+
this.firebaseService = this.moduleRef.get(FirebaseService, {
22+
strict: false,
23+
});
1924
}
2025

2126
async getServerStatus(): Promise<ServerStatus> {
@@ -42,33 +47,49 @@ export class ServerService implements OnModuleInit {
4247
}
4348

4449
async startInstance(id: string): Promise<StartServerResponse> {
45-
if (!this.ec2Service) {
50+
if (!this.ec2Service || !this.firebaseService) {
4651
return {
4752
success: false,
48-
message: "Couldn't communicate with EC2 service",
53+
message: "Couldn't communicate with required services",
4954
};
5055
}
5156

5257
try {
5358
await this.ec2Service.startInstance(id);
59+
await this.firebaseService.sendNotification({
60+
title: 'Server started',
61+
message: '✅ The Ultimate Reloaded server has been started',
62+
});
5463
return { success: true, message: 'Instance started', started: [id] };
5564
} catch (e) {
65+
console.warn(
66+
' 🚀 ~ server.service.ts → Caught an error trying to start the server',
67+
e,
68+
);
5669
return { success: false, message: 'Failed to start instance' };
5770
}
5871
}
5972

6073
async stopServer(id: string): Promise<StopServerResponse> {
61-
if (!this.ec2Service) {
74+
if (!this.ec2Service || !this.firebaseService) {
6275
return {
6376
success: false,
64-
message: "Could't communicate with EC2 service",
77+
message: "Could't communicate with required services",
6578
};
6679
}
6780

6881
try {
6982
await this.ec2Service.stopInstance(id);
83+
await this.firebaseService.sendNotification({
84+
title: 'Server stopped',
85+
message: '⛔ The Ultimate Reloaded server has been stopped',
86+
});
7087
return { success: true, message: 'Instance stopped', stopped: [id] };
7188
} catch (e) {
89+
console.warn(
90+
' 🚀 ~ server.service.ts → Caught an error trying to stop the server',
91+
e,
92+
);
7293
return { success: false, message: 'Failed to stop instance' };
7394
}
7495
}

api/src/lib/user/user.controller.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
import {
1212
AuthRequest,
1313
ChangePasswordRequest,
14+
ChangeTokenRequest,
1415
NewUser,
1516
UserInfoResponse,
1617
validatePassword,
@@ -45,6 +46,31 @@ export class UserController {
4546
};
4647
}
4748

49+
@Patch('/notificationToken')
50+
async setNotificationToken(
51+
@Body() input: ChangeTokenRequest,
52+
@Req() request: Request,
53+
): Promise<ApiResponse> {
54+
if (!request.user) {
55+
return { success: false, message: 'Could not find user information' };
56+
}
57+
58+
try {
59+
const ref = await this.userService.findUserByUsernameRef(
60+
request.user.username,
61+
);
62+
if (ref !== undefined) {
63+
await this.userService.setNotificationToken(ref, input.token);
64+
return { success: true, message: 'Notification token updated' };
65+
} else {
66+
return { success: false, message: 'Could not find user information' };
67+
}
68+
} catch (e) {
69+
console.error('Failed to update notification token', e);
70+
return { success: false, message: 'An unkown error occurred' };
71+
}
72+
}
73+
4874
@Patch('/password')
4975
async changePassword(
5076
@Body() input: ChangePasswordRequest,

api/src/lib/user/user.interface.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { DocumentReference } from 'firebase-admin/firestore';
22
import { ApiResponse } from '../api.interface';
33

4+
export interface ChangeTokenRequest {
5+
token: string;
6+
}
7+
48
export interface NewUser {
59
username: string;
610
email: string;
@@ -12,6 +16,7 @@ export interface User {
1216
email: string;
1317
passwordHash: string;
1418
verified: boolean;
19+
notificationToken?: string;
1520
}
1621

1722
export interface AuthRequest {

api/src/lib/user/user.module.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { AuthenticationMiddleware } from '../auth/auth.middleware';
1111
@Module({
1212
providers: [UserService],
1313
controllers: [UserController],
14+
exports: [UserService],
1415
})
1516
export class UserModule implements NestModule {
1617
configure(consumer: MiddlewareConsumer) {
@@ -23,6 +24,10 @@ export class UserModule implements NestModule {
2324
path: 'user/info',
2425
method: RequestMethod.GET,
2526
},
27+
{
28+
path: 'user/notificationToken',
29+
method: RequestMethod.PATCH,
30+
},
2631
);
2732
}
2833
}

api/src/lib/user/user.service.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@ import { FirebaseService } from '../firebase/firebase.service';
44
import * as argon2 from 'argon2';
55
import { ModuleRef } from '@nestjs/core';
66
import { randomBytes } from 'crypto';
7-
import { DocumentData, DocumentReference } from 'firebase-admin/firestore';
7+
import {
8+
CollectionReference,
9+
DocumentData,
10+
DocumentReference,
11+
} from 'firebase-admin/firestore';
812

913
@Injectable()
1014
export class UserService implements OnModuleInit {
@@ -16,6 +20,10 @@ export class UserService implements OnModuleInit {
1620
});
1721
}
1822

23+
async setNotificationToken(userRef: DocumentReference<User>, token: string) {
24+
await userRef.set({ notificationToken: token }, { merge: true });
25+
}
26+
1927
async changePassword(
2028
userRef: DocumentReference,
2129
newPassword: string,
@@ -182,4 +190,20 @@ export class UserService implements OnModuleInit {
182190

183191
return user;
184192
}
193+
194+
async getAllNotificationTokens(): Promise<string[]> {
195+
const firestore = this.firebaseService?.getFirestore();
196+
const collection = firestore?.collection('/users');
197+
const docs = await collection?.get();
198+
const tokens = [];
199+
200+
for (const d of docs?.docs || []) {
201+
const doc = d.data() as User;
202+
if (doc.notificationToken) {
203+
tokens.push(doc.notificationToken);
204+
}
205+
}
206+
207+
return tokens;
208+
}
185209
}

0 commit comments

Comments
 (0)