tenongene / med-tracker

0 stars 0 forks source link

Designing API - Authentication #5

Open tenongene opened 6 months ago

tenongene commented 6 months ago

Wrote an Express/JS API with REST API methods and routes that create a user (POST /user/signup), login a user(POST /user/login), add a new drug (PATCH /user/add), edit an existing drug (PATCH /user/edit), and deletes an existing drug (PATCH /user/delete).

The patch methods were used because each item in the DynamoDB table represents a unique user, so adding to attributes which represented their drug lists involved modifying attributes.

The code was converted to a lambda function code and a new lambda function was created on AWS.

========================= const serverless = require('serverless-http'); const express = require('express'); const morgan = require('morgan'); const cors = require('cors'); const jwt = require('jsonwebtoken'); const _ = require('lodash'); const bcrypt = require('bcrypt'); const { randomInt } = require('node:crypto'); const validator = require('validator'); const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); const { DynamoDBDocumentClient, UpdateCommand, QueryCommand, PutCommand } = require('@aws-sdk/lib-dynamodb'); const ACCESS_KEY = process.env.ACCESS_KEY; const SECRET = process.env.SECRET; const REGION = process.env.REGION; const TABLE_NAME = process.env.TABLE_NAME; const TABLE_GSI = process.env.TABLE_GSI; const client = new DynamoDBClient({ region: REGION, accessKeyId: ACCESS_KEY, secretAccessKey: SECRET, }); const ddb = DynamoDBDocumentClient.from(client);

const app = express(); //JWT issue function const createToken = (id) => { return jwt.sign({ id }, process.env.JWT_SECRET, { expiresIn: process.env.EXP }); }; //hash function const hashPass = async (password) => { try { const salt = await bcrypt.genSalt(15); const hash = await bcrypt.hash(password, salt); return hash; } catch (err) { console.log(err.message); } }; //randomid generator for userId const id = () => { return randomInt(1000000); }; //auth middleware const requireAuth = (req, res, next) => { //verify console.log(req.headers); const authHeader = req.headers.authorization || req.headers.Authorization; if (!authHeader || !authHeader?.startsWith('Bearer ')) { return res.status(401).json({ error: 'Authorization token required' }); } const bearerToken = authHeader.split(' ')[1]; try { const { id } = jwt.verify(bearerToken, process.env.JWT_SECRET); req.id = id; } catch (error) { console.log(error.message); res.status(401).json({ error: 'Request not authorized', msg: error.message }); } next(); }; //middleware app.use(cors(), function (req, res, next) { res.header('Access-Control-Allow-Origin', 'https://medtracker.d2va14boe1rzgf.amplifyapp.com'); res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization'); res.header('Access-Control-Allow-Credentials', 'true'); next(); }); app.use(express.json()); app.use(morgan('common'));

//signup app.post('/user/signup', async (req, res) => { const { email, password, firstName } = req.body; const jwtoken = createToken({ password }); const h_pass = await hashPass(password); const userModel = { TableName: TABLE_NAME, ConditionExpression: 'attribute_not_exists(email)', Item: { password: ${h_pass}, email: ${email}, firstName: ${_.capitalize(firstName)}, id: ${id()}, drugList: [], }, };

try {
    // field validation
    if (!email || !password || !firstName) {
        throw Error('All fields must be filled');
    }

    if (!validator.isEmail(email)) {
        throw Error('Email must be a valid email');
    }

    if (!validator.isStrongPassword(password)) {
        throw Error('Password must be at least 6 characters in length and contain at least one special character');
    }

    const response = await ddb.send(new PutCommand(userModel));
    console.log(response);

    res.status(201).json({ msg: 'User created successfully', email, password: h_pass, jwtoken });
} catch (err) {
    res.status(400).send({ error: err.message });
}

});

//login app.post('/user/login', async (req, res) => { const { email, password } = req.body;

try {
    const response = await ddb.send(
        new QueryCommand({
            TableName: TABLE_NAME,
            KeyConditionExpression: 'email = :emailval',
            Select: 'ALL_ATTRIBUTES',
            ExpressionAttributeValues: {
                ':emailval': email,
            },
        })
    );

    if (!email || !password) {
        throw Error('All fields must be filled');
    }

    if (response.Items.length === 0) {
        throw Error('No user found with that email!');
    }

    //bcrypt authentication
    bcrypt.compare(password, response.Items[0].password).then((result) => {
        const accessToken = createToken({ user: response.Items[0].firstName, id: response.Items[0].id });

        result
            ? res.status(200).json({
                    // response,
                    msg: 'User logged in successfully',
                    accessToken,
                    user: response.Items[0].firstName,
                    id: response.Items[0].id,
                    drugList: response.Items[0].drugList,
              })
            : res.status(403).json({ msg: 'Login Failed! Invalid password!' });
        console.log(response);
    });
} catch (err) {
    console.log(err.message);
    res.status(404).json({ error: err.message });

    //
}

});

// app.use(requireAuth());

//get app.get('/user/:id', async (req, res) => { try { const response = await ddb.send( new QueryCommand({ TableName: TABLE_NAME, IndexName: TABLE_GSI, KeyConditionExpression: 'id = :idvalue', ExpressionAttributeValues: { ':idvalue': ${req.params.id}, },

            ProjectionExpression: 'drugList',
        })
    );
    console.log(response);
    res.status(200).json(response);
    return response;
} catch (err) {
    res.status(403).json({ error: err.message });
    console.log(err.message);
}

});

//Add user drug app.patch('/user/add', async (req, res) => { const { newDrug, email } = req.body;

try {
    const response = await ddb.send(
        new UpdateCommand({
            TableName: TABLE_NAME,
            Key: { email: email },
            UpdateExpression: 'SET drugList = list_append(:newdrug, drugList)',
            ExpressionAttributeValues: {
                ':newdrug': newDrug,
            },
            ReturnValues: 'ALL_NEW',
        })
    );

    res.status(200).json({ response });

    //
} catch (error) {
    console.log(error.message);
}

});

//Edit user drug app.patch('/user/edit', async (req, res) => { const { drugIndex, email, updatedDrug } = req.body;

try {
    const response = await ddb.send(
        new UpdateCommand({
            TableName: TABLE_NAME,
            Key: { email: email },
            UpdateExpression: `SET drugList[${drugIndex}] = :updated_drug`,
            ExpressionAttributeValues: {
                ':updated_drug': updatedDrug,
            },
            ReturnValues: 'ALL_NEW',
        })
    );

    res.status(200).json({ response });

    //
} catch (error) {
    res.status(400).send({ error: error.message });
}

});

//Delete user drug app.patch('/user/delete', async (req, res) => { const { drugIndex, email } = req.body;

try {
    const response = await ddb.send(
        new UpdateCommand({
            TableName: TABLE_NAME,
            Key: { email: email },
            UpdateExpression: `REMOVE drugList[${drugIndex}]`,
            ReturnValues: 'ALL_NEW',
        })
    );
    res.status(200).json({ response });

    //
} catch (error) {
    res.status(400).send({ error: error.message });
}

});

//export lambda function module.exports.handler = serverless(app);

===========================================

tenongene commented 6 months ago

Image

===================== configured a function URL that will serve as the endpoint for the API

Image

tenongene commented 6 months ago

Prepared a lambda layer with all the node modules and dependencies for access by the lambda function

Image

tenongene commented 6 months ago

Added the environment variables for the lambda function.

Image