From c866d0acec507e4b7f3f64f1af8759ec1fd86d0d Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 01:22:13 +0300 Subject: [PATCH 01/46] chore(api): update jwt expiration --- api/.env.example | 1 + api/src/auth/auth.module.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/api/.env.example b/api/.env.example index 326a054..0e25293 100644 --- a/api/.env.example +++ b/api/.env.example @@ -1,6 +1,7 @@ PORT= MONGO_URI= JWT_SECRET=secret +JWT_EXPIRATION=60d FIREBASE_PROJECT_ID= FIREBASE_PRIVATE_KEY_ID= diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts index 076e40d..1d170f0 100644 --- a/api/src/auth/auth.module.ts +++ b/api/src/auth/auth.module.ts @@ -29,7 +29,7 @@ import { PassportModule, JwtModule.register({ secret: process.env.JWT_SECRET, - signOptions: { expiresIn: '180d' }, + signOptions: { expiresIn: process.env.JWT_EXPIRATION || '60d' }, }), MailModule, ], From aa8201eaef629fb14890773616146bda82820d1a Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 01:26:26 +0300 Subject: [PATCH 02/46] chore(web): check for token expiration on launch and redirect to login --- web/components/Navbar.tsx | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/web/components/Navbar.tsx b/web/components/Navbar.tsx index e64dfd8..ef4be3b 100644 --- a/web/components/Navbar.tsx +++ b/web/components/Navbar.tsx @@ -19,12 +19,30 @@ import Router from 'next/router' import { useDispatch, useSelector } from 'react-redux' import { logout, selectAuthUser } from '../store/authSlice' import Image from 'next/image' +import { useEffect } from 'react' +import { authService } from '../services/authService' export default function Navbar() { const dispatch = useDispatch() const { colorMode, toggleColorMode } = useColorMode() const authUser = useSelector(selectAuthUser) + useEffect(() => { + const timout = setTimeout(async () => { + if (authUser) { + authService + .whoAmI() + .catch((e) => { + if (e.response.status === 401) { + dispatch(logout()) + } + }) + .then((res) => {}) + } + }, 5000) + return () => clearTimeout(timout) + }, [authUser, dispatch]) + return ( <> Date: Mon, 15 Apr 2024 01:27:02 +0300 Subject: [PATCH 03/46] chore(web): update meta tags --- web/components/meta/Meta.tsx | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/web/components/meta/Meta.tsx b/web/components/meta/Meta.tsx index 6c90993..e37fe20 100644 --- a/web/components/meta/Meta.tsx +++ b/web/components/meta/Meta.tsx @@ -5,9 +5,19 @@ export default function Meta() { TextBee - SMS Gateway - - - + + + ) From 838dcf42488c50a31be0225f1a6049d1cbcb2d5c Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 01:35:24 +0300 Subject: [PATCH 04/46] feat(api): track apiKey usage --- api/src/auth/auth.service.ts | 14 ++++++++++++++ api/src/auth/guards/auth.guard.ts | 3 +++ api/src/auth/schemas/api-key.schema.ts | 6 ++++++ 3 files changed, 23 insertions(+) diff --git a/api/src/auth/auth.service.ts b/api/src/auth/auth.service.ts index c6e4b27..612dfac 100644 --- a/api/src/auth/auth.service.ts +++ b/api/src/auth/auth.service.ts @@ -196,4 +196,18 @@ export class AuthService { await this.apiKeyModel.deleteOne({ _id: apiKeyId }) } + + async trackApiKeyUsage(apiKeyId: string) { + this.apiKeyModel + .findByIdAndUpdate( + apiKeyId, + { $inc: { usageCount: 1 }, lastUsedAt: new Date() }, + { new: true }, + ) + .exec() + .catch((e) => { + console.log('Failed to track api key usage') + console.log(e) + }) + } } diff --git a/api/src/auth/guards/auth.guard.ts b/api/src/auth/guards/auth.guard.ts index 4ab5a34..8bfc9c2 100644 --- a/api/src/auth/guards/auth.guard.ts +++ b/api/src/auth/guards/auth.guard.ts @@ -46,6 +46,9 @@ export class AuthGuard implements CanActivate { const user = await this.usersService.findOne({ _id: userId }) if (user) { request.user = user + if (request.query.apiKey) { + this.authService.trackApiKeyUsage(user._id) + } return true } } diff --git a/api/src/auth/schemas/api-key.schema.ts b/api/src/auth/schemas/api-key.schema.ts index 7a1d54a..426aed0 100644 --- a/api/src/auth/schemas/api-key.schema.ts +++ b/api/src/auth/schemas/api-key.schema.ts @@ -16,6 +16,12 @@ export class ApiKey { @Prop({ type: Types.ObjectId, ref: User.name }) user: User + + @Prop({ type: Number, default: 0 }) + usageCount: number + + @Prop({ type: Date }) + lastUsedAt: Date } export const ApiKeySchema = SchemaFactory.createForClass(ApiKey) From 583a5b8ad6dd22f41d837c1754c59a277e29e39c Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 01:40:44 +0300 Subject: [PATCH 05/46] fix(web): include missing api call for checking current user --- web/services/authService.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/web/services/authService.ts b/web/services/authService.ts index 129c769..2c7e6dd 100644 --- a/web/services/authService.ts +++ b/web/services/authService.ts @@ -45,6 +45,11 @@ class AuthService { }) return res.data.data } + + async whoAmI() { + const res = await httpClient.get(`/auth/who-am-i`) + return res.data.data + } } export const authService = new AuthService() From 96776aa879d0ce596a353cce8413520c9376f788 Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 01:41:35 +0300 Subject: [PATCH 06/46] chore(api): support apiKey authentication via headers in addition to query params --- api/src/auth/guards/auth.guard.ts | 29 +++++++++++++++-------------- api/src/main.ts | 5 +++++ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/api/src/auth/guards/auth.guard.ts b/api/src/auth/guards/auth.guard.ts index 8bfc9c2..f3ca682 100644 --- a/api/src/auth/guards/auth.guard.ts +++ b/api/src/auth/guards/auth.guard.ts @@ -20,25 +20,29 @@ export class AuthGuard implements CanActivate { ) {} async canActivate(context: ExecutionContext): Promise { - var userId - const request = context.switchToHttp().getRequest() + let userId + const apiKeyString = request.headers['x-api-key'] || request.query.apiKey if (request.headers.authorization?.startsWith('Bearer ')) { const bearerToken = request.headers.authorization.split(' ')[1] - const payload = this.jwtService.verify(bearerToken) - userId = payload.sub - } - - // check apiKey in query params - else if (request.query.apiKey) { - const apiKeyStr = request.query.apiKey - const regex = new RegExp(`^${apiKeyStr.substr(0, 17)}`, 'g') + try { + const payload = this.jwtService.verify(bearerToken) + userId = payload.sub + } catch (e) { + throw new HttpException( + { error: 'Unauthorized' }, + HttpStatus.UNAUTHORIZED, + ) + } + } else if (apiKeyString) { + const regex = new RegExp(`^${apiKeyString.substr(0, 17)}`, 'g') const apiKey = await this.authService.findApiKey({ apiKey: { $regex: regex }, }) - if (apiKey && bcrypt.compareSync(apiKeyStr, apiKey.hashedApiKey)) { + if (apiKey && bcrypt.compareSync(apiKeyString, apiKey.hashedApiKey)) { userId = apiKey.user + this.authService.trackApiKeyUsage(apiKey._id) } } @@ -46,9 +50,6 @@ export class AuthGuard implements CanActivate { const user = await this.usersService.findOne({ _id: userId }) if (user) { request.user = user - if (request.query.apiKey) { - this.authService.trackApiKeyUsage(user._id) - } return true } } diff --git a/api/src/main.ts b/api/src/main.ts index 83abe89..e54e09b 100644 --- a/api/src/main.ts +++ b/api/src/main.ts @@ -20,6 +20,11 @@ async function bootstrap() { .setDescription('TextBee - Android SMS Gateway API Docs') .setVersion('1.0') .addBearerAuth() + .addApiKey({ + type: 'apiKey', + name: 'x-api-key', + in: 'header', + }) .build() const document = SwaggerModule.createDocument(app, config) SwaggerModule.setup('', app, document, { From 80406ab3310fd8f7de263ab404014c8b577977c0 Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 01:43:56 +0300 Subject: [PATCH 07/46] chore(api): update sms schema --- api/src/gateway/gateway.module.ts | 5 ++++ api/src/gateway/schemas/sms.schema.ts | 38 +++++++++++++++++++++++++-- api/src/gateway/sms-type.enum.ts | 4 +++ 3 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 api/src/gateway/sms-type.enum.ts diff --git a/api/src/gateway/gateway.module.ts b/api/src/gateway/gateway.module.ts index 34052d6..167bc67 100644 --- a/api/src/gateway/gateway.module.ts +++ b/api/src/gateway/gateway.module.ts @@ -5,6 +5,7 @@ import { GatewayController } from './gateway.controller' import { GatewayService } from './gateway.service' import { AuthModule } from '../auth/auth.module' import { UsersModule } from '../users/users.module' +import { SMS, SMSSchema } from './schemas/sms.schema' @Module({ imports: [ @@ -13,6 +14,10 @@ import { UsersModule } from '../users/users.module' name: Device.name, schema: DeviceSchema, }, + { + name: SMS.name, + schema: SMSSchema, + }, ]), AuthModule, UsersModule, diff --git a/api/src/gateway/schemas/sms.schema.ts b/api/src/gateway/schemas/sms.schema.ts index 98772ee..348c8f3 100644 --- a/api/src/gateway/schemas/sms.schema.ts +++ b/api/src/gateway/schemas/sms.schema.ts @@ -1,6 +1,7 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose' import { Document, Types } from 'mongoose' import { Device } from './device.schema' +import { SMSType } from '../sms-type.enum' export type SMSDocument = SMS & Document @@ -8,14 +9,47 @@ export type SMSDocument = SMS & Document export class SMS { _id?: Types.ObjectId - @Prop({ type: Types.ObjectId, ref: Device.name }) + @Prop({ type: Types.ObjectId, ref: Device.name, required: true }) device: Device @Prop({ type: String, required: true }) message: string @Prop({ type: String, required: true }) - to: string + type: string + + // fields for incoming messages + @Prop({ type: String }) + sender: string + + @Prop({ type: Date, default: Date.now }) + receivedAt: Date + + // fields for outgoing messages + @Prop({ type: String }) + recipient: string + + @Prop({ type: Date, default: Date.now }) + requestedAt: Date + + @Prop({ type: Date }) + sentAt: Date + + @Prop({ type: Date }) + deliveredAt: Date + + @Prop({ type: Date }) + failedAt: Date + + // @Prop({ type: String }) + // failureReason: string + + // @Prop({ type: String }) + // status: string + + // misc metadata for debugging + @Prop({ type: Object }) + metadata: Record } export const SMSSchema = SchemaFactory.createForClass(SMS) diff --git a/api/src/gateway/sms-type.enum.ts b/api/src/gateway/sms-type.enum.ts new file mode 100644 index 0000000..7c7203b --- /dev/null +++ b/api/src/gateway/sms-type.enum.ts @@ -0,0 +1,4 @@ +export enum SMSType { + SENT = 'SENT', + RECEIVED = 'RECEIVED', +} From 3d6f3da6d1057a09b92c7b0aba54874846a0fff3 Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 01:50:53 +0300 Subject: [PATCH 08/46] chore(api): update response codes to 200 instead of the default 201 --- api/src/auth/auth.controller.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/src/auth/auth.controller.ts b/api/src/auth/auth.controller.ts index feb886f..c5a39f7 100644 --- a/api/src/auth/auth.controller.ts +++ b/api/src/auth/auth.controller.ts @@ -27,6 +27,7 @@ export class AuthController { constructor(private authService: AuthService) {} @ApiOperation({ summary: 'Login' }) + @HttpCode(HttpStatus.OK) @Post('/login') async login(@Body() input: LoginInputDTO) { const data = await this.authService.login(input) @@ -34,6 +35,7 @@ export class AuthController { } @ApiOperation({ summary: 'Login With Google' }) + @HttpCode(HttpStatus.OK) @Post('/google-login') async googleLogin(@Body() input: any) { const data = await this.authService.loginWithGoogle(input.idToken) From c31763d1e45f78d843860e46a00304d25db4d178 Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 01:51:55 +0300 Subject: [PATCH 09/46] chore(android): replace query apiKey auth by header --- .../com/vernu/sms/services/GatewayApiService.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/java/com/vernu/sms/services/GatewayApiService.java b/android/app/src/main/java/com/vernu/sms/services/GatewayApiService.java index 73e546e..7d78ba7 100644 --- a/android/app/src/main/java/com/vernu/sms/services/GatewayApiService.java +++ b/android/app/src/main/java/com/vernu/sms/services/GatewayApiService.java @@ -1,19 +1,24 @@ package com.vernu.sms.services; +import com.vernu.sms.dtos.SMSDTO; +import com.vernu.sms.dtos.SMSForwardResponseDTO; import com.vernu.sms.dtos.RegisterDeviceInputDTO; import com.vernu.sms.dtos.RegisterDeviceResponseDTO; import retrofit2.Call; import retrofit2.http.Body; +import retrofit2.http.Header; import retrofit2.http.PATCH; import retrofit2.http.POST; import retrofit2.http.Path; -import retrofit2.http.Query; public interface GatewayApiService { @POST("gateway/devices") - Call registerDevice(@Query("apiKey") String apiKey, @Body() RegisterDeviceInputDTO body); + Call registerDevice(@Header("x-api-key") String apiKey, @Body() RegisterDeviceInputDTO body); @PATCH("gateway/devices/{deviceId}") - Call updateDevice(@Path("deviceId") String deviceId, @Query("apiKey") String apiKey, @Body() RegisterDeviceInputDTO body); + Call updateDevice(@Path("deviceId") String deviceId, @Header("x-api-key") String apiKey, @Body() RegisterDeviceInputDTO body); + + @POST("gateway/devices/{deviceId}/receivedSMS") + Call sendReceivedSMS(@Path("deviceId") String deviceId, @Header("x-api-key") String apiKey, @Body() SMSDTO body); } \ No newline at end of file From cdb1a0d73a51cf83c697b1419dd6260d1530b868 Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 02:04:47 +0300 Subject: [PATCH 10/46] chore(api): update sms payload fields --- api/src/gateway/gateway.dto.ts | 54 ++++++++++++++++++++++++++++-- api/src/gateway/gateway.service.ts | 19 +++++++++-- 2 files changed, 68 insertions(+), 5 deletions(-) diff --git a/api/src/gateway/gateway.dto.ts b/api/src/gateway/gateway.dto.ts index 07baf66..cb405b0 100644 --- a/api/src/gateway/gateway.dto.ts +++ b/api/src/gateway/gateway.dto.ts @@ -39,16 +39,64 @@ export class SMSData { @ApiProperty({ type: String, required: true, - description: 'SMS text', + description: 'The message to send', }) - smsBody: string + message: string @ApiProperty({ type: Array, required: true, - description: 'Array of phone numbers', + description: 'List of phone numbers to send the SMS to', example: ['+2519xxxxxxxx', '+2517xxxxxxxx'], }) + recipients: string[] + + // TODO: restructure the Payload such that it contains bactchId, smsId, recipients and message in an optimized way + // message: string + // bactchId: string + // list: { + // smsId: string + // recipient: string + // } + + // Legacy fields to be removed in the future + // @ApiProperty({ + // type: String, + // required: true, + // description: '(Legacy) Will be Replace with `message` field in the future', + // }) + smsBody: string + + // @ApiProperty({ + // type: Array, + // required: false, + // description: + // '(Legacy) Will be Replace with `recipients` field in the future', + // example: ['+2519xxxxxxxx', '+2517xxxxxxxx'], + // }) receivers: string[] } export class SendSMSInputDTO extends SMSData {} + +export class ReceivedSMSDTO { + @ApiProperty({ + type: String, + required: true, + description: 'The message received', + }) + message: string + + @ApiProperty({ + type: String, + required: true, + description: 'The phone number of the sender', + }) + sender: string + + @ApiProperty({ + type: Date, + required: true, + description: 'The time the message was received', + }) + receivedAt: Date +} diff --git a/api/src/gateway/gateway.service.ts b/api/src/gateway/gateway.service.ts index 5080743..f995008 100644 --- a/api/src/gateway/gateway.service.ts +++ b/api/src/gateway/gateway.service.ts @@ -6,10 +6,13 @@ import * as firebaseAdmin from 'firebase-admin' import { RegisterDeviceInputDTO, SendSMSInputDTO } from './gateway.dto' import { User } from '../users/schemas/user.schema' import { AuthService } from 'src/auth/auth.service' +import { SMS } from './schemas/sms.schema' + @Injectable() export class GatewayService { constructor( @InjectModel(Device.name) private deviceModel: Model, + @InjectModel(SMS.name) private smsModel: Model, private authService: AuthService, ) {} @@ -76,6 +79,14 @@ export class GatewayService { } async sendSMS(deviceId: string, smsData: SendSMSInputDTO): Promise { + const updatedSMSData = { + message: smsData.message || smsData.smsBody, + recipients: smsData.recipients || smsData.receivers, + + // Legacy fields to be removed in the future + smsBody: smsData.message || smsData.smsBody, + receivers: smsData.recipients || smsData.receivers, + } const device = await this.deviceModel.findById(deviceId) if (!device?.enabled) { @@ -88,11 +99,15 @@ export class GatewayService { ) } + const stringifiedSMSData = JSON.stringify(updatedSMSData) const payload: any = { data: { - smsData: JSON.stringify(smsData), + smsData: stringifiedSMSData, }, } + + // TODO: Save SMS and Implement a queue to send the SMS if recipients are too many + try { const response = await firebaseAdmin .messaging() @@ -100,7 +115,7 @@ export class GatewayService { this.deviceModel .findByIdAndUpdate(deviceId, { - $inc: { sentSMSCount: smsData.receivers.length }, + $inc: { sentSMSCount: updatedSMSData.recipients.length }, }) .exec() .catch((e) => { From ef49ac1624e259eacd07f2034bff7c1b74973dde Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 02:07:41 +0300 Subject: [PATCH 11/46] chore(android): main activity minor ui change --- android/app/src/main/res/layout/activity_main.xml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index eb007a3..4771469 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -50,7 +50,8 @@ android:id="@+id/defaultSimSlotRadioGroup" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="horizontal"> + android:orientation="vertical" + > From 646e7f11b63e8684e6c0ebfc32b53c20009e6b21 Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 02:10:12 +0300 Subject: [PATCH 12/46] chore(android): add AppConstants class --- .../main/java/com/vernu/sms/AppConstants.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 android/app/src/main/java/com/vernu/sms/AppConstants.java diff --git a/android/app/src/main/java/com/vernu/sms/AppConstants.java b/android/app/src/main/java/com/vernu/sms/AppConstants.java new file mode 100644 index 0000000..53a171f --- /dev/null +++ b/android/app/src/main/java/com/vernu/sms/AppConstants.java @@ -0,0 +1,18 @@ +package com.vernu.sms; + +import android.Manifest; + +public class AppConstants { + public static final String API_BASE_URL = "https://api.textbee.dev/api/v1/"; + public static final String[] requiredPermissions = new String[]{ + Manifest.permission.SEND_SMS, + Manifest.permission.READ_SMS, + Manifest.permission.RECEIVE_SMS, + Manifest.permission.READ_PHONE_STATE + }; + public static final String SHARED_PREFS_DEVICE_ID_KEY = "DEVICE_ID"; + public static final String SHARED_PREFS_API_KEY_KEY = "API_KEY"; + public static final String SHARED_PREFS_GATEWAY_ENABLED_KEY = "GATEWAY_ENABLED"; + + public static final String SHARED_PREFS_PREFERRED_SIM_KEY = "PREFERRED_SIM"; +} From a09510bc8234f0e2d4d5e2b6dec5cdea37c99892 Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 02:11:23 +0300 Subject: [PATCH 13/46] chore(android): update sms payload fields --- .../java/com/vernu/sms/models/SMSPayload.java | 25 +++++++++++-------- .../com/vernu/sms/services/FCMService.java | 16 ++++++------ 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/android/app/src/main/java/com/vernu/sms/models/SMSPayload.java b/android/app/src/main/java/com/vernu/sms/models/SMSPayload.java index 4e5417a..db98ba1 100644 --- a/android/app/src/main/java/com/vernu/sms/models/SMSPayload.java +++ b/android/app/src/main/java/com/vernu/sms/models/SMSPayload.java @@ -1,27 +1,30 @@ package com.vernu.sms.models; public class SMSPayload { + + private String[] recipients; + private String message; + + // Legacy fields that are no longer used private String[] receivers; private String smsBody; - public SMSPayload(String[] receivers, String smsBody) { - this.receivers = receivers; - this.smsBody = smsBody; + public SMSPayload() { } - public String[] getReceivers() { - return receivers; + public String[] getRecipients() { + return recipients; } - public void setReceivers(String[] receivers) { - this.receivers = receivers; + public void setRecipients(String[] recipients) { + this.recipients = recipients; } - public String getSmsBody() { - return smsBody; + public String getMessage() { + return message; } - public void setSmsBody(String smsBody) { - this.smsBody = smsBody; + public void setMessage(String message) { + this.message = message; } } diff --git a/android/app/src/main/java/com/vernu/sms/services/FCMService.java b/android/app/src/main/java/com/vernu/sms/services/FCMService.java index bae9b2c..b113084 100644 --- a/android/app/src/main/java/com/vernu/sms/services/FCMService.java +++ b/android/app/src/main/java/com/vernu/sms/services/FCMService.java @@ -9,42 +9,40 @@ import android.net.Uri; import android.os.Build; import android.util.Log; - import androidx.core.app.NotificationCompat; - import com.google.firebase.messaging.FirebaseMessagingService; import com.google.firebase.messaging.RemoteMessage; import com.google.gson.Gson; +import com.vernu.sms.AppConstants; import com.vernu.sms.R; import com.vernu.sms.activities.MainActivity; import com.vernu.sms.helpers.SMSHelper; import com.vernu.sms.helpers.SharedPreferenceHelper; import com.vernu.sms.models.SMSPayload; - public class FCMService extends FirebaseMessagingService { - private static final String TAG = "MyFirebaseMsgService"; + private static final String TAG = "FirebaseMessagingService"; private static final String DEFAULT_NOTIFICATION_CHANNEL_ID = "N1"; @Override public void onMessageReceived(RemoteMessage remoteMessage) { - Log.d("FCM_MESSAGE", remoteMessage.getData().toString()); + Log.d(TAG, remoteMessage.getData().toString()); Gson gson = new Gson(); SMSPayload smsPayload = gson.fromJson(remoteMessage.getData().get("smsData"), SMSPayload.class); // Check if message contains a data payload. if (remoteMessage.getData().size() > 0) { - int preferedSim = SharedPreferenceHelper.getSharedPreferenceInt(this, "PREFERED_SIM", -1); - for (String receiver : smsPayload.getReceivers()) { + int preferedSim = SharedPreferenceHelper.getSharedPreferenceInt(this, AppConstants.SHARED_PREFS_PREFERRED_SIM_KEY, -1); + for (String receiver : smsPayload.getRecipients()) { if(preferedSim == -1) { - SMSHelper.sendSMS(receiver, smsPayload.getSmsBody()); + SMSHelper.sendSMS(receiver, smsPayload.getMessage()); continue; } try { - SMSHelper.sendSMSFromSpecificSim(receiver, smsPayload.getSmsBody(), preferedSim); + SMSHelper.sendSMSFromSpecificSim(receiver, smsPayload.getMessage(), preferedSim); } catch(Exception e) { Log.d("SMS_SEND_ERROR", e.getMessage()); } From cd0c40bffaf1dadc10c6fa54a2fa41a386334ac0 Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 02:13:27 +0300 Subject: [PATCH 14/46] chore(android): add a function to clear a shared pref value --- .../java/com/vernu/sms/helpers/SharedPreferenceHelper.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/android/app/src/main/java/com/vernu/sms/helpers/SharedPreferenceHelper.java b/android/app/src/main/java/com/vernu/sms/helpers/SharedPreferenceHelper.java index f478598..ea7386c 100644 --- a/android/app/src/main/java/com/vernu/sms/helpers/SharedPreferenceHelper.java +++ b/android/app/src/main/java/com/vernu/sms/helpers/SharedPreferenceHelper.java @@ -44,4 +44,11 @@ public static boolean getSharedPreferenceBoolean(Context context, String key, bo SharedPreferences settings = context.getSharedPreferences(PREF_FILE, 0); return settings.getBoolean(key, defValue); } + + public static void clearSharedPreference(Context context, String key) { + SharedPreferences settings = context.getSharedPreferences(PREF_FILE, 0); + SharedPreferences.Editor editor = settings.edit(); + editor.remove(key); + editor.apply(); + } } \ No newline at end of file From 93652cd8354129e6dd2577dfab8d24b4f0da3aef Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 02:14:30 +0300 Subject: [PATCH 15/46] chore(android): create an ApiManager class --- .../main/java/com/vernu/sms/ApiManager.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 android/app/src/main/java/com/vernu/sms/ApiManager.java diff --git a/android/app/src/main/java/com/vernu/sms/ApiManager.java b/android/app/src/main/java/com/vernu/sms/ApiManager.java new file mode 100644 index 0000000..e991b75 --- /dev/null +++ b/android/app/src/main/java/com/vernu/sms/ApiManager.java @@ -0,0 +1,33 @@ +package com.vernu.sms; + +import com.vernu.sms.services.GatewayApiService; + +import retrofit2.Retrofit; +import retrofit2.converter.gson.GsonConverterFactory; + +public class ApiManager { + private static GatewayApiService apiService; + + public static GatewayApiService getApiService() { + if (apiService == null) { + apiService = createApiService(); + } + return apiService; + } + + private static GatewayApiService createApiService() { +// OkHttpClient.Builder httpClient = new OkHttpClient.Builder(); +// HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor(); +// loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY); +// httpClient.addInterceptor(loggingInterceptor); + + Retrofit retrofit = new Retrofit.Builder() + .baseUrl(AppConstants.API_BASE_URL) +// .client(httpClient.build()) + .addConverterFactory(GsonConverterFactory.create()) + .build(); + apiService = retrofit.create(GatewayApiService.class); + + return retrofit.create(GatewayApiService.class); + } +} From 2013ee5f7e4e6612b8fa3a4bd083ef24a1c8792b Mon Sep 17 00:00:00 2001 From: isra el Date: Mon, 15 Apr 2024 02:15:15 +0300 Subject: [PATCH 16/46] chore(android): update types for device version fields --- .../java/com/vernu/sms/dtos/RegisterDeviceInputDTO.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/android/app/src/main/java/com/vernu/sms/dtos/RegisterDeviceInputDTO.java b/android/app/src/main/java/com/vernu/sms/dtos/RegisterDeviceInputDTO.java index f803814..f7e47fa 100644 --- a/android/app/src/main/java/com/vernu/sms/dtos/RegisterDeviceInputDTO.java +++ b/android/app/src/main/java/com/vernu/sms/dtos/RegisterDeviceInputDTO.java @@ -11,7 +11,7 @@ public class RegisterDeviceInputDTO { private String os; private String osVersion; private String appVersionName; - private String appVersionCode; + private int appVersionCode; public RegisterDeviceInputDTO() { } @@ -100,11 +100,11 @@ public void setAppVersionName(String appVersionName) { this.appVersionName = appVersionName; } - public String getAppVersionCode() { + public int getAppVersionCode() { return appVersionCode; } - public void setAppVersionCode(String appVersionCode) { + public void setAppVersionCode(int appVersionCode) { this.appVersionCode = appVersionCode; } } From 51b1828b1d1f0ca81ef91078c2db139ac48cd729 Mon Sep 17 00:00:00 2001 From: isra el Date: Tue, 16 Apr 2024 07:38:19 +0300 Subject: [PATCH 17/46] feat(api): receive and save sms --- api/src/gateway/gateway.controller.ts | 25 ++++++++++++++- api/src/gateway/gateway.service.ts | 44 +++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/api/src/gateway/gateway.controller.ts b/api/src/gateway/gateway.controller.ts index 2d35dbe..1275f52 100644 --- a/api/src/gateway/gateway.controller.ts +++ b/api/src/gateway/gateway.controller.ts @@ -8,10 +8,16 @@ import { Request, Get, Delete, + HttpCode, + HttpStatus, } from '@nestjs/common' import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger' import { AuthGuard } from '../auth/guards/auth.guard' -import { RegisterDeviceInputDTO, SendSMSInputDTO } from './gateway.dto' +import { + ReceivedSMSDTO, + RegisterDeviceInputDTO, + SendSMSInputDTO, +} from './gateway.dto' import { GatewayService } from './gateway.service' import { CanModifyDevice } from './guards/can-modify-device.guard' @@ -98,4 +104,21 @@ export class GatewayController { const data = await this.gatewayService.sendSMS(deviceId, smsData) return { data } } + + @ApiOperation({ summary: 'Received SMS from a device' }) + @ApiQuery({ + name: 'apiKey', + required: false, + description: 'Required if jwt bearer token not provided', + }) + @HttpCode(HttpStatus.OK) + @Post('/devices/:id/receivedSMS') + @UseGuards(AuthGuard, CanModifyDevice) + async receivedSMS( + @Param('id') deviceId: string, + @Body() dto: ReceivedSMSDTO, + ) { + const data = await this.gatewayService.receivedSMS(deviceId, dto) + return { data } + } } diff --git a/api/src/gateway/gateway.service.ts b/api/src/gateway/gateway.service.ts index f995008..6935383 100644 --- a/api/src/gateway/gateway.service.ts +++ b/api/src/gateway/gateway.service.ts @@ -3,11 +3,15 @@ import { InjectModel } from '@nestjs/mongoose' import { Device, DeviceDocument } from './schemas/device.schema' import { Model } from 'mongoose' import * as firebaseAdmin from 'firebase-admin' -import { RegisterDeviceInputDTO, SendSMSInputDTO } from './gateway.dto' +import { + ReceivedSMSDTO, + RegisterDeviceInputDTO, + SendSMSInputDTO, +} from './gateway.dto' import { User } from '../users/schemas/user.schema' import { AuthService } from 'src/auth/auth.service' import { SMS } from './schemas/sms.schema' - +import { SMSType } from './sms-type.enum' @Injectable() export class GatewayService { constructor( @@ -133,6 +137,42 @@ export class GatewayService { } } + async receivedSMS(deviceId: string, dto: ReceivedSMSDTO): Promise { + const device = await this.deviceModel.findById(deviceId) + + if (!device) { + throw new HttpException( + { + success: false, + error: 'Device does not exist', + }, + HttpStatus.BAD_REQUEST, + ) + } + + if (!dto.receivedAt || !dto.sender || !dto.message) { + throw new HttpException( + { + success: false, + error: 'Invalid received SMS data', + }, + HttpStatus.BAD_REQUEST, + ) + } + + const sms = await this.smsModel.create({ + device: device._id, + message: dto.message, + type: SMSType.RECEIVED, + sender: dto.sender, + receivedAt: dto.receivedAt, + }) + + // TODO: Implement webhook to forward received SMS to user's callback URL + + return sms + } + async getStatsForUser(user: User) { const devices = await this.deviceModel.find({ user: user._id }) const apiKeys = await this.authService.getUserApiKeys(user) From c82f8e801cfe6111813574431748a7a9246b4e25 Mon Sep 17 00:00:00 2001 From: isra el Date: Tue, 16 Apr 2024 07:38:52 +0300 Subject: [PATCH 18/46] chore(android): reorganize components in main activity --- .../app/src/main/res/layout/activity_main.xml | 127 ++++++++++++++---- 1 file changed, 99 insertions(+), 28 deletions(-) diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml index 4771469..5271681 100644 --- a/android/app/src/main/res/layout/activity_main.xml +++ b/android/app/src/main/res/layout/activity_main.xml @@ -12,10 +12,10 @@ android:layout_height="wrap_content" android:background="#ccccccee" android:orientation="vertical" + android:padding="12dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - android:padding="12dp"> + app:layout_constraintStart_toStartOf="parent"> -