DEV Community

Ephriam Henderson
Ephriam Henderson

Posted on

Day 12 [D&D Character Sheets]

Report

Today I got user authentication working with passport. I started setting by setting up my password hashing. I noticed that in the passport documentation that they were using methods on the user object to compare and hash passwords and had an realization. I never made the connection to do store the hashing logic on the model but I loved the pattern. Password hashing is intrinsically tied to the user object and it makes sense to store them together, it also make maintenance easier. With this in mind, I made a static and instance method on my player schema to handle password hashing.

// db/models/player/index.js
const Player= new Schema({...});

Player.methods.checkPassword = function(plainTextPassword) {
    return bcrypt.compare(plainTextPassword, this.password)
}

Player.statics.hashPassword = function(plainTextPassword) {
    return bcrypt.hash(plainTextPassword, 10)
}

module.exports = Player

You can see the static method being used in the seeder. And the instance method in the passport local strategy.

// db/seeder.js
const player = await db.models.Player.create({
    username: "admin",
    email: "ephriamhenderson@ephriamhenderson.dev",
    // Static method that hashes user password.
    password: await db.models.Player.hashPassword("password") 
});
// helpers/init-passport.js
const player = await db.models.Player.findOne({
    email: email.toLowerCase()
});
// ...
// The instance method that compares the password.
let isMatch = await player.checkPassword(password);

Getting passport working properly took a bit of debugging. First the local strategy didn't seem to be running. Soon I realized that I needed to define the name of my username and password field.

// helpers/init-passport.js
// broken
passport.use(
    new LocalStrategy(
        async (email, password, done) => {
            logger.debug(`Auth attempt for: ${email}`);
            const db = await dbPromise;
            const player = await db.models.Player.findOne({
                email: email.toLowerCase()
            });

            if (!player) {
                logger.debug(`No email found for: ${email}`);
                return done(null, false, { message: "Incorrect Credentials" });
            }

            let isMatch = await player.checkPassword(password);

            if (!isMatch) {
                logger.debug(`Incorrect password for: ${email}`);
                return done(null, false, { message: "Incorrect Credentials" });
            }
            logger.debug(`${email} logged in successfully.`);

            return done(null, player);
        }
    )
);
// helpers/init-passport.js
// fixed
passport.use(
        new LocalStrategy(
            {
                // define the name of the username and password fields
                usernameField: "email",
                passwordField: "password"
            },
            async (email, password, done) => {
                logger.debug(`Auth attempt for: ${email}`);
                // Make sure the db is connected and and the db object returned
                const db = await dbPromise;
                const player = await db.models.Player.findOne({
                    email: email.toLowerCase()
                });

                // Does player with that email exist? If not send an error.
                if (!player) {
                    logger.debug(`No email found for: ${email}`);
                    return done(null, false, { message: "Incorrect Credentials" });
                }

                let isMatch = await player.checkPassword(password);

                // Does the password match? If not send and error.
                if (!isMatch) {
                    logger.debug(`Incorrect password for: ${email}`);
                    return done(null, false, { message: "Incorrect Credentials" });
                }
                logger.debug(`${email} logged in successfully.`);

                return done(null, player);
            }
        )
    );

Once I got the local strategy to run correctly I encountered an issue with my login post route.

// router/users/index.js
router.post(
    "/login",
    bodyParser.urlencoded({ extended: true }),
    // This should redirect depending on failure or success.
    passport.authenticate("local", {
        successRedirect: "/",
        failureRedirect: "/users/login"
    })
);

Passport wouldn't redirect correctly, when I made a request it would hang without throwing an error. Almost as if I had forgot to send a response in a normal route. Eventually I found the problem in my passport serialization.

// helpers/init-passport.js
// broken
passport.serializeUser((user, done) => {
    logger.debug(`Serializing ${user.email}`);
    done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
    logger.debug(`Deserializing user: ${id}`);
    const db = await dbPromise;
    let user = db.models.Player.findById(id);
});
// helpers/init-passport.js
// fixed
passport.serializeUser((user, done) => {
    logger.debug(`Serializing ${user.email}`);
    done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
    logger.debug(`Deserializing user: ${id}`);
    const db = await dbPromise;
    let user = db.models.Player.findById(id);
    // I had forgot to call done.
    done(null, user);
});

I'm happy with today. I had a good time debugging this issue and I got authentication working fairly easily with passport. As a junior dev I still have a ton to learn about security and would be interested in learning how to make my passwords more secure, am I making any blatant security mistakes? Any tips for best practices? Feel free to tell me how bad I am at this!

Project

[100days] The DND Character Sheet App

This is the first project of my 100 days of coding This is an app to keep D&D character sheets.

Stack

I'll be using Node.js and building a full-stack Express app with MongoDB.

Requirements

Minimum Viable

  • Present a D&D Character Sheet
    • The sheet should display all the same info as the first page of the 5e Official sheet.
  • Users should be able to log in and create player-characters.
  • Users should be able to edit character sheets.
  • Users should be able to organize character sheets into groups (parties/tables)
  • Sheets should auto calculate basic stats like ability modifiers
    • Support Proficiency Bonuses

Cake

  • Extend character creation to allow the user to use any of the three common stat gen methods
    • Point Buy
    • Standard Array
    • Roll
  • Extend the character sheet to all the info in the 5e official sheet.
  • Allow for image uploads for character portraits.
  • Allow for…

The First project will be an app to keep D&D character sheets.

Stack

I'll be using Node.js and building a full-stack Express app with MongoDB.

Requirements

Minimum Viable

  • [ ] Investigate automating or finding a source of info for the data in the SRD.
  • [ ] Present a D&D Character Sheet
    • [ ] The sheet should display all the same info as the first page of the 5e Official sheet.
  • [ ] Users should be able to log in and create player-characters.
  • [ ] Users should be able to edit character sheets.
  • [ ] Users should be able to organize character sheets into groups (parties/tables)
  • [ ] Sheets should auto calculate basic stats like ability modifiers.
    • [ ] Support Proficiency Bonuses

Cake

  • [ ] Extend character creation to allow the user to use any of the three common stat gen methods.
    • [ ] Point Buy
    • [ ] Standard Array
    • [ ] Roll
  • [ ] Extend the character sheet to all the info in the 5e official sheet.
  • [ ] Allow for image uploads for character portraits.
  • [ ] Allow for extended descriptions/backstories.
    • [ ] Characters should have nice full page backstories.
    • [ ] Preferably use a markdown editor.

Top comments (0)