Skip to content

Commit

Permalink
Added validation and fixed some potential bugs
Browse files Browse the repository at this point in the history
  • Loading branch information
jvik committed Dec 22, 2024
1 parent e86df21 commit 214776b
Show file tree
Hide file tree
Showing 7 changed files with 151 additions and 40 deletions.
1 change: 1 addition & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
.env
2 changes: 1 addition & 1 deletion dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
FROM node:slim

# We don't need the standalone Chromium
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD true
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true

# Install Google Chrome Stable and fonts
# Note: this installs the necessary libs to make the browser work with Puppeteer.
Expand Down
50 changes: 50 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"dotenv": "^16.3.2",
"eslint": "^8.56.0",
"express": "^4.18.2",
"joi": "^17.13.3",
"puppeteer": "^23.11.1",
"tslib": "^2.6.2",
"typescript": "^5.3.3"
Expand Down
92 changes: 55 additions & 37 deletions src/auth.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import config from './config.js';
import puppeteer, { type Page } from 'puppeteer';
import axios from 'axios';
import fs from 'node:fs';
Expand All @@ -7,17 +8,12 @@ import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

const clientId = process.env.CLIENT_ID || '';
const redirectUri = process.env.REDIRECT_URI || 'io.elaway.no.app://auth.elaway.io/ios/io.elaway.no.app/callback';
const oauthScope = "openid profile email";
const state = "randomstate";
const redirectUri = "io.elaway.no.app://auth.elaway.io/ios/io.elaway.no.app/callback";
const elawayAuthorizationUrl = "https://auth.elaway.io/authorize";
const ampecoApiUrl = "https://no.eu-elaway.charge.ampeco.tech/api/v1/app/oauth/token";
const elawayTokenUrl = "https://auth.elaway.io/oauth/token";
const elawayClientId = process.env.ELAWAY_CLIENT_ID || '1';
const elawayClientSecret = process.env.ELAWAY_CLIENT_SECRET || '';
const elawayUser = process.env.ELAWAY_USER || '';
const elawayPassword = process.env.ELAWAY_PASSWORD || '';
const tokenFilePath = path.resolve(__dirname, 'tokens.json');

interface IdTokenResponse {
Expand Down Expand Up @@ -67,7 +63,7 @@ async function getAuthorizationCode(page: Page): Promise<string | null> {
async function exchangeCodeForIdAndAuthToken(code: string): Promise<IdTokenResponse> {
const response = await axios.post(elawayTokenUrl, {
grant_type: "authorization_code",
client_id: clientId,
client_id: config.clientId,
redirect_uri: redirectUri,
code: code
}, {
Expand Down Expand Up @@ -98,29 +94,34 @@ async function exchangeCodeForIdAndAuthToken(code: string): Promise<IdTokenRespo
// return response.data;
// }

async function getElawayToken(accessToken: string, idToken: string): Promise<ElawayTokenResponse> {
const response = await axios.post(ampecoApiUrl, {
token: JSON.stringify({
accessToken: accessToken,
idToken: idToken,
scope: "openid profile email",
expiresIn: 100,
tokenType: "Bearer"
}),
type: "auth0",
grant_type: "third-party",
client_id: elawayClientId,
client_secret: elawayClientSecret
}, {
headers: {
"Content-Type": "application/json",
"User-Agent": "insomnia/10.0.0"
}
});
async function getElawayToken(accessToken: string, idToken: string): Promise<ElawayTokenResponse | null> {
try {
const response = await axios.post(ampecoApiUrl, {
token: JSON.stringify({
accessToken: accessToken,
idToken: idToken,
scope: "openid profile email",
expiresIn: 100,
tokenType: "Bearer"
}),
type: "auth0",
grant_type: "third-party",
client_id: config.elawayClientId,
client_secret: config.elawayClientSecret
}, {
headers: {
"Content-Type": "application/json",
"User-Agent": "insomnia/10.0.0"
}
});

saveTokens(response.data);
saveTokens(response.data);

return response.data;
return response.data;
} catch (error) {
console.error(error.message);
return null;
}
}

function saveTokens(tokenResponse: ElawayTokenResponse): StoredElawayToken {
Expand All @@ -134,6 +135,8 @@ function saveTokens(tokenResponse: ElawayTokenResponse): StoredElawayToken {
};
fs.writeFileSync(tokenFilePath, JSON.stringify(storedToken));

console.log("New bearer token saved");

return storedToken;
}

Expand All @@ -149,31 +152,41 @@ function loadTokens(): StoredElawayToken | null {
async function startOauth(): Promise<ElawayTokenResponse | null> {
let tokenResponse: ElawayTokenResponse | null = null;
let accessIdResponse: null | IdTokenResponse = null;
const authUrl = `${elawayAuthorizationUrl}?response_type=code&client_id=${encodeURIComponent(clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(oauthScope)}&state=${encodeURIComponent(state)}`;
const authUrl = `${elawayAuthorizationUrl}?response_type=code&client_id=${encodeURIComponent(config.clientId)}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=${encodeURIComponent(oauthScope)}&state=${encodeURIComponent(state)}`;

const browser = await puppeteer.launch({ headless: true, args: ['--no-sandbox'] });
const page = await browser.newPage();

try {
await page.goto(authUrl);
await page.goto(authUrl, { waitUntil: 'domcontentloaded', timeout: 10000 });

const errorMessage = await page.evaluate(() => {
const errorElement = document.querySelector('p.error-message');
return errorElement ? (errorElement as HTMLElement).innerText : null;
});

if (errorMessage) {
console.error("Error on login page:", errorMessage);
throw new Error("Error during login. You most likely have the wrong CLIENT_ID")
}

await page.waitForSelector('input[name="username"]');
await page.waitForSelector('input[name="password"]');
await page.type('input[name="username"]', elawayUser, { delay: 50 });
await page.type('input[name="password"]', elawayPassword, { delay: 50 });
await page.type('input[name="username"]', config.elawayUser, { delay: 50 });
await page.type('input[name="password"]', config.elawayPassword, { delay: 50 });
await page.keyboard.press('Enter');

const code = await getAuthorizationCode(page);
if (code) {
console.log("Fann autorisasjonskode:", code);
console.log("Found authorization code:");
await browser.close();

accessIdResponse = await exchangeCodeForIdAndAuthToken(code);

tokenResponse = await getElawayToken(accessIdResponse.access_token, accessIdResponse.id_token);

}
} catch (error) {
console.error("Feil ved handtering:", error);
console.error(error.message);
} finally {
await browser.close();
}
Expand All @@ -199,12 +212,17 @@ async function getValidCredentials(): Promise<StoredElawayToken | null> {
// }

if (!validBearerToken) {
console.log("No existing bearer token found. Attempting to get new token.");
const newToken = await startOauth();
if (newToken) {
return loadTokens();

if (!newToken) {
throw new Error("Could not get valid credentials");
}
return loadTokens();
}

console.log("Using stored bearer token");

return storedToken;
}

Expand Down
37 changes: 37 additions & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import dotenv from 'dotenv';
import Joi from 'joi';

dotenv.config();

// Define the schema for validation
const envVarsSchema = Joi.object({
PORT: Joi.number().default(3000),
ELAWAY_USER: Joi.string().email().required(),
ELAWAY_CLIENT_ID: Joi.string().default('1'),
ELAWAY_CLIENT_SECRET: Joi.string().required(),
ELAWAY_PASSWORD: Joi.string().required(),
POLLING_INTERVAL: Joi.number().default(60000),
CLIENT_ID: Joi.string().required()
}).unknown();

// Validate environment variables
const { value: envVars, error } = envVarsSchema
.prefs({ errors: { label: 'key' } })
.validate(process.env);

if (error) {
throw new Error(`Config validation error: ${error.message}`);
}

// Export the validated config
const config = {
port: envVars.PORT,
elawayUser: envVars.ELAWAY_USER,
elawayPassword: envVars.ELAWAY_PASSWORD,
elawayClientId: envVars.ELAWAY_CLIENT_ID,
elawayClientSecret: envVars.ELAWAY_CLIENT_SECRET,
pollingInterval: envVars.POLLING_INTERVAL,
clientId: envVars.CLIENT_ID,
};

export default config;
8 changes: 6 additions & 2 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import 'dotenv/config';
import config from './config.js';
import express from 'express';
import chargerRouter from './charger/chargerRouter.js';
import axios from 'axios';
import { getValidCredentials } from './auth.js';
import Charger from './charger/charger.js';

const port = process.env.PORT || 3000;
const port = config.port;
const app = express();

const token = await getValidCredentials();
Expand All @@ -17,6 +17,10 @@ axios.interceptors.request.use(async (config) => {
return config;
});

if (!token) {
throw new Error('Could not get valid credentials');
}

const charger = await Charger.getInstance();
charger.startPeriodicCheck();

Expand Down

0 comments on commit 214776b

Please sign in to comment.