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, DELETE ON item_outfit_relationships TO impress2020;
|
||||||
GRANT SELECT ON neopets_connections TO impress2020;
|
GRANT SELECT ON neopets_connections TO impress2020;
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON outfits 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;
|
GRANT SELECT, INSERT, UPDATE ON openneo_id.users TO impress2020;
|
||||||
|
|
||||||
-- mysqldump
|
-- mysqldump
|
||||||
|
|
|
@ -21,7 +21,6 @@ import {
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { ErrorMessage, getGraphQLErrorMessage } from "../util";
|
import { ErrorMessage, getGraphQLErrorMessage } from "../util";
|
||||||
import WIPCallout from "./WIPCallout";
|
|
||||||
|
|
||||||
export default function LoginModal({ isOpen, onClose }) {
|
export default function LoginModal({ isOpen, onClose }) {
|
||||||
return (
|
return (
|
||||||
|
@ -225,10 +224,12 @@ function CreateAccountForm() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={onSubmit}>
|
<form onSubmit={onSubmit}>
|
||||||
<Box display="flex" justifyContent="center" marginBottom="3">
|
<FormControl
|
||||||
<WIPCallout>TODO: This form isn't wired up yet!</WIPCallout>
|
isInvalid={
|
||||||
</Box>
|
errorTypes.includes("USERNAME_IS_REQUIRED") ||
|
||||||
<FormControl isInvalid={errorTypes.includes("USERNAME_IS_REQUIRED")}>
|
errorTypes.includes("USERNAME_MUST_BE_UNIQUE")
|
||||||
|
}
|
||||||
|
>
|
||||||
<FormLabel>DTI Username</FormLabel>
|
<FormLabel>DTI Username</FormLabel>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -238,12 +239,17 @@ function CreateAccountForm() {
|
||||||
reset();
|
reset();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FormHelperText>
|
|
||||||
This will be separate from your Neopets.com account.
|
|
||||||
</FormHelperText>
|
|
||||||
{errorTypes.includes("USERNAME_IS_REQUIRED") && (
|
{errorTypes.includes("USERNAME_IS_REQUIRED") && (
|
||||||
<FormErrorMessage>Username can't be blank</FormErrorMessage>
|
<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>
|
</FormControl>
|
||||||
<Box height="4" />
|
<Box height="4" />
|
||||||
<FormControl
|
<FormControl
|
||||||
|
@ -260,12 +266,12 @@ function CreateAccountForm() {
|
||||||
reset();
|
reset();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<FormHelperText>
|
|
||||||
Careful, never use your Neopets password for another site!
|
|
||||||
</FormHelperText>
|
|
||||||
{errorTypes.includes("PASSWORD_IS_REQUIRED") && (
|
{errorTypes.includes("PASSWORD_IS_REQUIRED") && (
|
||||||
<FormErrorMessage>Password can't be blank</FormErrorMessage>
|
<FormErrorMessage>Password can't be blank</FormErrorMessage>
|
||||||
)}
|
)}
|
||||||
|
<FormHelperText>
|
||||||
|
Careful, never use your Neopets password for another site!
|
||||||
|
</FormHelperText>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<Box height="4" />
|
<Box height="4" />
|
||||||
<FormControl isInvalid={passwordsDontMatch}>
|
<FormControl isInvalid={passwordsDontMatch}>
|
||||||
|
@ -284,7 +290,8 @@ function CreateAccountForm() {
|
||||||
<FormControl
|
<FormControl
|
||||||
isInvalid={
|
isInvalid={
|
||||||
errorTypes.includes("EMAIL_IS_REQUIRED") ||
|
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>
|
<FormLabel>Email address</FormLabel>
|
||||||
|
@ -296,17 +303,22 @@ function CreateAccountForm() {
|
||||||
reset();
|
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") && (
|
{errorTypes.includes("EMAIL_IS_REQUIRED") && (
|
||||||
<FormErrorMessage>Email can't be blank</FormErrorMessage>
|
<FormErrorMessage>Email can't be blank</FormErrorMessage>
|
||||||
)}
|
)}
|
||||||
{errorTypes.includes("EMAIL_MUST_BE_VALID") && (
|
{errorTypes.includes("EMAIL_MUST_BE_VALID") && (
|
||||||
<FormErrorMessage>Email must be valid</FormErrorMessage>
|
<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>
|
</FormControl>
|
||||||
{error && (
|
{error && (
|
||||||
<ErrorMessage marginTop="4">
|
<ErrorMessage marginTop="4">
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import { createHmac } from "crypto";
|
import { createHmac, randomBytes } from "crypto";
|
||||||
import { normalizeRow } from "./util";
|
import { normalizeRow } from "./util";
|
||||||
|
|
||||||
// https://stackoverflow.com/a/201378/107415
|
// https://stackoverflow.com/a/201378/107415
|
||||||
|
@ -153,8 +153,8 @@ function computeSignatureForAuthToken(unsignedAuthToken) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function createAccount(
|
export async function createAccount(
|
||||||
{ username, password, email, _ /* ipAddress */ },
|
{ username, password, email, ipAddress },
|
||||||
__ /* db */
|
db
|
||||||
) {
|
) {
|
||||||
const errors = [];
|
const errors = [];
|
||||||
if (!username) {
|
if (!username) {
|
||||||
|
@ -170,21 +170,76 @@ export async function createAccount(
|
||||||
errors.push({ type: "EMAIL_MUST_BE_VALID" });
|
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) {
|
if (errors.length > 0) {
|
||||||
return { errors, authToken: null };
|
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(`
|
const connection = await db.getConnection();
|
||||||
// INSERT INTO openneo_id.users
|
await connection.beginTransaction();
|
||||||
// (name, encrypted_password, email, password_salt, sign_in_count,
|
let impressId;
|
||||||
// current_sign_in_at, current_sign_in_ip, created_at, updated_at)
|
try {
|
||||||
// VALUES (?, ?, ?, ?, 1, CURRENT_TIMESTAMP(), ?,
|
const [openneoIdResult] = await connection.query(
|
||||||
// CURRENT_TIMESTAMP(), CURRENT_TIMESTAMP());
|
`
|
||||||
// `, [username, encryptedPassword, email, passwordSalt, ipAddress]);
|
INSERT INTO openneo_id.users
|
||||||
|
(name, encrypted_password, email, password_salt, sign_in_count,
|
||||||
// return { errors: [], authToken: createAuthToken(6) };
|
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();
|
||||||
|
|
||||||
|
// 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 {
|
enum CreateAccountErrorType {
|
||||||
USERNAME_IS_REQUIRED
|
USERNAME_IS_REQUIRED
|
||||||
USERNAME_ALREADY_TAKEN
|
USERNAME_MUST_BE_UNIQUE
|
||||||
PASSWORD_IS_REQUIRED
|
PASSWORD_IS_REQUIRED
|
||||||
EMAIL_IS_REQUIRED
|
EMAIL_IS_REQUIRED
|
||||||
EMAIL_MUST_BE_VALID
|
EMAIL_MUST_BE_VALID
|
||||||
|
EMAIL_MUST_BE_UNIQUE
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue