Browse Source

refactor: move everything to one repo

pull/1/head
isra el 3 years ago
parent
commit
78cce57091
  1. 0
      android/.gitignore
  2. 0
      android/.idea/.gitignore
  3. 0
      android/.idea/.name
  4. 0
      android/.idea/compiler.xml
  5. 0
      android/.idea/gradle.xml
  6. 0
      android/.idea/misc.xml
  7. 0
      android/.idea/vcs.xml
  8. 0
      android/app/.gitignore
  9. 0
      android/app/build.gradle
  10. 0
      android/app/google-services.json
  11. 0
      android/app/proguard-rules.pro
  12. 0
      android/app/src/androidTest/java/com/vernu/sms/ExampleInstrumentedTest.java
  13. 0
      android/app/src/main/AndroidManifest.xml
  14. 2
      android/app/src/main/java/com/vernu/sms/activities/MainActivity.java
  15. 0
      android/app/src/main/java/com/vernu/sms/dtos/RegisterDeviceInputDTO.java
  16. 0
      android/app/src/main/java/com/vernu/sms/dtos/RegisterDeviceResponseDTO.java
  17. 0
      android/app/src/main/java/com/vernu/sms/helpers/SMSHelper.java
  18. 0
      android/app/src/main/java/com/vernu/sms/helpers/SharedPreferenceHelper.java
  19. 0
      android/app/src/main/java/com/vernu/sms/models/SMSPayload.java
  20. 0
      android/app/src/main/java/com/vernu/sms/services/FCMService.java
  21. 0
      android/app/src/main/java/com/vernu/sms/services/GatewayApiService.java
  22. 0
      android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml
  23. 0
      android/app/src/main/res/drawable/ic_baseline_content_copy_24.xml
  24. 0
      android/app/src/main/res/drawable/ic_baseline_phone_android_24.xml
  25. 0
      android/app/src/main/res/drawable/ic_baseline_qr_code_24.xml
  26. 0
      android/app/src/main/res/drawable/ic_launcher_background.xml
  27. 0
      android/app/src/main/res/layout/activity_main.xml
  28. 0
      android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
  29. 0
      android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
  30. 0
      android/app/src/main/res/mipmap-hdpi/ic_launcher.webp
  31. 0
      android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
  32. 0
      android/app/src/main/res/mipmap-mdpi/ic_launcher.webp
  33. 0
      android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
  34. 0
      android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp
  35. 0
      android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
  36. 0
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
  37. 0
      android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
  38. 0
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
  39. 0
      android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
  40. 0
      android/app/src/main/res/values-night/themes.xml
  41. 0
      android/app/src/main/res/values/colors.xml
  42. 0
      android/app/src/main/res/values/strings.xml
  43. 0
      android/app/src/main/res/values/themes.xml
  44. 0
      android/app/src/test/java/com/vernu/sms/ExampleUnitTest.java
  45. 0
      android/build.gradle
  46. 0
      android/gradle.properties
  47. 0
      android/gradle/wrapper/gradle-wrapper.jar
  48. 0
      android/gradle/wrapper/gradle-wrapper.properties
  49. 0
      android/gradlew
  50. 0
      android/gradlew.bat
  51. 0
      android/settings.gradle
  52. 10
      api/.env.example
  53. 24
      api/.eslintrc.js
  54. 38
      api/.gitignore
  55. 5
      api/.prettierrc
  56. 1
      api/Procfile
  57. 73
      api/README.md
  58. 4
      api/nest-cli.json
  59. 17979
      api/package-lock.json
  60. 84
      api/package.json
  61. 22
      api/src/app.controller.spec.ts
  62. 5
      api/src/app.controller.ts
  63. 19
      api/src/app.module.ts
  64. 6
      api/src/app.service.ts
  65. 18
      api/src/auth/auth.controller.spec.ts
  66. 74
      api/src/auth/auth.controller.ts
  67. 23
      api/src/auth/auth.dto.ts
  68. 57
      api/src/auth/auth.guard.ts
  69. 30
      api/src/auth/auth.module.ts
  70. 18
      api/src/auth/auth.service.spec.ts
  71. 100
      api/src/auth/auth.service.ts
  72. 5
      api/src/auth/jwt-auth.guard.ts
  73. 26
      api/src/auth/jwt.strategy.ts
  74. 21
      api/src/auth/schemas/api-key.schema.ts
  75. 18
      api/src/gateway/gateway.controller.spec.ts
  76. 63
      api/src/gateway/gateway.controller.ts
  77. 54
      api/src/gateway/gateway.dto.ts
  78. 24
      api/src/gateway/gateway.module.ts
  79. 18
      api/src/gateway/gateway.service.spec.ts
  80. 88
      api/src/gateway/gateway.service.ts
  81. 48
      api/src/gateway/schemas/device.schema.ts
  82. 23
      api/src/gateway/schemas/sms.schema.ts
  83. 51
      api/src/main.ts
  84. 27
      api/src/users/schemas/user.schema.ts
  85. 4
      api/src/users/user-roles.enum.ts
  86. 18
      api/src/users/users.controller.spec.ts
  87. 4
      api/src/users/users.controller.ts
  88. 20
      api/src/users/users.module.ts
  89. 18
      api/src/users/users.service.spec.ts
  90. 36
      api/src/users/users.service.ts
  91. 24
      api/test/app.e2e-spec.ts
  92. 9
      api/test/jest-e2e.json
  93. 4
      api/tsconfig.build.json
  94. 21
      api/tsconfig.json
  95. 3
      web/.eslintrc.json
  96. 38
      web/.gitignore
  97. 34
      web/README.md
  98. 118
      web/components/Navbar.tsx
  99. 72
      web/components/dashboard/ApiKeyList.tsx
  100. 154
      web/components/dashboard/GenerateApiKey.tsx

0
.gitignore → android/.gitignore

0
.idea/.gitignore → android/.idea/.gitignore

0
.idea/.name → android/.idea/.name

0
.idea/compiler.xml → android/.idea/compiler.xml

0
.idea/gradle.xml → android/.idea/gradle.xml

0
.idea/misc.xml → android/.idea/misc.xml

0
.idea/vcs.xml → android/.idea/vcs.xml

0
app/.gitignore → android/app/.gitignore

0
app/build.gradle → android/app/build.gradle

0
app/google-services.json → android/app/google-services.json

0
app/proguard-rules.pro → android/app/proguard-rules.pro

0
app/src/androidTest/java/com/vernu/sms/ExampleInstrumentedTest.java → android/app/src/androidTest/java/com/vernu/sms/ExampleInstrumentedTest.java

0
app/src/main/AndroidManifest.xml → android/app/src/main/AndroidManifest.xml

2
app/src/main/java/com/vernu/sms/activities/MainActivity.java → android/app/src/main/java/com/vernu/sms/activities/MainActivity.java

@ -57,7 +57,7 @@ public class MainActivity extends AppCompatActivity {
private static final int SEND_SMS_PERMISSION_REQUEST_CODE = 0;
private static final int SCAN_QR_REQUEST_CODE = 49374;
private static final String API_BASE_URL = "https://vernu-sms.herokuapp.com/api/v1/";
private static final String API_BASE_URL = "https://api.sms.real.et/api/v1/";
private String deviceId = null;

0
app/src/main/java/com/vernu/sms/dtos/RegisterDeviceInputDTO.java → android/app/src/main/java/com/vernu/sms/dtos/RegisterDeviceInputDTO.java

0
app/src/main/java/com/vernu/sms/dtos/RegisterDeviceResponseDTO.java → android/app/src/main/java/com/vernu/sms/dtos/RegisterDeviceResponseDTO.java

0
app/src/main/java/com/vernu/sms/helpers/SMSHelper.java → android/app/src/main/java/com/vernu/sms/helpers/SMSHelper.java

0
app/src/main/java/com/vernu/sms/helpers/SharedPreferenceHelper.java → android/app/src/main/java/com/vernu/sms/helpers/SharedPreferenceHelper.java

0
app/src/main/java/com/vernu/sms/models/SMSPayload.java → android/app/src/main/java/com/vernu/sms/models/SMSPayload.java

0
app/src/main/java/com/vernu/sms/services/FCMService.java → android/app/src/main/java/com/vernu/sms/services/FCMService.java

0
app/src/main/java/com/vernu/sms/services/GatewayApiService.java → android/app/src/main/java/com/vernu/sms/services/GatewayApiService.java

0
app/src/main/res/drawable-v24/ic_launcher_foreground.xml → android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml

0
app/src/main/res/drawable/ic_baseline_content_copy_24.xml → android/app/src/main/res/drawable/ic_baseline_content_copy_24.xml

0
app/src/main/res/drawable/ic_baseline_phone_android_24.xml → android/app/src/main/res/drawable/ic_baseline_phone_android_24.xml

0
app/src/main/res/drawable/ic_baseline_qr_code_24.xml → android/app/src/main/res/drawable/ic_baseline_qr_code_24.xml

0
app/src/main/res/drawable/ic_launcher_background.xml → android/app/src/main/res/drawable/ic_launcher_background.xml

0
app/src/main/res/layout/activity_main.xml → android/app/src/main/res/layout/activity_main.xml

0
app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml → android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml

0
app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml → android/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml

0
app/src/main/res/mipmap-hdpi/ic_launcher.webp → android/app/src/main/res/mipmap-hdpi/ic_launcher.webp

0
app/src/main/res/mipmap-hdpi/ic_launcher_round.webp → android/app/src/main/res/mipmap-hdpi/ic_launcher_round.webp

0
app/src/main/res/mipmap-mdpi/ic_launcher.webp → android/app/src/main/res/mipmap-mdpi/ic_launcher.webp

0
app/src/main/res/mipmap-mdpi/ic_launcher_round.webp → android/app/src/main/res/mipmap-mdpi/ic_launcher_round.webp

0
app/src/main/res/mipmap-xhdpi/ic_launcher.webp → android/app/src/main/res/mipmap-xhdpi/ic_launcher.webp

0
app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp → android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp

0
app/src/main/res/mipmap-xxhdpi/ic_launcher.webp → android/app/src/main/res/mipmap-xxhdpi/ic_launcher.webp

0
app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp → android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp

0
app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp → android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp

0
app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp → android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp

0
app/src/main/res/values-night/themes.xml → android/app/src/main/res/values-night/themes.xml

0
app/src/main/res/values/colors.xml → android/app/src/main/res/values/colors.xml

0
app/src/main/res/values/strings.xml → android/app/src/main/res/values/strings.xml

0
app/src/main/res/values/themes.xml → android/app/src/main/res/values/themes.xml

0
app/src/test/java/com/vernu/sms/ExampleUnitTest.java → android/app/src/test/java/com/vernu/sms/ExampleUnitTest.java

0
build.gradle → android/build.gradle

0
gradle.properties → android/gradle.properties

0
gradle/wrapper/gradle-wrapper.jar → android/gradle/wrapper/gradle-wrapper.jar

0
gradle/wrapper/gradle-wrapper.properties → android/gradle/wrapper/gradle-wrapper.properties

0
gradlew → android/gradlew

0
gradlew.bat → android/gradlew.bat

0
settings.gradle → android/settings.gradle

10
api/.env.example

@ -0,0 +1,10 @@
PORT=
MONGO_URI=
JWT_SECRET=secret
FIREBASE_PROJECT_ID=
FIREBASE_PRIVATE_KEY_ID=
FIREBASE_PRIVATE_KEY=
FIREBASE_CLIENT_EMAIL=
FIREBASE_CLIENT_ID=
FIREBASE_CLIENT_C509_CERT_URL=

24
api/.eslintrc.js

@ -0,0 +1,24 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
extends: [
'plugin:@typescript-eslint/recommended',
'plugin:prettier/recommended',
],
root: true,
env: {
node: true,
jest: true,
},
ignorePatterns: ['.eslintrc.js'],
rules: {
'@typescript-eslint/interface-name-prefix': 'off',
'@typescript-eslint/explicit-function-return-type': 'off',
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};

38
api/.gitignore

@ -0,0 +1,38 @@
# compiled output
/dist
/node_modules
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.env

5
api/.prettierrc

@ -0,0 +1,5 @@
{
"singleQuote": true,
"trailingComma": "all",
"semi": false
}

1
api/Procfile

@ -0,0 +1 @@
web: npm run start:prod

73
api/README.md

@ -0,0 +1,73 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo_text.svg" width="320" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ npm install
```
## Running the app
```bash
# development
$ npm run start
# watch mode
$ npm run start:dev
# production mode
$ npm run start:prod
```
## Test
```bash
# unit tests
$ npm run test
# e2e tests
$ npm run test:e2e
# test coverage
$ npm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

4
api/nest-cli.json

@ -0,0 +1,4 @@
{
"collection": "@nestjs/schematics",
"sourceRoot": "src"
}

17979
api/package-lock.json
File diff suppressed because it is too large
View File

84
api/package.json

@ -0,0 +1,84 @@
{
"name": "sms-gateway-backend",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"start:dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/common": "^8.0.0",
"@nestjs/core": "^8.0.0",
"@nestjs/jwt": "^8.0.0",
"@nestjs/mongoose": "^9.0.2",
"@nestjs/passport": "^8.2.1",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/swagger": "^5.2.1",
"bcryptjs": "^2.4.3",
"dotenv": "^16.0.0",
"firebase-admin": "^10.0.2",
"mongoose": "^6.2.4",
"passport": "^0.5.2",
"passport-jwt": "^4.0.0",
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.2.0",
"swagger-ui-express": "^4.3.0",
"uuid": "^8.3.2"
},
"devDependencies": {
"@nestjs/cli": "^8.0.0",
"@nestjs/schematics": "^8.0.0",
"@nestjs/testing": "^8.0.0",
"@types/express": "^4.17.13",
"@types/jest": "27.0.2",
"@types/node": "^16.0.0",
"@types/passport-jwt": "^3.0.6",
"@types/supertest": "^2.0.11",
"@typescript-eslint/eslint-plugin": "^5.0.0",
"@typescript-eslint/parser": "^5.0.0",
"eslint": "^8.0.1",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"jest": "^27.2.5",
"prettier": "^2.3.2",
"source-map-support": "^0.5.20",
"supertest": "^6.1.3",
"ts-jest": "^27.0.3",
"ts-loader": "^9.2.3",
"ts-node": "^10.0.0",
"tsconfig-paths": "^3.10.1",
"typescript": "^4.3.5"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

22
api/src/app.controller.spec.ts

@ -0,0 +1,22 @@
import { Test, TestingModule } from '@nestjs/testing'
import { AppController } from './app.controller'
import { AppService } from './app.service'
describe('AppController', () => {
let appController: AppController
beforeEach(async () => {
const app: TestingModule = await Test.createTestingModule({
controllers: [AppController],
providers: [AppService],
}).compile()
appController = app.get<AppController>(AppController)
})
describe('root', () => {
it('should return "Hello World!"', () => {
expect(appController.getHello()).toBe('Hello World!')
})
})
})

5
api/src/app.controller.ts

@ -0,0 +1,5 @@
import { Controller, Get } from '@nestjs/common'
import { AppService } from './app.service'
@Controller()
export class AppController {}

19
api/src/app.module.ts

@ -0,0 +1,19 @@
import { Module } from '@nestjs/common'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { MongooseModule } from '@nestjs/mongoose'
import { GatewayModule } from './gateway/gateway.module'
import { AuthModule } from './auth/auth.module'
import { UsersModule } from './users/users.module'
@Module({
imports: [
MongooseModule.forRoot(process.env.MONGO_URI),
AuthModule,
UsersModule,
GatewayModule,
],
controllers: [AppController],
providers: [AppService],
})
export class AppModule {}

6
api/src/app.service.ts

@ -0,0 +1,6 @@
import { Injectable } from '@nestjs/common'
@Injectable()
export class AppService {
constructor() {}
}

18
api/src/auth/auth.controller.spec.ts

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'
import { AuthController } from './auth.controller'
describe('AuthController', () => {
let controller: AuthController
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [AuthController],
}).compile()
controller = module.get<AuthController>(AuthController)
})
it('should be defined', () => {
expect(controller).toBeDefined()
})
})

74
api/src/auth/auth.controller.ts

@ -0,0 +1,74 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Post,
Request,
UseGuards,
} from '@nestjs/common'
import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'
import { LoginInputDTO, RegisterInputDTO } from './auth.dto'
import { AuthGuard } from './auth.guard'
import { AuthService } from './auth.service'
@ApiTags('auth')
@Controller('auth')
export class AuthController {
constructor(private authService: AuthService) {}
@ApiOperation({ summary: 'Login' })
@Post('/login')
async login(@Body() input: LoginInputDTO) {
const data = await this.authService.login(input)
return { data }
}
@ApiOperation({ summary: 'Register' })
@Post('/register')
async register(@Body() input: RegisterInputDTO) {
const data = await this.authService.register(input)
return { data }
}
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Generate Api Key' })
@ApiQuery({
name: 'apiKey',
required: false,
description: 'Required if jwt bearer token not provided',
})
@ApiBearerAuth()
@Post('/api-keys')
async generateApiKey(@Request() req) {
const { apiKey, message } = await this.authService.generateApiKey(req.user)
return { data: apiKey, message }
}
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Get Api Key List (masked***)' })
@ApiQuery({
name: 'apiKey',
required: false,
description: 'Required if jwt bearer token not provided',
})
@ApiBearerAuth()
@Get('/api-keys')
async getApiKey(@Request() req) {
const data = await this.authService.getUserApiKeys(req.user)
return { data }
}
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Generate Api Key' })
@ApiBearerAuth()
@HttpCode(HttpStatus.OK)
@Delete('/api-keys/:id')
async deleteApiKey(@Param() params) {
await this.authService.deleteApiKey(params.id)
return { message: 'API Key Deleted' }
}
}

23
api/src/auth/auth.dto.ts

@ -0,0 +1,23 @@
import { ApiProperty } from '@nestjs/swagger'
export class RegisterInputDTO {
@ApiProperty({ type: String, required: true })
name: string
@ApiProperty({ type: String, required: true })
email: string
@ApiProperty({ type: String })
primaryPhone?: string
@ApiProperty({ type: String, required: true })
password: string
}
export class LoginInputDTO {
@ApiProperty({ type: String, required: true })
email: string
@ApiProperty({ type: String, required: true })
password: string
}

57
api/src/auth/auth.guard.ts

@ -0,0 +1,57 @@
import {
CanActivate,
ExecutionContext,
HttpException,
HttpStatus,
Injectable,
} from '@nestjs/common'
import { JwtService } from '@nestjs/jwt'
import { UsersService } from 'src/users/users.service'
import { AuthService } from './auth.service'
import * as bcrypt from 'bcryptjs'
@Injectable()
// Guard for authenticating users by either jwt token or api key
export class AuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private usersService: UsersService,
private authService: AuthService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
var userId
const request = context.switchToHttp().getRequest()
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
if (apiKeyStr) {
var regex = new RegExp(`^${apiKeyStr.substr(0, 17)}`, 'g')
const apiKey = await this.authService.findApiKeys({
apiKey: { $regex: regex },
})
if (apiKey && bcrypt.compareSync(apiKeyStr, apiKey.hashedApiKey)) {
userId = apiKey.user
}
}
}
if (userId) {
const user = await this.authService.validateUser(userId)
if (user) {
request.user = user
return true
}
}
throw new HttpException({ error: 'Unauthorized' }, HttpStatus.UNAUTHORIZED)
}
}

30
api/src/auth/auth.module.ts

@ -0,0 +1,30 @@
import { Module } from '@nestjs/common'
import { JwtModule } from '@nestjs/jwt'
import { MongooseModule } from '@nestjs/mongoose'
import { PassportModule } from '@nestjs/passport'
import { UsersModule } from 'src/users/users.module'
import { AuthController } from './auth.controller'
import { AuthService } from './auth.service'
import { JwtStrategy } from './jwt.strategy'
import { ApiKey, ApiKeySchema } from './schemas/api-key.schema'
@Module({
imports: [
MongooseModule.forFeature([
{
name: ApiKey.name,
schema: ApiKeySchema,
},
]),
UsersModule,
PassportModule,
JwtModule.register({
secret: process.env.JWT_SECRET,
signOptions: { expiresIn: '30d' },
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy, MongooseModule],
exports: [AuthService, JwtModule],
})
export class AuthModule {}

18
api/src/auth/auth.service.spec.ts

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'
import { AuthService } from './auth.service'
describe('AuthService', () => {
let service: AuthService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [AuthService],
}).compile()
service = module.get<AuthService>(AuthService)
})
it('should be defined', () => {
expect(service).toBeDefined()
})
})

100
api/src/auth/auth.service.ts

@ -0,0 +1,100 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'
import { UsersService } from 'src/users/users.service'
import { JwtService } from '@nestjs/jwt'
import * as bcrypt from 'bcryptjs'
import { v4 as uuidv4 } from 'uuid'
import { InjectModel } from '@nestjs/mongoose'
import { ApiKey, ApiKeyDocument } from './schemas/api-key.schema'
import { Model } from 'mongoose'
import { User } from 'src/users/schemas/user.schema'
@Injectable()
export class AuthService {
constructor(
private usersService: UsersService,
private jwtService: JwtService,
@InjectModel(ApiKey.name) private apiKeyModel: Model<ApiKeyDocument>,
) {}
async validateUser(_id: string): Promise<User | null> {
const user = await this.usersService.findOne({ _id })
if (user) {
return user
}
return null
}
async login(userData: any) {
const user = await this.usersService.findOne({ email: userData.email })
if (!user) {
throw new HttpException(
{ error: 'User not found' },
HttpStatus.UNAUTHORIZED,
)
}
if (!(await bcrypt.compare(userData.password, user.password))) {
throw new HttpException(
{ error: 'Invalid credentials' },
HttpStatus.UNAUTHORIZED,
)
}
const payload = { email: user.email, sub: user._id }
return {
accessToken: this.jwtService.sign(payload),
user,
}
}
async register(userData: any) {
const hashedPassword = await bcrypt.hash(userData.password, 10)
const user = await this.usersService.create({
...userData,
password: hashedPassword,
})
const payload = { email: user.email, sub: user._id }
return {
accessToken: this.jwtService.sign(payload),
user,
}
}
async generateApiKey(currentUser: User) {
const apiKey = uuidv4()
const hashedApiKey = await bcrypt.hash(apiKey, 10)
const newApiKey = new this.apiKeyModel({
apiKey: apiKey.substr(0, 17) + '******************',
hashedApiKey,
user: currentUser._id,
})
await newApiKey.save()
return { apiKey, message: 'Save this key, it wont be shown again ;)' }
}
async getUserApiKeys(currentUser: User) {
return this.apiKeyModel.find({ user: currentUser._id })
}
async findApiKeys(params) {
return this.apiKeyModel.findOne(params)
}
async deleteApiKey(apiKeyId: string) {
const apiKey = await this.apiKeyModel.findOne({ _id: apiKeyId })
if (!apiKey) {
throw new HttpException(
{
error: 'Api key not found',
},
HttpStatus.NOT_FOUND,
)
}
await this.apiKeyModel.deleteOne({ _id: apiKeyId })
}
}

5
api/src/auth/jwt-auth.guard.ts

@ -0,0 +1,5 @@
import { Injectable } from '@nestjs/common'
import { AuthGuard } from '@nestjs/passport'
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

26
api/src/auth/jwt.strategy.ts

@ -0,0 +1,26 @@
import { ExtractJwt, Strategy } from 'passport-jwt'
import { PassportStrategy } from '@nestjs/passport'
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'
import { UsersService } from 'src/users/users.service'
import { User } from 'src/users/schemas/user.schema'
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(private usersService: UsersService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: process.env.JWT_SECRET,
})
}
async validate(payload: any): Promise<User> {
const userId = payload.sub
const user = await this.usersService.findOne({ _id: userId })
if (!user) {
throw new HttpException('Unauthorized', HttpStatus.UNAUTHORIZED)
} else {
return user
}
}
}

21
api/src/auth/schemas/api-key.schema.ts

@ -0,0 +1,21 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { Document, Types } from 'mongoose'
import { User } from 'src/users/schemas/user.schema'
export type ApiKeyDocument = ApiKey & Document
@Schema({ timestamps: true })
export class ApiKey {
_id?: Types.ObjectId
@Prop({ type: String })
apiKey: string // save first few chars only [ abc123****** ]
@Prop({ type: String })
hashedApiKey: string
@Prop({ type: Types.ObjectId, ref: User.name })
user: User
}
export const ApiKeySchema = SchemaFactory.createForClass(ApiKey)

18
api/src/gateway/gateway.controller.spec.ts

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'
import { GatewayController } from './gateway.controller'
describe('GatewayController', () => {
let controller: GatewayController
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [GatewayController],
}).compile()
controller = module.get<GatewayController>(GatewayController)
})
it('should be defined', () => {
expect(controller).toBeDefined()
})
})

63
api/src/gateway/gateway.controller.ts

@ -0,0 +1,63 @@
import {
Body,
Controller,
Param,
Patch,
Post,
UseGuards,
Request,
} from '@nestjs/common'
import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger'
import { AuthGuard } from 'src/auth/auth.guard'
import { RegisterDeviceInputDTO, SendSMSInputDTO } from './gateway.dto'
import { GatewayService } from './gateway.service'
@ApiTags('gateway')
@ApiBearerAuth()
@Controller('gateway')
export class GatewayController {
constructor(private readonly gatewayService: GatewayService) {}
@UseGuards(AuthGuard)
@ApiOperation({ summary: 'Register device' })
@ApiQuery({
name: 'apiKey',
required: false,
description: 'Required if jwt bearer token not provided',
})
@Post('/devices')
async registerDevice(@Body() input: RegisterDeviceInputDTO, @Request() req) {
const data = await this.gatewayService.registerDevice(input, req.user)
return { data }
}
@ApiOperation({ summary: 'Update device' })
@ApiQuery({
name: 'apiKey',
required: false,
description: 'Required if jwt bearer token not provided',
})
@Patch('/devices/:id')
async updateDevice(
@Param('id') deviceId: string,
@Body() input: RegisterDeviceInputDTO,
) {
const data = await this.gatewayService.updateDevice(deviceId, input)
return { data }
}
@ApiOperation({ summary: 'Send SMS to a device' })
@ApiQuery({
name: 'apiKey',
required: false,
description: 'Required if jwt bearer token not provided',
})
@Post('/devices/:id/sendSMS')
async sendSMS(
@Param('id') deviceId: string,
@Body() smsData: SendSMSInputDTO,
) {
const data = await this.gatewayService.sendSMS(deviceId, smsData)
return { data }
}
}

54
api/src/gateway/gateway.dto.ts

@ -0,0 +1,54 @@
import { ApiProperty } from '@nestjs/swagger'
export class RegisterDeviceInputDTO {
@ApiProperty({ type: Boolean })
enabled?: boolean
@ApiProperty({ type: String })
fcmToken?: string
@ApiProperty({ type: String })
brand?: string
@ApiProperty({ type: String })
manufacturer?: string
@ApiProperty({ type: String })
model?: string
@ApiProperty({ type: String })
serial?: string
@ApiProperty({ type: String })
buildId?: string
@ApiProperty({ type: String })
os?: string
@ApiProperty({ type: String })
osVersion?: string
@ApiProperty({ type: String })
appVersionName?: string
@ApiProperty({ type: String })
appVersionCode?: number
}
export class ISMSData {
@ApiProperty({
type: String,
required: true,
description: 'SMS text',
})
smsBody: string
@ApiProperty({
type: Array,
required: true,
description: 'Array of phone numbers',
example: ['+2519xxxxxxxx', '+2517xxxxxxxx'],
})
receivers: string[]
}
export class SendSMSInputDTO extends ISMSData {}

24
api/src/gateway/gateway.module.ts

@ -0,0 +1,24 @@
import { Module } from '@nestjs/common'
import { MongooseModule } from '@nestjs/mongoose'
import { Device, DeviceSchema } from './schemas/device.schema'
import { GatewayController } from './gateway.controller'
import { GatewayService } from './gateway.service'
import { AuthModule } from 'src/auth/auth.module'
import { UsersModule } from 'src/users/users.module'
@Module({
imports: [
MongooseModule.forFeature([
{
name: Device.name,
schema: DeviceSchema,
},
]),
AuthModule,
UsersModule,
],
controllers: [GatewayController],
providers: [GatewayService],
exports: [MongooseModule, GatewayService],
})
export class GatewayModule {}

18
api/src/gateway/gateway.service.spec.ts

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'
import { GatewayService } from './gateway.service'
describe('GatewayService', () => {
let service: GatewayService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [GatewayService],
}).compile()
service = module.get<GatewayService>(GatewayService)
})
it('should be defined', () => {
expect(service).toBeDefined()
})
})

88
api/src/gateway/gateway.service.ts

@ -0,0 +1,88 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'
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 { User } from 'src/users/schemas/user.schema'
@Injectable()
export class GatewayService {
constructor(
@InjectModel(Device.name) private deviceModel: Model<DeviceDocument>,
) {}
async registerDevice(
input: RegisterDeviceInputDTO,
user: User,
): Promise<any> {
return await this.deviceModel.create({ ...input, user })
}
async updateDevice(
deviceId: string,
input: RegisterDeviceInputDTO,
): Promise<any> {
const device = await this.deviceModel.findById(deviceId)
if (!device) {
throw new HttpException(
{
error: 'Device not found',
},
HttpStatus.NOT_FOUND,
)
}
return await this.deviceModel.findByIdAndUpdate(
deviceId,
{ $set: input },
{ new: true },
)
}
async sendSMS(deviceId: string, smsData: SendSMSInputDTO): Promise<any> {
const device = await this.deviceModel.findById(deviceId)
if (!device) {
throw new HttpException(
{
error: 'Device not found',
},
HttpStatus.NOT_FOUND,
)
}
if (!device.enabled) {
throw new HttpException(
{
success: false,
error: 'Device is disabled',
},
HttpStatus.BAD_REQUEST,
)
}
const payload: any = {
// notification: {
// title: 'SMS',
// body: 'message',
// },
data: {
smsData: JSON.stringify(smsData),
},
}
try {
const response = await firebaseAdmin
.messaging()
.sendToDevice(device.fcmToken, payload, { priority: 'high' })
return response
} catch (e) {
throw new HttpException(
{
error: 'Failed to send SMS',
},
HttpStatus.BAD_REQUEST,
)
}
}
}

48
api/src/gateway/schemas/device.schema.ts

@ -0,0 +1,48 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { Document, Types } from 'mongoose'
import { User } from 'src/users/schemas/user.schema'
export type DeviceDocument = Device & Document
@Schema({ timestamps: true })
export class Device {
_id?: Types.ObjectId
@Prop({ type: Types.ObjectId, ref: User.name })
user: User
@Prop({ type: Boolean, default: false })
enabled: boolean
@Prop({ type: String })
fcmToken: string
@Prop({ type: String })
brand: string
@Prop({ type: String })
manufacturer: string
@Prop({ type: String })
model: string
@Prop({ type: String })
serial: string
@Prop({ type: String })
buildId: string
@Prop({ type: String })
os: string
@Prop({ type: String })
osVersion: string
@Prop({ type: String })
appVersionName: string
@Prop({ type: Number })
appVersionCode: number
}
export const DeviceSchema = SchemaFactory.createForClass(Device)

23
api/src/gateway/schemas/sms.schema.ts

@ -0,0 +1,23 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { Document, Types } from 'mongoose'
import { ApiKey } from 'src/auth/schemas/api-key.schema'
import { User } from 'src/users/schemas/user.schema'
import { Device } from './device.schema'
export type SMSDocument = SMS & Document
@Schema({ timestamps: true })
export class SMS {
_id?: Types.ObjectId
@Prop({ type: Types.ObjectId, ref: Device.name })
device: Device
@Prop({ type: String, required: true })
message: string
@Prop({ type: String, required: true })
to: string
}
export const SMSSchema = SchemaFactory.createForClass(SMS)

51
api/src/main.ts

@ -0,0 +1,51 @@
import 'dotenv/config'
import { VersioningType } from '@nestjs/common'
import { NestFactory } from '@nestjs/core'
import { AppModule } from './app.module'
import * as firebase from 'firebase-admin'
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger'
async function bootstrap() {
const app = await NestFactory.create(AppModule)
const PORT = process.env.PORT || 3005
app.setGlobalPrefix('api')
app.enableVersioning({
defaultVersion: '1',
type: VersioningType.URI,
})
const config = new DocumentBuilder()
.setTitle('VERNU SMS Gateway api docs')
.setDescription('api docs')
.setVersion('1.0')
.addBearerAuth()
.build()
const document = SwaggerModule.createDocument(app, config)
SwaggerModule.setup('', app, document, {
swaggerOptions: {
persistAuthorization: true,
},
})
const firebaseConfig = {
type: 'service_account',
projectId: process.env.FIREBASE_PROJECT_ID,
privateKeyId: process.env.FIREBASE_PRIVATE_KEY_ID,
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
clientId: process.env.FIREBASE_CLIENT_ID,
authUri: 'https://accounts.google.com/o/oauth2/auth',
tokenUri: 'https://oauth2.googleapis.com/token',
authProviderX509CertUrl: 'https://www.googleapis.com/oauth2/v1/certs',
clientC509CertUrl: process.env.FIREBASE_CLIENT_C509_CERT_URL,
}
firebase.initializeApp({
credential: firebase.credential.cert(firebaseConfig),
})
app.enableCors()
await app.listen(PORT)
}
bootstrap()

27
api/src/users/schemas/user.schema.ts

@ -0,0 +1,27 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { Document, Types } from 'mongoose'
import { UserRole } from '../user-roles.enum'
export type UserDocument = User & Document
@Schema({ timestamps: true })
export class User {
_id?: Types.ObjectId
@Prop({ type: String })
name: string
@Prop({ type: String, required: true, unique: true, lowercase: true })
email: string
@Prop({ type: String, trim: true })
primaryPhone: string
@Prop({ type: String, required: true })
password: string
@Prop({ type: String, default: UserRole.REGULAR })
role: string
}
export const UserSchema = SchemaFactory.createForClass(User)

4
api/src/users/user-roles.enum.ts

@ -0,0 +1,4 @@
export enum UserRole {
ADMIN = 'ADMIN',
REGULAR = 'REGULAR',
}

18
api/src/users/users.controller.spec.ts

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'
import { UsersController } from './users.controller'
describe('UsersController', () => {
let controller: UsersController
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
controllers: [UsersController],
}).compile()
controller = module.get<UsersController>(UsersController)
})
it('should be defined', () => {
expect(controller).toBeDefined()
})
})

4
api/src/users/users.controller.ts

@ -0,0 +1,4 @@
import { Controller } from '@nestjs/common'
@Controller('users')
export class UsersController {}

20
api/src/users/users.module.ts

@ -0,0 +1,20 @@
import { Module } from '@nestjs/common'
import { MongooseModule } from '@nestjs/mongoose'
import { User, UserSchema } from './schemas/user.schema'
import { UsersController } from './users.controller'
import { UsersService } from './users.service'
@Module({
imports: [
MongooseModule.forFeature([
{
name: User.name,
schema: UserSchema,
},
]),
],
controllers: [UsersController],
providers: [UsersService],
exports: [MongooseModule, UsersService],
})
export class UsersModule {}

18
api/src/users/users.service.spec.ts

@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing'
import { UsersService } from './users.service'
describe('UsersService', () => {
let service: UsersService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [UsersService],
}).compile()
service = module.get<UsersService>(UsersService)
})
it('should be defined', () => {
expect(service).toBeDefined()
})
})

36
api/src/users/users.service.ts

@ -0,0 +1,36 @@
import { HttpException, HttpStatus, Injectable } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'
import { User, UserDocument } from './schemas/user.schema'
import { Model } from 'mongoose'
@Injectable()
export class UsersService {
constructor(@InjectModel(User.name) private userModel: Model<UserDocument>) {}
async findOne(params) {
return await this.userModel.findOne(params)
}
async findAll() {
return await this.userModel.find()
}
async create(userData: any) {
const { name, email, password } = userData
if (await this.findOne({ email })) {
throw new HttpException(
{
error: 'user exists with the same email',
},
HttpStatus.BAD_REQUEST,
)
}
const newUser = new this.userModel({
name,
email,
password,
})
return await newUser.save()
}
}

24
api/test/app.e2e-spec.ts

@ -0,0 +1,24 @@
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication } from '@nestjs/common'
import * as request from 'supertest'
import { AppModule } from './../src/app.module'
describe('AppController (e2e)', () => {
let app: INestApplication
beforeEach(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
}).compile()
app = moduleFixture.createNestApplication()
await app.init()
})
it('/ (GET)', () => {
return request(app.getHttpServer())
.get('/')
.expect(200)
.expect('Hello World!')
})
})

9
api/test/jest-e2e.json

@ -0,0 +1,9 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".e2e-spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

4
api/tsconfig.build.json

@ -0,0 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

21
api/tsconfig.json

@ -0,0 +1,21 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "es2017",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
}
}

3
web/.eslintrc.json

@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}

38
web/.gitignore

@ -0,0 +1,38 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
.env

34
web/README.md

@ -0,0 +1,34 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file.
[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`.
The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.

118
web/components/Navbar.tsx

@ -0,0 +1,118 @@
import {
Box,
Flex,
Avatar,
Button,
Menu,
MenuButton,
MenuList,
MenuItem,
MenuDivider,
useColorModeValue,
Stack,
useColorMode,
Center,
Image,
} from '@chakra-ui/react'
import Link from 'next/link'
import { MoonIcon, SunIcon } from '@chakra-ui/icons'
import Router from 'next/router'
import { useDispatch, useSelector } from 'react-redux'
import { logout, selectAuth } from '../store/authSlice'
export default function Navbar() {
const dispatch = useDispatch()
const { colorMode, toggleColorMode } = useColorMode()
const { user } = useSelector(selectAuth)
return (
<>
<Box bg={useColorModeValue('gray.100', 'gray.900')} px={4}>
<Flex h={16} alignItems={'center'} justifyContent={'space-between'}>
<Link href='/' passHref>
<Flex alignItems={'center'}>
<Image
alt={'Hero Image'}
fit={'cover'}
w={'30px'}
h={'30px'}
src={'/images/sms-gateway-logo.png'}
/>
<Box style={{ cursor: 'pointer', marginLeft: '5px' }}>
VERNU SMS
</Box>
</Flex>
</Link>
<Flex alignItems={'center'}>
<Stack direction={'row'} spacing={7}>
<Button onClick={toggleColorMode}>
{colorMode === 'light' ? <MoonIcon /> : <SunIcon />}
</Button>
{!user ? (
<>
<Menu>
<Link href='/login' passHref>
<MenuButton>Login</MenuButton>
</Link>
<Link href='/register' passHref>
<MenuButton>Register</MenuButton>
</Link>
</Menu>
</>
) : (
<Menu>
<MenuButton
as={Button}
rounded={'full'}
variant={'link'}
cursor={'pointer'}
minW={0}
>
<Avatar
size={'sm'}
src={'https://avatars.dicebear.com/api/male/username.svg'}
/>
</MenuButton>
<MenuList alignItems={'center'}>
<br />
<Center>
<Avatar
size={'xl'}
src={
'https://avatars.dicebear.com/api/male/username.svg'
}
/>
</Center>
<br />
<Center>
<p>{user?.name}</p>
</Center>
<br />
<MenuDivider />
<MenuItem
onClick={() => {
Router.push('/dashboard')
}}
>
Dashboard
</MenuItem>
<MenuItem>Account Settings</MenuItem>
<MenuItem
onClick={() => {
dispatch(logout())
}}
>
Logout
</MenuItem>
</MenuList>
</Menu>
)}
</Stack>
</Flex>
</Flex>
</Box>
</>
)
}

72
web/components/dashboard/ApiKeyList.tsx

@ -0,0 +1,72 @@
import { DeleteIcon } from '@chakra-ui/icons'
import {
Table,
TableContainer,
Tbody,
Td,
Th,
Thead,
Tooltip,
Tr,
useToast,
} from '@chakra-ui/react'
import { useEffect, useState } from 'react'
import { useSelector } from 'react-redux'
import { deleteApiKeyRequest, getApiKeyListRequest } from '../../services'
import { selectAuth } from '../../store/authSlice'
const ApiKeyList = () => {
const [apiKeyList, setApiKeyList] = useState([])
const toast = useToast()
const { user, accessToken } = useSelector(selectAuth)
useEffect(() => {
if (user && accessToken) {
getApiKeyListRequest().then((apiKeys) => {
setApiKeyList(apiKeys)
})
}
}, [user, accessToken])
const onDelete = (apiKeyId: string) => {
deleteApiKeyRequest(apiKeyId)
setApiKeyList(apiKeyList.filter((apiKey) => apiKey._id !== apiKeyId))
toast({
title: 'Success',
description: 'API Key deleted',
})
}
return (
<TableContainer>
<Table variant='simple'>
<Thead>
<Tr>
<Th>Your API Keys</Th>
<Th>Status</Th>
<Th></Th>
</Tr>
</Thead>
<Tbody>
{apiKeyList.map((apiKey) => (
<Tr key={apiKey}>
<Td>{apiKey.apiKey}</Td>
<Td>{apiKey.status}</Td>
<Td>
<Tooltip label='Double Click to delete'>
<DeleteIcon
onDoubleClick={(e) => {
onDelete(apiKey._id)
}}
/>
</Tooltip>
</Td>
</Tr>
))}
</Tbody>
</Table>
</TableContainer>
)
}
export default ApiKeyList

154
web/components/dashboard/GenerateApiKey.tsx

@ -0,0 +1,154 @@
import {
Button,
chakra,
Flex,
Modal,
ModalBody,
ModalCloseButton,
ModalContent,
ModalFooter,
ModalHeader,
ModalOverlay,
useColorModeValue,
useToast,
} from '@chakra-ui/react'
import { useState } from 'react'
import QRCode from 'react-qr-code'
import { generateApiKeyRequest } from '../../services'
const NewApiKeyGeneratedModal = ({
isOpen = false,
generatedApiKey,
onClose,
showQR = false,
...props
}) => {
const toast = useToast()
return (
<Modal isOpen={isOpen} onClose={onClose}>
<ModalOverlay />
<ModalContent>
<ModalHeader>Api Key Generated</ModalHeader>
<ModalCloseButton />
<ModalBody>
{showQR && (
<>
<chakra.h1
fontSize='md'
fontWeight='bold'
mt={2}
// color={useColorModeValue('gray.800', 'white')}
>
Open the SMS Gateway App and scan this QR to get started
</chakra.h1>
<Flex justifyContent='center'>
<QRCode value={generatedApiKey} />{' '}
</Flex>
</>
)}
<chakra.h1
fontSize='lg'
fontWeight='bold'
mt={2}
// color={useColorModeValue('gray.800', 'white')}
>
{generatedApiKey}
</chakra.h1>
<chakra.h1
fontSize='lg'
fontWeight='bold'
mt={2}
color={useColorModeValue('red.800', 'white')}
>
{'Save this key, it wont be shown again ;)'}
</chakra.h1>
</ModalBody>
<ModalFooter>
<Button
variant='ghost'
onClick={() => {
navigator.clipboard.writeText(generatedApiKey)
toast({
title: 'Copied to clipboard',
status: 'success',
})
}}
>
Copy to Clipboard
</Button>{' '}
<Button
colorScheme='blue'
mr={3}
onClick={() => {
onClose()
}}
>
Close
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}
export default function GenerateApiKey() {
const [generatedApiKey, setGeneratedApiKey] = useState(null)
const [generatingApiKey, setGeneratingApiKey] = useState(null)
const [showGeneratedApiKeyModal, setShowGeneratedApiKeyModal] =
useState(false)
const generateApiKey = async () => {
setGeneratingApiKey(true)
const newApiKey = await generateApiKeyRequest()
setGeneratedApiKey(newApiKey)
setShowGeneratedApiKeyModal(true)
setGeneratingApiKey(false)
}
return (
<>
{' '}
<Flex justifyContent='center'>
<Button
/* flex={1} */
px={4}
fontSize={'sm'}
rounded={'full'}
bg={'blue.400'}
color={'white'}
boxShadow={
'0px 1px 25px -5px rgb(66 153 225 / 48%), 0 10px 10px -5px rgb(66 153 225 / 43%)'
}
_hover={{
bg: 'blue.500',
}}
_focus={{
bg: 'blue.500',
}}
onClick={generateApiKey}
disabled={generatingApiKey}
>
{generatingApiKey
? 'generating... '
: 'Generate Api Key/ Register Device'}
</Button>
</Flex>
{generatedApiKey && (
<>
{
<NewApiKeyGeneratedModal
isOpen={showGeneratedApiKeyModal}
generatedApiKey={generatedApiKey}
showQR={true}
onClose={() => {
setShowGeneratedApiKeyModal(false)
}}
/>
}
</>
)}
</>
)
}

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save