├── .env.example ├── .prettierignore ├── .eslintignore ├── .prettierrc ├── .eslintrc.json ├── .dockerignore ├── src ├── common │ └── http-exception.ts ├── index.ts ├── middleware │ ├── notFound.middleware.ts │ └── error.middleware.ts ├── utils │ └── app.ts ├── routes │ ├── auth.routes.ts │ ├── user.routes.ts │ └── vehicle.routes.ts └── controllers │ ├── auth.controller.ts │ ├── user.controller.ts │ └── vehicle.controller.ts ├── Dockerfile ├── .github ├── workflows │ ├── docker-publish.yml │ └── serverless-deploy.yml ├── feature_request.md └── bug_report.md ├── tsconfig.json ├── .gitignore ├── README.md ├── serverless.yml └── package.json /.env.example: -------------------------------------------------------------------------------- 1 | NODE_ENV=local 2 | PORT=3000 3 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | **/.next 3 | **/.serverless 4 | **/.serverless_nextjs -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .serverless 3 | .vscode 4 | *.config.js 5 | .webpack 6 | _warmup 7 | **/*.js 8 | dist -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 2, 4 | "semi": false, 5 | "singleQuote": true 6 | } 7 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["airbnb-base", "airbnb-typescript/base", "prettier"], 3 | "parserOptions": { 4 | "project": "./tsconfig.json" 5 | } 6 | } -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | npm-debug.log 3 | .env.example 4 | .eslintcache 5 | .eslintignore 6 | .eslintrc 7 | .gitignore 8 | .prettierignore 9 | .prettierrc 10 | README.md 11 | serverless.yml 12 | .serverless 13 | dist -------------------------------------------------------------------------------- /src/common/http-exception.ts: -------------------------------------------------------------------------------- 1 | export default class HttpException extends Error { 2 | public statusCode?: number 3 | 4 | public status?: number 5 | 6 | public message: string 7 | 8 | constructor(statusCode: number, message: string) { 9 | super(message) 10 | this.statusCode = statusCode 11 | this.message = message 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import dotenv from 'dotenv' 2 | import serverless from 'serverless-http' 3 | import app from './utils/app' 4 | 5 | dotenv.config() 6 | 7 | const { NODE_ENV, PORT } = process.env 8 | 9 | if (NODE_ENV === 'local' || !NODE_ENV) { 10 | app.listen(PORT || 3000, () => { 11 | console.log(`Listening at http://localhost:${PORT || 3000}`) 12 | }) 13 | } 14 | 15 | export default serverless(app) 16 | -------------------------------------------------------------------------------- /src/middleware/notFound.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express' 2 | 3 | const notFoundHandler = ( 4 | request: Request, 5 | response: Response, 6 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 7 | next: NextFunction 8 | ): void => { 9 | const error = 'NOT_FOUND' 10 | const message = 'The request resource could not be found.' 11 | 12 | response.status(404).json({ 13 | status: 404, 14 | error, 15 | message, 16 | }) 17 | } 18 | 19 | export default notFoundHandler 20 | -------------------------------------------------------------------------------- /src/middleware/error.middleware.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response, NextFunction } from 'express' 2 | import HttpException from '../common/http-exception' 3 | 4 | const errorHandler = ( 5 | error: HttpException, 6 | request: Request, 7 | response: Response, 8 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 9 | next: NextFunction 10 | ): void => { 11 | const status = error.statusCode || error.status || 500 12 | response.status(status).send({ 13 | status, 14 | message: error.message, 15 | }) 16 | } 17 | 18 | export default errorHandler 19 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:latest 2 | 3 | # Set working directory 4 | WORKDIR /app 5 | 6 | # Copy package.json 7 | COPY package*.json ./ 8 | 9 | # Install dependencies 10 | RUN npm install 11 | 12 | # Copy application files 13 | COPY . . 14 | 15 | # Build the application 16 | RUN npm run build 17 | 18 | # Remove all files except distribution 19 | RUN find -maxdepth 1 ! -name dist ! -name node_modules ! -name . -exec rm -rv {} \; 20 | 21 | # CD into the dist directory 22 | WORKDIR /app/dist 23 | 24 | # Set NODE_ENV to local 25 | ENV NODE_ENV=local 26 | 27 | # Set PORT to 3000 28 | ENV PORT=3000 29 | 30 | # Start the application 31 | CMD node . -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v2 12 | - name: docker login 13 | env: 14 | DOCKER_USER: ${{secrets.DOCKER_USER}} 15 | DOCKER_PASSWORD: ${{secrets.DOCKER_PASSWORD}} 16 | run: | 17 | docker login -u $DOCKER_USER -p $DOCKER_PASSWORD 18 | - name: Build the Docker image 19 | run: docker build -t ianjwhite99/connected-car-docker-api . 20 | - name: Docker Push 21 | run: docker push ianjwhite99/connected-car-docker-api 22 | -------------------------------------------------------------------------------- /.github/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** A clear and concise description 10 | of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** A clear and concise description of what you want to happen. 13 | 14 | **Describe alternatives you've considered** A clear and concise description of any alternative 15 | solutions or features you've considered. 16 | 17 | **Additional context** Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /src/utils/app.ts: -------------------------------------------------------------------------------- 1 | import cors from 'cors' 2 | import helmet from 'helmet' 3 | import express from 'express' 4 | import authRoutes from '../routes/auth.routes' 5 | import userRoutes from '../routes/user.routes' 6 | import vehicleRoutes from '../routes/vehicle.routes' 7 | import errorHandler from '../middleware/error.middleware' 8 | import notFoundHandler from '../middleware/notFound.middleware' 9 | 10 | const app = express() 11 | 12 | app.use(helmet()) 13 | app.use(cors()) 14 | app.use(express.json()) 15 | app.use('/auth', authRoutes) 16 | app.use('/user', userRoutes) 17 | app.use('/vehicle', vehicleRoutes) 18 | 19 | app.use(errorHandler) 20 | app.use(notFoundHandler) 21 | 22 | export default app 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildOnSave": false, 3 | "compileOnSave": true, 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "sourceMap": true, 7 | "target": "es6", 8 | "module": "commonjs", 9 | "outDir": "./dist/", 10 | "allowJs": true, 11 | "pretty": true, 12 | "skipLibCheck": true, 13 | "importHelpers": false, 14 | "esModuleInterop": true, 15 | "resolveJsonModule": true, 16 | "declaration": true, 17 | "lib": ["es2020"], 18 | "moduleResolution": "node", 19 | "noUnusedParameters": false, 20 | "allowSyntheticDefaultImports": true, 21 | "strict": true, 22 | "strictFunctionTypes": false, 23 | "noUnusedLocals": true, 24 | "suppressImplicitAnyIndexErrors": true 25 | }, 26 | "include": ["src/**/*"] 27 | } 28 | -------------------------------------------------------------------------------- /.github/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | --- 8 | 9 | **Describe the bug** A clear and concise description of what the bug is. 10 | 11 | **To Reproduce** Steps to reproduce the behavior: 12 | 13 | 1. Go to '...' 14 | 2. Click on '....' 15 | 3. Scroll down to '....' 16 | 4. See error 17 | 18 | **Expected behavior** A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** If applicable, add screenshots to help explain your problem. 21 | 22 | **Desktop (please complete the following information):** 23 | 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Smartphone (please complete the following information):** 29 | 30 | - Device: [e.g. iPhone6] 31 | - OS: [e.g. iOS8.1] 32 | - Browser [e.g. stock browser, safari] 33 | - Version [e.g. 22] 34 | 35 | **Additional context** Add any other context about the problem here. 36 | -------------------------------------------------------------------------------- /.github/workflows/serverless-deploy.yml: -------------------------------------------------------------------------------- 1 | name: Serverless CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | deploy: 9 | name: deploy 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node-version: [16.x] 15 | 16 | steps: 17 | - uses: actions/checkout@v3 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | 23 | - name: Make envfile 24 | uses: SpicyPizza/create-envfile@v1.3 25 | with: 26 | envkey_NODE_ENV: 'production' 27 | envkey_PORT: 3000 28 | 29 | - name: Install Dependencies 30 | run: yarn install 31 | 32 | - name: serverless deploy 33 | uses: serverless/github-action@v3.1 34 | with: 35 | args: -c "serverless deploy" 36 | entrypoint: /bin/sh 37 | env: 38 | AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} 39 | AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Lock files 2 | package-lock.json 3 | yarn.lock 4 | 5 | # Environment variables 6 | .env 7 | 8 | # Logs 9 | logs 10 | *.log 11 | npm-debug.log* 12 | 13 | # Documentation 14 | docs 15 | 16 | # Runtime data 17 | pids 18 | *.pid 19 | *.seed 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # node-waf configuration 34 | .lock-wscript 35 | 36 | # Compiled binary addons (http://nodejs.org/api/addons.html) 37 | build/Release 38 | dist 39 | .build 40 | 41 | # Dependency directories 42 | node_modules 43 | jspm_packages 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Serverless 52 | .serverless 53 | 54 | # misc 55 | .DS_Store 56 | .webpackCache 57 | .eslintcache 58 | package-lock.json 59 | yarn.lock 60 | 61 | # Visual Studio code 62 | .vscode 63 | 64 | # Serverless 65 | .serverless 66 | 67 | # Distribution 68 | dist -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # connected-car-node-api 2 | 3 | ExpressJS API built on top of the [connected-car-node-sdk](https://github.com/ianjwhite99/connected-car-node-sdk). This API is designed to act as a middleman between the client and FordPass API. This application can be deployed using [serverless](https://serverless.com/) or [docker](https://www.docker.com/). The latest docker image is available on docker hub in the [connected-car-docker-api](https://hub.docker.com/r/ianjwhite99/connected-car-docker-api/) repository. 4 | 5 | ## Getting Started 6 | 7 | Setting up the local environment is as simple as cloning this repository and running the following commands: 8 | 9 | ```bash 10 | npm install 11 | npm run dev:start 12 | ``` 13 | 14 | This will start a local development server on port 3000. 15 | 16 | ## Docker Build 17 | 18 | If you want to build a custom docker image for a special use case, you can use do the following: 19 | 20 | Building the docker image: 21 | 22 | ```bash 23 | docker build -t connected-car-docker-api . 24 | ``` 25 | 26 | Running the docker image: 27 | 28 | ```bash 29 | docker run -p 8080:3000 -d connected-car-docker-api 30 | ``` 31 | 32 | ## Disclaimer 33 | 34 | THIS CODEBASE IS NOT ENDORSED, AFFILIATED, OR ASSOCIATED WITH FORD, FOMOCO OR THE FORD MOTOR COMPANY. 35 | -------------------------------------------------------------------------------- /src/routes/auth.routes.ts: -------------------------------------------------------------------------------- 1 | import express, { NextFunction, Request, Response } from 'express' 2 | import HttpException from '../common/http-exception' 3 | import { login, exchangeRefreshToken } from '../controllers/auth.controller' 4 | 5 | const authRouter = express.Router() 6 | 7 | /** 8 | * @api {post} /auth/fetchToken Fetch Login Token 9 | */ 10 | authRouter.post( 11 | '/fetchToken', 12 | async (req: Request, res: Response, next: NextFunction) => { 13 | const { email, password } = req.body 14 | if (email && password) { 15 | login(email, password) 16 | .then((result) => { 17 | res.status(200).json(result) 18 | }) 19 | .catch((err: HttpException) => { 20 | next(err) 21 | }) 22 | } else next(new HttpException(400, 'Missing required email or password')) 23 | } 24 | ) 25 | 26 | /** 27 | * @api {post} /auth/refreshToken Fetch Access Token from Refresh Token 28 | */ 29 | authRouter.post( 30 | '/refreshToken', 31 | async (req: Request, res: Response, next: NextFunction) => { 32 | const { refreshToken } = req.body 33 | if (refreshToken) { 34 | exchangeRefreshToken(refreshToken) 35 | .then((result) => { 36 | res.status(200).json(result) 37 | }) 38 | .catch((err: HttpException) => { 39 | next(err) 40 | }) 41 | } else next(new HttpException(400, 'Missing required refresh token')) 42 | } 43 | ) 44 | 45 | export default authRouter 46 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | service: connectedcar-sdk-service 2 | frameworkVersion: '3' 3 | useDotenv: true 4 | 5 | provider: 6 | name: aws 7 | runtime: nodejs14.x 8 | lambdaHashingVersion: 20201221 9 | region: us-east-1 10 | stage: prod 11 | vpc: 12 | securityGroupIds: 13 | - sg-04e5dfe7ee46291e2 14 | subnetIds: 15 | - subnet-0610c3440d4ef0000 16 | - subnet-0f84eddf0ac228106 17 | - subnet-0d76000da2fe492e3 18 | - subnet-0d6d1bd0b880548c8 19 | - subnet-00434a825a38fafe9 20 | - subnet-0627547e0505d5981 21 | 22 | plugins: 23 | - serverless-offline 24 | - serverless-dotenv-plugin 25 | - serverless-domain-manager 26 | - serverless-bundle 27 | 28 | custom: 29 | customDomain: 30 | domainName: api.connectedcar-sdk.com 31 | basePath: '' 32 | stage: ${self:provider.stage} 33 | createRoute53Record: true 34 | 35 | functions: 36 | app: 37 | handler: src/index.default 38 | timeout: 30 39 | environment: 40 | NODE_ENV: production 41 | PORT: 3000 42 | events: 43 | - http: 44 | path: auth 45 | method: any 46 | cors: true 47 | - http: 48 | path: auth/{id} 49 | method: any 50 | cors: true 51 | - http: 52 | path: user 53 | method: any 54 | cors: true 55 | - http: 56 | path: user/{id} 57 | method: any 58 | cors: true 59 | - http: 60 | path: vehicle 61 | method: any 62 | cors: true 63 | - http: 64 | path: vehicle/{id} 65 | method: any 66 | cors: true 67 | -------------------------------------------------------------------------------- /src/controllers/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import connectedcar from 'connected-car' 2 | import HttpException from '../common/http-exception' 3 | 4 | const scClient = connectedcar.AuthClient('9fb503e0-715b-47e8-adfd-ad4b7770f73b') 5 | 6 | /** 7 | * Return user access token 8 | * @param username 9 | * @param password 10 | */ 11 | export const login = async (username: string, password: string): Promise => 12 | scClient 13 | .getAccessTokenFromCredentials({ username, password }) 14 | .then((token) => { 15 | if (token) { 16 | return { 17 | accessToken: token.getValue(), 18 | refreshToken: token.getRefreshToken(), 19 | expiresIn: token.getExpiresAt(), 20 | } 21 | } 22 | throw new HttpException( 23 | 500, 24 | 'An unknown error occured while fetching user access token' 25 | ) 26 | }) 27 | .catch((err) => { 28 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 29 | }) 30 | 31 | /** 32 | * Exchange refresh token for access token 33 | * @param refreshToken 34 | * @returns 35 | */ 36 | export const exchangeRefreshToken = async ( 37 | refreshToken: string 38 | ): Promise => 39 | scClient 40 | .getAccessTokenFromRefreshToken(refreshToken) 41 | .then((token) => { 42 | if (token) { 43 | return { 44 | accessToken: token.getValue(), 45 | refreshToken: token.getRefreshToken(), 46 | expiresIn: token.getExpiresAt(), 47 | } 48 | } 49 | throw new HttpException( 50 | 500, 51 | 'An unknown error occured while fetching user access token' 52 | ) 53 | }) 54 | .catch((err) => { 55 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 56 | }) 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "connectedcar-service", 3 | "version": "0.0.0", 4 | "main": "dist/index.js", 5 | "types": "dist/index.d.ts", 6 | "files": [ 7 | "dist/**/*" 8 | ], 9 | "scripts": { 10 | "clean": "rimraf dist/*", 11 | "build": "tsc", 12 | "lint": "eslint './src/**/*' --ext .ts", 13 | "lint:fix": "eslint './src/**/*' --ext .ts --fix", 14 | "format": "prettier --write './src/**/*'", 15 | "dev:start": "npm-run-all lint format build start", 16 | "dev": "nodemon --watch src -e ts,ejs --exec npm run dev:start", 17 | "start": "node .", 18 | "type-check": "tsc -noEmit", 19 | "test": "jest --setupFiles dotenv/config" 20 | }, 21 | "author": "", 22 | "license": "ISC", 23 | "dependencies": { 24 | "@types/node": "^18.6.2", 25 | "connected-car": "^1.2.12", 26 | "cors": "^2.8.5", 27 | "dotenv": "^16.0.1", 28 | "express": "^4.17.2", 29 | "helmet": "^5.1.1", 30 | "serverless-http": "^3.0.1" 31 | }, 32 | "devDependencies": { 33 | "@types/cors": "^2.8.12", 34 | "@types/express": "^4.17.13", 35 | "@typescript-eslint/eslint-plugin": "^5.31.0", 36 | "@typescript-eslint/parser": "^5.31.0", 37 | "deasync": "^0.1.26", 38 | "eslint": "^8.20.0", 39 | "eslint-config-airbnb-base": "^15.0.0", 40 | "eslint-config-airbnb-typescript": "^17.0.0", 41 | "eslint-config-prettier": "^8.3.0", 42 | "eslint-plugin-import": "^2.25.4", 43 | "eslint-plugin-jsx-a11y": "^6.4.1", 44 | "eslint-plugin-prettier": "^4.2.1", 45 | "eslint-plugin-react": "^7.25.1", 46 | "nodemon": "^2.0.15", 47 | "npm-run-all": "^4.1.5", 48 | "prettier": "^2.5.1", 49 | "serverless": "^3.21.0", 50 | "serverless-bundle": "^5.2.0", 51 | "serverless-domain-manager": "^6.0.3", 52 | "serverless-dotenv-plugin": "^4.0.1", 53 | "serverless-offline": "^9.1.0", 54 | "ts-node": "^10.4.0", 55 | "typescript": "^4.5.5" 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/controllers/user.controller.ts: -------------------------------------------------------------------------------- 1 | import connectedcar from 'connected-car' 2 | import HttpException from '../common/http-exception' 3 | 4 | /** 5 | * Fetch User info with access token 6 | * @param accessToken 7 | * @returns 8 | */ 9 | export const info = async (accessToken: string): Promise => { 10 | const user = connectedcar.User(accessToken) 11 | return user 12 | .info() 13 | .then((result) => result) 14 | .catch((err) => { 15 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 16 | }) 17 | } 18 | 19 | /** 20 | * Fetch User messages with access token 21 | * @param accessToken 22 | * @returns 23 | */ 24 | export const getMessages = async (accessToken: string): Promise => { 25 | const user = connectedcar.User(accessToken) 26 | return user 27 | .getMessages() 28 | .then((result) => result) 29 | .catch((err) => { 30 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 31 | }) 32 | } 33 | 34 | /** 35 | * Add new vehicle to user with passed VIN 36 | * @param vin 37 | * @param accessToken 38 | * @returns 39 | */ 40 | export const addVehicle = async ( 41 | vin: string, 42 | accessToken: string 43 | ): Promise => { 44 | const user = connectedcar.User(accessToken) 45 | return user 46 | .addVehicle(vin) 47 | .then((result) => result) 48 | .catch((err) => { 49 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 50 | }) 51 | } 52 | 53 | /** 54 | * Delete vehicle from user with passed VIN 55 | * @param vin 56 | * @param accessToken 57 | * @returns 58 | */ 59 | export const deleteVehicle = async ( 60 | vin: string, 61 | accessToken: string 62 | ): Promise => { 63 | const user = connectedcar.User(accessToken) 64 | return user 65 | .deleteVehicle(vin) 66 | .then((result) => result) 67 | .catch((err) => { 68 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 69 | }) 70 | } 71 | 72 | /** 73 | * Authorize request to access vehicle from another user 74 | * @param messageId 75 | * @param accessToken 76 | * @returns 77 | */ 78 | export const authorizeVehicle = async ( 79 | messageId: string, 80 | accessToken: string 81 | ): Promise => { 82 | const user = connectedcar.User(accessToken) 83 | return user 84 | .authorizeVehicle(messageId) 85 | .then((result) => result) 86 | .catch((err) => { 87 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 88 | }) 89 | } 90 | 91 | /** 92 | * Request access to vehicle from user 93 | * @param vin 94 | * @param accessToken 95 | * @returns 96 | */ 97 | export const requestVehicleAccess = async ( 98 | vin: string, 99 | accessToken: string 100 | ): Promise => { 101 | const user = connectedcar.User(accessToken) 102 | return user 103 | .requestVehicleAccess(vin) 104 | .then((result) => result) 105 | .catch((err) => { 106 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 107 | }) 108 | } 109 | 110 | /** 111 | * Get list of user account vehicles 112 | * @param accessToken 113 | * @returns 114 | */ 115 | export const listVehicles = async (accessToken: string): Promise => { 116 | const user = connectedcar.User(accessToken) 117 | return user 118 | .vehicles() 119 | .then((result) => result) 120 | .catch((err) => { 121 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 122 | }) 123 | } 124 | -------------------------------------------------------------------------------- /src/controllers/vehicle.controller.ts: -------------------------------------------------------------------------------- 1 | import connectedcar from 'connected-car' 2 | import HttpException from '../common/http-exception' 3 | 4 | /** 5 | * Fetch vehicle status based on passed vin 6 | * @param vin 7 | * @param accessToken 8 | * @returns 9 | */ 10 | export const status = async ( 11 | vin: string, 12 | accessToken: string 13 | ): Promise => { 14 | const vehicle = connectedcar.Vehicle(vin, accessToken) 15 | return vehicle 16 | .status() 17 | .then((result) => result) 18 | .catch((err) => { 19 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 20 | }) 21 | } 22 | 23 | /** 24 | * Get vehicle authorization status based on passed vin 25 | * @param vin 26 | * @param accessToken 27 | * @returns 28 | */ 29 | export const authStatus = async ( 30 | vin: string, 31 | accessToken: string 32 | ): Promise => { 33 | const vehicle = connectedcar.Vehicle(vin, accessToken) 34 | return vehicle 35 | .authorizationStatus() 36 | .then((result) => result) 37 | .catch((err) => { 38 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 39 | }) 40 | } 41 | 42 | /** 43 | * Send a vehicle authorzation request based on passed vin 44 | * @param vin 45 | * @param accessToken 46 | * @returns 47 | */ 48 | export const sendAuthorization = async ( 49 | vin: string, 50 | accessToken: string 51 | ): Promise => { 52 | const vehicle = connectedcar.Vehicle(vin, accessToken) 53 | return vehicle 54 | .sendAuthorization() 55 | .then((result) => result) 56 | .catch((err) => { 57 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 58 | }) 59 | } 60 | 61 | /** 62 | * Fetch vehicle details based on passed vin 63 | * @param vin 64 | * @param accessToken 65 | * @returns 66 | */ 67 | export const details = async ( 68 | vin: string, 69 | accessToken: string 70 | ): Promise => { 71 | const vehicle = connectedcar.Vehicle(vin, accessToken) 72 | return vehicle 73 | .details() 74 | .then((result) => result) 75 | .catch((err) => { 76 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 77 | }) 78 | } 79 | 80 | /** 81 | * Send wakeup request to vehicle 82 | * @param vin 83 | * @param accessToken 84 | * @returns 85 | */ 86 | export const wakeup = async ( 87 | vin: string, 88 | accessToken: string 89 | ): Promise => { 90 | const vehicle = connectedcar.Vehicle(vin, accessToken) 91 | return vehicle 92 | .wakeup() 93 | .then((result) => result) 94 | .catch((err) => { 95 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 96 | }) 97 | } 98 | 99 | /** 100 | * Attempt to start vehicle engine 101 | * @param vin 102 | * @param accessToken 103 | * @returns 104 | */ 105 | export const start = async (vin: string, accessToken: string): Promise => { 106 | const vehicle = connectedcar.Vehicle(vin, accessToken) 107 | return vehicle 108 | .start() 109 | .then((result) => result) 110 | .catch((err) => { 111 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 112 | }) 113 | } 114 | 115 | /** 116 | * Attempts to stop vehicle engine 117 | * @param vin 118 | * @param accessToken 119 | * @returns 120 | */ 121 | export const stop = async (vin: string, accessToken: string): Promise => { 122 | const vehicle = connectedcar.Vehicle(vin, accessToken) 123 | return vehicle 124 | .stop() 125 | .then((result) => result) 126 | .catch((err) => { 127 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 128 | }) 129 | } 130 | 131 | /** 132 | * Attempt to lock vehicle doors 133 | * @param vin 134 | * @param accessToken 135 | * @returns 136 | */ 137 | export const lock = async (vin: string, accessToken: string): Promise => { 138 | const vehicle = connectedcar.Vehicle(vin, accessToken) 139 | return vehicle 140 | .lock() 141 | .then((result) => result) 142 | .catch((err) => { 143 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 144 | }) 145 | } 146 | 147 | /** 148 | * Attempt to unlock vehicle doors 149 | * @param vin 150 | * @param accessToken 151 | * @returns 152 | */ 153 | export const unlock = async ( 154 | vin: string, 155 | accessToken: string 156 | ): Promise => { 157 | const vehicle = connectedcar.Vehicle(vin, accessToken) 158 | return vehicle 159 | .unlock() 160 | .then((result) => result) 161 | .catch((err) => { 162 | throw new HttpException(err.SyncErrorStatus, err.SyncErrorMessage) 163 | }) 164 | } 165 | -------------------------------------------------------------------------------- /src/routes/user.routes.ts: -------------------------------------------------------------------------------- 1 | import express, { NextFunction, Request, Response } from 'express' 2 | import HttpException from '../common/http-exception' 3 | import { 4 | addVehicle, 5 | authorizeVehicle, 6 | deleteVehicle, 7 | getMessages, 8 | info, 9 | listVehicles, 10 | requestVehicleAccess, 11 | } from '../controllers/user.controller' 12 | 13 | const userRouter = express.Router() 14 | 15 | /** 16 | * @api {get} /user/info Fetch User Info 17 | */ 18 | userRouter.get( 19 | '/info', 20 | async (req: Request, res: Response, next: NextFunction) => { 21 | if (req.headers.authorization) { 22 | const accessToken = req.headers.authorization.split(' ')[1] 23 | await info(accessToken) 24 | .then((result) => { 25 | res.status(200).json(result) 26 | }) 27 | .catch((err: HttpException) => { 28 | next(err) 29 | }) 30 | } else next(new HttpException(401, 'Missing access token')) 31 | } 32 | ) 33 | 34 | /** 35 | * @api {get} /user/messages Fetch User Messages 36 | */ 37 | userRouter.get( 38 | '/messages', 39 | async (req: Request, res: Response, next: NextFunction) => { 40 | if (req.headers.authorization) { 41 | const accessToken = req.headers.authorization.split(' ')[1] 42 | await getMessages(accessToken) 43 | .then((result) => { 44 | res.status(200).json(result) 45 | }) 46 | .catch((err: HttpException) => { 47 | next(err) 48 | }) 49 | } else next(new HttpException(401, 'Missing access token')) 50 | } 51 | ) 52 | 53 | /** 54 | * @api {get} /user/vehicle List all user vehicles 55 | */ 56 | userRouter.get( 57 | '/vehicle', 58 | async (req: Request, res: Response, next: NextFunction) => { 59 | if (req.headers.authorization) { 60 | const accessToken = req.headers.authorization.split(' ')[1] 61 | await listVehicles(accessToken) 62 | .then((result) => { 63 | res.status(200).json(result) 64 | }) 65 | .catch((err: HttpException) => { 66 | next(err) 67 | }) 68 | } else next(new HttpException(401, 'Missing access token')) 69 | } 70 | ) 71 | 72 | /** 73 | * @api {post} /user/vehicle Add new user vehicle 74 | */ 75 | userRouter.post( 76 | '/vehicle', 77 | async (req: Request, res: Response, next: NextFunction) => { 78 | if (req.headers.authorization && req.body.vin) { 79 | const accessToken = req.headers.authorization.split(' ')[1] 80 | const { vin } = req.body 81 | await addVehicle(vin, accessToken) 82 | .then((result) => { 83 | res.status(200).json(result) 84 | }) 85 | .catch((err: HttpException) => { 86 | next(err) 87 | }) 88 | } else next(new HttpException(401, 'Missing access token')) 89 | } 90 | ) 91 | 92 | /** 93 | * @api {delete} /user/vehicle Remove user vehicle 94 | */ 95 | userRouter.delete( 96 | '/vehicle', 97 | async (req: Request, res: Response, next: NextFunction) => { 98 | if (req.headers.authorization && req.body.vin) { 99 | const accessToken = req.headers.authorization.split(' ')[1] 100 | const { vin } = req.body 101 | await deleteVehicle(vin, accessToken) 102 | .then((result) => { 103 | res.status(200).json(result) 104 | }) 105 | .catch((err: HttpException) => { 106 | next(err) 107 | }) 108 | } else next(new HttpException(401, 'Missing access token')) 109 | } 110 | ) 111 | 112 | /** 113 | * @api {post} /user/authorizeVehicle Authorize user vehicle access request 114 | */ 115 | userRouter.post( 116 | '/authorizeVehicle', 117 | async (req: Request, res: Response, next: NextFunction) => { 118 | if (req.headers.authorization && req.body.messageId) { 119 | const accessToken = req.headers.authorization.split(' ')[1] 120 | const { messageId } = req.body 121 | await authorizeVehicle(messageId, accessToken) 122 | .then((result) => { 123 | res.status(200).json(result) 124 | }) 125 | .catch((err: HttpException) => { 126 | next(err) 127 | }) 128 | } else next(new HttpException(401, 'Missing access token')) 129 | } 130 | ) 131 | 132 | /** 133 | * @api {post} /user/requestVehicleAccess Request access to another user vehicle 134 | */ 135 | userRouter.post( 136 | '/requestVehicleAccess', 137 | async (req: Request, res: Response, next: NextFunction) => { 138 | if (req.headers.authorization && req.body.messageId) { 139 | const accessToken = req.headers.authorization.split(' ')[1] 140 | const { vin } = req.body 141 | await requestVehicleAccess(vin, accessToken) 142 | .then((result) => { 143 | res.status(200).json(result) 144 | }) 145 | .catch((err: HttpException) => { 146 | next(err) 147 | }) 148 | } else next(new HttpException(401, 'Missing access token')) 149 | } 150 | ) 151 | 152 | export default userRouter 153 | -------------------------------------------------------------------------------- /src/routes/vehicle.routes.ts: -------------------------------------------------------------------------------- 1 | import express, { NextFunction, Request, Response } from 'express' 2 | import { 3 | authStatus, 4 | details, 5 | lock, 6 | sendAuthorization, 7 | start, 8 | status, 9 | stop, 10 | unlock, 11 | wakeup, 12 | } from '../controllers/vehicle.controller' 13 | import HttpException from '../common/http-exception' 14 | 15 | const vehicleRouter = express.Router() 16 | 17 | /** 18 | * @api {get} /vehicle/status Fetch Vehicle status 19 | */ 20 | vehicleRouter.get( 21 | '/status', 22 | async (req: Request, res: Response, next: NextFunction) => { 23 | if (req.headers.authorization) { 24 | const accessToken = req.headers.authorization.split(' ')[1] 25 | const { vin } = req.query 26 | if (vin) { 27 | await status(vin.toString(), accessToken) 28 | .then((result) => { 29 | res.status(200).json(result) 30 | }) 31 | .catch((err: HttpException) => { 32 | next(err) 33 | }) 34 | } else next(new HttpException(400, 'Malformed Request: VIN required')) 35 | } else next(new HttpException(401, 'Missing access token')) 36 | } 37 | ) 38 | 39 | /** 40 | * @api {get} /vehicle/authstatus Fetch Vehicle authorization status 41 | */ 42 | vehicleRouter.get( 43 | '/authstatus', 44 | async (req: Request, res: Response, next: NextFunction) => { 45 | if (req.headers.authorization) { 46 | const accessToken = req.headers.authorization.split(' ')[1] 47 | const { vin } = req.query 48 | if (vin) { 49 | await authStatus(vin.toString(), accessToken) 50 | .then((result) => { 51 | res.status(200).json(result) 52 | }) 53 | .catch((err: HttpException) => { 54 | next(err) 55 | }) 56 | } else next(new HttpException(400, 'Malformed Request: VIN required')) 57 | } else next(new HttpException(401, 'Missing access token')) 58 | } 59 | ) 60 | 61 | /** 62 | * @api {post} /vehicle/sendAuth Send vehicle authorization request 63 | */ 64 | vehicleRouter.post( 65 | '/sendAuth', 66 | async (req: Request, res: Response, next: NextFunction) => { 67 | if (req.headers.authorization) { 68 | const accessToken = req.headers.authorization.split(' ')[1] 69 | const { vin } = req.body 70 | if (vin) { 71 | await sendAuthorization(vin, accessToken) 72 | .then((result) => { 73 | res.status(200).json(result) 74 | }) 75 | .catch((err: HttpException) => { 76 | next(err) 77 | }) 78 | } else next(new HttpException(400, 'Malformed Request: VIN required')) 79 | } else next(new HttpException(401, 'Missing access token')) 80 | } 81 | ) 82 | 83 | /** 84 | * @api {get} /vehicle/details Fetch Vehicle details 85 | */ 86 | vehicleRouter.get( 87 | '/details', 88 | async (req: Request, res: Response, next: NextFunction) => { 89 | if (req.headers.authorization) { 90 | const accessToken = req.headers.authorization.split(' ')[1] 91 | const { vin } = req.query 92 | if (vin) { 93 | await details(vin.toString(), accessToken) 94 | .then((result) => { 95 | res.status(200).json(result) 96 | }) 97 | .catch((err: HttpException) => { 98 | next(err) 99 | }) 100 | } else next(new HttpException(400, 'Malformed Request: VIN required')) 101 | } else next(new HttpException(401, 'Missing access token')) 102 | } 103 | ) 104 | 105 | /** 106 | * @api {post} /vehicle/wakeup Send vehicle wakeup request 107 | */ 108 | vehicleRouter.post( 109 | '/wakeup', 110 | async (req: Request, res: Response, next: NextFunction) => { 111 | if (req.headers.authorization) { 112 | const accessToken = req.headers.authorization.split(' ')[1] 113 | const { vin } = req.body 114 | if (vin) { 115 | await wakeup(vin, accessToken) 116 | .then((result) => { 117 | res.status(200).json(result) 118 | }) 119 | .catch((err: HttpException) => { 120 | next(err) 121 | }) 122 | } else next(new HttpException(400, 'Malformed Request: VIN required')) 123 | } else next(new HttpException(401, 'Missing access token')) 124 | } 125 | ) 126 | 127 | /** 128 | * @api {post} /vehicle/start Send vehicle start request 129 | */ 130 | vehicleRouter.post( 131 | '/start', 132 | async (req: Request, res: Response, next: NextFunction) => { 133 | if (req.headers.authorization) { 134 | const accessToken = req.headers.authorization.split(' ')[1] 135 | const { vin } = req.body 136 | if (vin) { 137 | await start(vin, accessToken) 138 | .then((result) => { 139 | res.status(200).json(result) 140 | }) 141 | .catch((err: HttpException) => { 142 | next(err) 143 | }) 144 | } else next(new HttpException(400, 'Malformed Request: VIN required')) 145 | } else next(new HttpException(401, 'Missing access token')) 146 | } 147 | ) 148 | 149 | /** 150 | * @api {post} /vehicle/stop Send vehicle stop request 151 | */ 152 | vehicleRouter.post( 153 | '/stop', 154 | async (req: Request, res: Response, next: NextFunction) => { 155 | if (req.headers.authorization) { 156 | const accessToken = req.headers.authorization.split(' ')[1] 157 | const { vin } = req.body 158 | if (vin) { 159 | await stop(vin, accessToken) 160 | .then((result) => { 161 | res.status(200).json(result) 162 | }) 163 | .catch((err: HttpException) => { 164 | next(err) 165 | }) 166 | } else next(new HttpException(400, 'Malformed Request: VIN required')) 167 | } else next(new HttpException(401, 'Missing access token')) 168 | } 169 | ) 170 | 171 | /** 172 | * @api {post} /vehicle/unlock Send vehicle unlock request 173 | */ 174 | vehicleRouter.post( 175 | '/unlock', 176 | async (req: Request, res: Response, next: NextFunction) => { 177 | if (req.headers.authorization) { 178 | const accessToken = req.headers.authorization.split(' ')[1] 179 | const { vin } = req.body 180 | if (vin) { 181 | await unlock(vin, accessToken) 182 | .then((result) => { 183 | res.status(200).json(result) 184 | }) 185 | .catch((err: HttpException) => { 186 | next(err) 187 | }) 188 | } else next(new HttpException(400, 'Malformed Request: VIN required')) 189 | } else next(new HttpException(401, 'Missing access token')) 190 | } 191 | ) 192 | 193 | /** 194 | * @api {post} /vehicle/lock Send vehicle lock request 195 | */ 196 | vehicleRouter.post( 197 | '/lock', 198 | async (req: Request, res: Response, next: NextFunction) => { 199 | if (req.headers.authorization) { 200 | const accessToken = req.headers.authorization.split(' ')[1] 201 | const { vin } = req.body 202 | if (vin) { 203 | await lock(vin, accessToken) 204 | .then((result) => { 205 | res.status(200).json(result) 206 | }) 207 | .catch((err: HttpException) => { 208 | next(err) 209 | }) 210 | } else next(new HttpException(400, 'Malformed Request: VIN required')) 211 | } else next(new HttpException(401, 'Missing access token')) 212 | } 213 | ) 214 | 215 | export default vehicleRouter 216 | --------------------------------------------------------------------------------