diff --git a/src/controllers/AccountController.ts b/src/controllers/AccountController.ts index e4affd2..6b3e808 100644 --- a/src/controllers/AccountController.ts +++ b/src/controllers/AccountController.ts @@ -1,4 +1,5 @@ import { RequestHandler } from "../entities/RequestHandler"; +import { AccountHelper } from "../helpers/AccountHelper"; /** * Account controller @@ -13,9 +14,31 @@ export class AccountController { * * @param handler */ - public static LoginUser(handler: RequestHandler) { + public static async LoginUser(handler: RequestHandler) { - handler.success("Successful operation."); + // Get post data + const email = handler.postEmail("userMail"); + const password = handler.postString("userPassword"); + + // TODO : add limits + + // Authenticate user + const tokens = await AccountHelper.LoginUser(email, password, handler.getClientInfo()); + + if(tokens == null) { + // TODO : add limits + + handler.error(401, "Invalid e-mail address / password !"); + } + + // Success + handler.send({ + success: "User signed in!", + tokens: { + token1: tokens.token1, + token2: tokens.token2 + } + }); } } \ No newline at end of file diff --git a/src/entities/RequestHandler.ts b/src/entities/RequestHandler.ts index 3b9ca3b..ea8a3f8 100644 --- a/src/entities/RequestHandler.ts +++ b/src/entities/RequestHandler.ts @@ -1,6 +1,7 @@ import { Response, Request } from "express"; import { APIHelper } from "../helpers/APIHelper"; import { APIClient } from "./APIClient"; +import { checkMail } from "../utils/StringUtils"; /** * Response to a request @@ -31,7 +32,7 @@ export class RequestHandler { * @param required If set to true (true by default), an error will * be thrown if the string is not included in the request */ - public getString(name : string, minLength : number = 1, required : boolean = true) : string { + public postString(name : string, minLength : number = 1, required : boolean = true) : string { const param = this.getPostParam(name); // Check if parameter was not found @@ -47,6 +48,20 @@ export class RequestHandler { return param; } + /** + * Get an email address included in a post request + * + * @param name The name of the POST filed + */ + public postEmail(name: string) : string { + const email = this.postString(name, 3); + + if(!checkMail(email)) + this.error(400, email + " is not a valid email address!"); + + return email; + } + /** * Validate API tokens @@ -56,8 +71,8 @@ export class RequestHandler { public async checkAPITokens() { // Extract API name & token from request - const apiName = this.getString("serviceName"); - const apiToken = this.getString("serviceToken"); + const apiName = this.postString("serviceName"); + const apiToken = this.postString("serviceToken"); // Validate the client const client = await APIHelper.GetClient(apiName, apiToken); @@ -81,7 +96,18 @@ export class RequestHandler { } /** - * Output an error code + * Get information about API client + */ + public getClientInfo() : APIClient { + + if(!this.client) + throw Error("Try to get information about client but client has not been authenticated!"); + + return this.client; + } + + /** + * Output an error code and throws an error * * @param code HTTP Status code * @param message The message to send diff --git a/src/entities/UserLoginTokens.ts b/src/entities/UserLoginTokens.ts new file mode 100644 index 0000000..7b457c8 --- /dev/null +++ b/src/entities/UserLoginTokens.ts @@ -0,0 +1,12 @@ +/** + * User tokens + * + * @author Pierre HUBERT + */ + +export interface UserLoginTokens { + userID : number, + clientID : number, + token1: string, + token2: string, +} \ No newline at end of file diff --git a/src/helpers/AccountHelper.ts b/src/helpers/AccountHelper.ts new file mode 100644 index 0000000..5b458a2 --- /dev/null +++ b/src/helpers/AccountHelper.ts @@ -0,0 +1,102 @@ +import { crypt, sha1, randomStr } from "../utils/CryptUtils"; +import { APIClient } from "../entities/APIClient"; +import { UserLoginTokens } from "../entities/UserLoginTokens"; +import { DatabaseHelper } from "./DatabaseHelper"; + +/** + * Account helper + * + * @author Pierre HUBERT + */ + +const USER_TABLE = "utilisateurs"; +const USERS_TOKENS_TABLE = "comunic_api_users_tokens"; + +export class AccountHelper { + + /** + * Given email address and password, try to sign in user + * + * @param email The email of the user + * @param password User password + * @param client Information about associated client / null if none found + */ + static async LoginUser(email: string, password: string, client: APIClient) : Promise { + + // Perform a request on the database + const row = await DatabaseHelper.QueryRow({ + table: USER_TABLE, + fields: ["ID"], + where: { + mail: email, + password: this.CryptPassword(password) + } + }); + + // Check if user was found + if(row == null) + return null; + const userID = row.ID; + + // Check for existing tokens + let tokens = await this.GetClientTokens(userID, client); + + if(tokens != null) + return tokens; + + const newTokens : UserLoginTokens = { + userID: userID, + clientID: client.id, + token1: randomStr(150), + token2: "dummy_data" + } + + // Save new tokens + await DatabaseHelper.InsertRow(USERS_TOKENS_TABLE, { + user_id: newTokens.userID, + service_id: newTokens.clientID, + token1: newTokens.token1, + token2: newTokens.token2 + }); + + return newTokens; + } + + /** + * Get user client tokens (if it exists) + * + * @param userID Target user ID + * @param client Information about associated client / null if none found + */ + private static async GetClientTokens(userID: number, client: APIClient): Promise { + const row = await DatabaseHelper.QueryRow({ + table: USERS_TOKENS_TABLE, + where: { + user_id: userID, + service_id: client.id + } + }); + + return row == null ? null : this.DBToUserTokens(row); + } + + /** + * Crypt a password + * + * @param pass The password to crypt + * @return Encrypted string + */ + private static CryptPassword(pass: string) : string { + return crypt(sha1(pass), sha1(pass)); + } + + + private static DBToUserTokens(row : any) : UserLoginTokens { + return { + userID: row.user_id, + clientID: row.service_id, + token1: row.token1, + token2: row.token2 + }; + } +} \ No newline at end of file diff --git a/src/helpers/DatabaseHelper.ts b/src/helpers/DatabaseHelper.ts index d480fde..852badc 100644 --- a/src/helpers/DatabaseHelper.ts +++ b/src/helpers/DatabaseHelper.ts @@ -9,6 +9,7 @@ import { conf } from "./ConfigHelper"; export interface QueryInformation { table: string, + fields ?: Array, where ?: Object, limit ?: number, } @@ -51,7 +52,12 @@ export class DatabaseHelper { */ static async Query(info: QueryInformation) : Promise> { // Prepare SQL request - let request = "SELECT * FROM " + info.table; + let request = "SELECT "; + + // Requested fields + request += info.fields ? info.fields.join(",") : "*"; + + request += " FROM " + info.table; let args = []; // Add where arguments @@ -61,7 +67,7 @@ export class DatabaseHelper { for(const k in info.where) { if(!info.where.hasOwnProperty(k)) continue; - const v = info.where[k]; + const v = info.where[k].toString(); request += k; request += v.startsWith("%") || v.endsWith("%") ? " LIKE " : " = " @@ -107,4 +113,21 @@ export class DatabaseHelper { return result[0]; } + + /** + * Insert an new entry into the database + * + * @param info Information about the entry + * @returns The ID of the inserted column (if any) + */ + static async InsertRow(table : string, values : any) : Promise { + return new Promise((resolve, reject) => { + this.connection.query("INSERT INTO " + table + " SET ?", values, (err, results, fields) => { + if(err) + reject(err); + + resolve(results.insertId); + }); + }); + } } \ No newline at end of file diff --git a/src/utils/CryptUtils.ts b/src/utils/CryptUtils.ts index 8922dc4..741959f 100644 --- a/src/utils/CryptUtils.ts +++ b/src/utils/CryptUtils.ts @@ -27,4 +27,29 @@ export function sha1(str : string) : string { */ export function crypt(str : string, salt: string) : string { return execSync("/usr/bin/php -r \"echo crypt(\'"+str+"\', \'"+salt+"\');\"").toString(); +} + +/** + * Generate a random integer + * + * @param max Maximum value to except + */ +export function randInt(max: number) : number { + return Math.floor(Math.random() * Math.floor(max)); +} + +/** + * Generate a random string + * + * @param length The length of the string to generate + * @param keyspace Keyspace to use + */ +export function randomStr(length: number, keyspace : string = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") : string { + let str = ""; + + for (let i = 0; i < length; i++) { + str += keyspace[randInt(keyspace.length)]; + } + + return str; } \ No newline at end of file diff --git a/src/utils/StringUtils.ts b/src/utils/StringUtils.ts new file mode 100644 index 0000000..7f116c1 --- /dev/null +++ b/src/utils/StringUtils.ts @@ -0,0 +1,15 @@ +/** + * String utilities + * + * @author Pierre HUBERT + */ + +/** + * Check a given email address + * + * @param {String} emailAddress The email address to check + * @return {Boolean} True for a valid email address / false else + */ +export function checkMail(emailAddress: string): boolean { + return (emailAddress.match(/^[a-zA-Z0-9_.]+@[a-zA-Z0-9-.]{1,}[.][a-zA-Z]{2,8}$/) === null ? false : true); +} \ No newline at end of file