Account creation is ready!
Yay it seems to be working, owo! Added the actual database work, and some additional uniqueness checking.
This commit is contained in:
parent
4c1bda274a
commit
50e04385e3
4 changed files with 100 additions and 32 deletions
|
@ -30,7 +30,7 @@ GRANT SELECT, UPDATE ON closet_lists TO impress2020;
|
|||
GRANT SELECT, DELETE ON item_outfit_relationships TO impress2020;
|
||||
GRANT SELECT ON neopets_connections TO impress2020;
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON outfits TO impress2020;
|
||||
GRANT SELECT, UPDATE ON users TO impress2020;
|
||||
GRANT SELECT, INSERT, UPDATE ON users TO impress2020;
|
||||
GRANT SELECT, INSERT, UPDATE ON openneo_id.users TO impress2020;
|
||||
|
||||
-- mysqldump
|
||||
|
|
|
@ -21,7 +21,6 @@ import {
|
|||
} from "@chakra-ui/react";
|
||||
import React from "react";
|
||||
import { ErrorMessage, getGraphQLErrorMessage } from "../util";
|
||||
import WIPCallout from "./WIPCallout";
|
||||
|
||||
export default function LoginModal({ isOpen, onClose }) {
|
||||
return (
|
||||
|
@ -225,10 +224,12 @@ function CreateAccountForm() {
|
|||
|
||||
return (
|
||||
<form onSubmit={onSubmit}>
|
||||
<Box display="flex" justifyContent="center" marginBottom="3">
|
||||
<WIPCallout>TODO: This form isn't wired up yet!</WIPCallout>
|
||||
</Box>
|
||||
<FormControl isInvalid={errorTypes.includes("USERNAME_IS_REQUIRED")}>
|
||||
<FormControl
|
||||
isInvalid={
|
||||
errorTypes.includes("USERNAME_IS_REQUIRED") ||
|
||||
errorTypes.includes("USERNAME_MUST_BE_UNIQUE")
|
||||
}
|
||||
>
|
||||
<FormLabel>DTI Username</FormLabel>
|
||||
<Input
|
||||
type="text"
|
||||
|
@ -238,12 +239,17 @@ function CreateAccountForm() {
|
|||
reset();
|
||||
}}
|
||||
/>
|
||||
<FormHelperText>
|
||||
This will be separate from your Neopets.com account.
|
||||
</FormHelperText>
|
||||
{errorTypes.includes("USERNAME_IS_REQUIRED") && (
|
||||
<FormErrorMessage>Username can't be blank</FormErrorMessage>
|
||||
)}
|
||||
{errorTypes.includes("USERNAME_MUST_BE_UNIQUE") && (
|
||||
<FormErrorMessage>
|
||||
This username is already taken! Try another?
|
||||
</FormErrorMessage>
|
||||
)}
|
||||
<FormHelperText>
|
||||
This will be separate from your Neopets.com account.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<Box height="4" />
|
||||
<FormControl
|
||||
|
@ -260,12 +266,12 @@ function CreateAccountForm() {
|
|||
reset();
|
||||
}}
|
||||
/>
|
||||
<FormHelperText>
|
||||
Careful, never use your Neopets password for another site!
|
||||
</FormHelperText>
|
||||
{errorTypes.includes("PASSWORD_IS_REQUIRED") && (
|
||||
<FormErrorMessage>Password can't be blank</FormErrorMessage>
|
||||
)}
|
||||
<FormHelperText>
|
||||
Careful, never use your Neopets password for another site!
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
<Box height="4" />
|
||||
<FormControl isInvalid={passwordsDontMatch}>
|
||||
|
@ -284,7 +290,8 @@ function CreateAccountForm() {
|
|||
<FormControl
|
||||
isInvalid={
|
||||
errorTypes.includes("EMAIL_IS_REQUIRED") ||
|
||||
errorTypes.includes("EMAIL_MUST_BE_VALID")
|
||||
errorTypes.includes("EMAIL_MUST_BE_VALID") ||
|
||||
errorTypes.includes("EMAIL_MUST_BE_UNIQUE")
|
||||
}
|
||||
>
|
||||
<FormLabel>Email address</FormLabel>
|
||||
|
@ -296,17 +303,22 @@ function CreateAccountForm() {
|
|||
reset();
|
||||
}}
|
||||
/>
|
||||
<FormHelperText>
|
||||
We'll use this in the future if you need to reset your password, or
|
||||
for us to contact you about your account. We won't sell this address,
|
||||
and we won't send marketing-y emails.
|
||||
</FormHelperText>
|
||||
{errorTypes.includes("EMAIL_IS_REQUIRED") && (
|
||||
<FormErrorMessage>Email can't be blank</FormErrorMessage>
|
||||
)}
|
||||
{errorTypes.includes("EMAIL_MUST_BE_VALID") && (
|
||||
<FormErrorMessage>Email must be valid</FormErrorMessage>
|
||||
)}
|
||||
{errorTypes.includes("EMAIL_MUST_BE_UNIQUE") && (
|
||||
<FormErrorMessage>
|
||||
We already have an account with this address. Maybe it's yours?
|
||||
</FormErrorMessage>
|
||||
)}
|
||||
<FormHelperText>
|
||||
We'll use this in the future if you need to reset your password, or
|
||||
for us to contact you about your account. We won't sell this address,
|
||||
and we won't send marketing-y emails.
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
{error && (
|
||||
<ErrorMessage marginTop="4">
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { createHmac } from "crypto";
|
||||
import { createHmac, randomBytes } from "crypto";
|
||||
import { normalizeRow } from "./util";
|
||||
|
||||
// https://stackoverflow.com/a/201378/107415
|
||||
|
@ -153,8 +153,8 @@ function computeSignatureForAuthToken(unsignedAuthToken) {
|
|||
}
|
||||
|
||||
export async function createAccount(
|
||||
{ username, password, email, _ /* ipAddress */ },
|
||||
__ /* db */
|
||||
{ username, password, email, ipAddress },
|
||||
db
|
||||
) {
|
||||
const errors = [];
|
||||
if (!username) {
|
||||
|
@ -170,21 +170,76 @@ export async function createAccount(
|
|||
errors.push({ type: "EMAIL_MUST_BE_VALID" });
|
||||
}
|
||||
|
||||
// TODO: Add an error for non-unique username.
|
||||
const [nameRows] = await db.query(
|
||||
`
|
||||
SELECT count(*) FROM openneo_id.users WHERE name = ?;
|
||||
`,
|
||||
[username]
|
||||
);
|
||||
if (nameRows[0]["count(*)"] > 0) {
|
||||
errors.push({ type: "USERNAME_MUST_BE_UNIQUE" });
|
||||
}
|
||||
|
||||
// TODO: It'd be nice to not require email uniqueness, to avoid data leaks.
|
||||
// I don't want to argue with this constraint from Classic yet though.
|
||||
const [emailRows] = await db.query(
|
||||
`
|
||||
SELECT count(*) FROM openneo_id.users WHERE email = ?;
|
||||
`,
|
||||
[email]
|
||||
);
|
||||
if (emailRows[0]["count(*)"] > 0) {
|
||||
errors.push({ type: "EMAIL_MUST_BE_UNIQUE" });
|
||||
}
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { errors, authToken: null };
|
||||
}
|
||||
|
||||
throw new Error(`TODO: Actually create the account!`);
|
||||
// We'll generate 16 cryptographically-secure random bytes, encoded as a
|
||||
// length-32 hex string. This will be the salt for our encrypted password. (A
|
||||
// unique salt per user prevents table-based lookup attacks, where the
|
||||
// attacker acquires or generates a bunch of password hashes, then searches
|
||||
// for them in our database; because each user's unique salt means that the
|
||||
// same password from different services - or different users on DTI even -
|
||||
// will have different hashes for each user. You'd need a lookup table for
|
||||
// each user!)
|
||||
const passwordSalt = randomBytes(16).toString("hex");
|
||||
const encryptedPassword = encryptPassword(password, passwordSalt);
|
||||
|
||||
// await db.query(`
|
||||
// INSERT INTO openneo_id.users
|
||||
// (name, encrypted_password, email, password_salt, sign_in_count,
|
||||
// current_sign_in_at, current_sign_in_ip, created_at, updated_at)
|
||||
// VALUES (?, ?, ?, ?, 1, CURRENT_TIMESTAMP(), ?,
|
||||
// CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP());
|
||||
// `, [username, encryptedPassword, email, passwordSalt, ipAddress]);
|
||||
const connection = await db.getConnection();
|
||||
await connection.beginTransaction();
|
||||
let impressId;
|
||||
try {
|
||||
const [openneoIdResult] = await connection.query(
|
||||
`
|
||||
INSERT INTO openneo_id.users
|
||||
(name, encrypted_password, email, password_salt, sign_in_count,
|
||||
current_sign_in_at, current_sign_in_ip, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, 1, CURRENT_TIMESTAMP(), ?,
|
||||
CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP());
|
||||
`,
|
||||
[username, encryptedPassword, email, passwordSalt, ipAddress]
|
||||
);
|
||||
const userIdInOpenneoId = openneoIdResult.insertId;
|
||||
const [impressResult] = await connection.query(
|
||||
`
|
||||
INSERT INTO openneo_impress.users
|
||||
(name, remote_id, auth_server_id) VALUES (?, ?, 1);
|
||||
`,
|
||||
[username, userIdInOpenneoId]
|
||||
);
|
||||
impressId = impressResult.insertId;
|
||||
} catch (error) {
|
||||
try {
|
||||
await connection.rollback();
|
||||
} catch (error2) {
|
||||
console.warn(`Error rolling back transaction:`, error2);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
await connection.commit();
|
||||
|
||||
// return { errors: [], authToken: createAuthToken(6) };
|
||||
// Okay, user created! Return an auth token for the newly-created account.
|
||||
return { errors: [], authToken: createAuthToken(impressId) };
|
||||
}
|
||||
|
|
|
@ -77,10 +77,11 @@ const typeDefs = gql`
|
|||
}
|
||||
enum CreateAccountErrorType {
|
||||
USERNAME_IS_REQUIRED
|
||||
USERNAME_ALREADY_TAKEN
|
||||
USERNAME_MUST_BE_UNIQUE
|
||||
PASSWORD_IS_REQUIRED
|
||||
EMAIL_IS_REQUIRED
|
||||
EMAIL_MUST_BE_VALID
|
||||
EMAIL_MUST_BE_UNIQUE
|
||||
}
|
||||
`;
|
||||
|
||||
|
|
Loading…
Reference in a new issue