Added ability for users to update their password. Minor tidy up.

This commit is contained in:
jd
2026-02-23 20:48:52 +00:00
parent 872a79663b
commit df60ad4552
5 changed files with 73 additions and 65 deletions

View File

@@ -1,6 +1,5 @@
import * as React from 'react'; import * as React from 'react';
import { brandColours } from '../utilities/helpers'; import { brandColours } from '../utilities/helpers';
import { size } from 'lodash';
interface InviteEmailProperties { interface InviteEmailProperties {
playerName: string; playerName: string;
@@ -21,57 +20,59 @@ export const InviteEmail = (props: InviteEmailProperties) => (
cellSpacing={0} cellSpacing={0}
cellPadding={0} cellPadding={0}
> >
<tr> <tbody>
<td align="center"> <tr>
<div <td align="center">
style={{ <div
padding: '20px',
borderRadius: '20px',
background: brandColours.white,
margin: '50px',
color: brandColours.dark,
maxWidth: '450px',
}}
>
<h1>You're in, {props.playerName}!</h1>
<p>
You've been invited to join {process.env.PRODUCT_NAME}, please click the button below to
finish signing up.
</p>
<p
style={{ style={{
marginBottom: '40px', padding: '20px',
borderRadius: '20px',
background: brandColours.white,
margin: '50px',
color: brandColours.dark,
maxWidth: '450px',
}} }}
> >
<a <h1>You're in, {props.playerName}!</h1>
<p>
You've been invited to join {process.env.PRODUCT_NAME}, please click the button below to
finish signing up.
</p>
<p
style={{ style={{
display: 'inline-block', marginBottom: '40px',
padding: '10px 20px',
borderRadius: '5px',
background: brandColours.primary,
textDecoration: 'none',
color: brandColours.light,
fontSize: '20px',
fontWeight: 'bold',
}} }}
href={`${process.env.ROOT_URL}/invitation/${props.inviteCode}`}
> >
Join {process.env.PRODUCT_NAME} <a
</a> style={{
</p> display: 'inline-block',
<p padding: '10px 20px',
style={{ borderRadius: '5px',
fontSize: '0.8rem', background: brandColours.primary,
opacity: '80%', textDecoration: 'none',
}} color: brandColours.light,
> fontSize: '20px',
If above button does not work, copy the link below into a new browser tab: fontWeight: 'bold',
<br /> }}
{`${process.env.ROOT_URL}/invitation/${props.inviteCode}`} href={`${process.env.ROOT_URL}/invitation/${props.inviteCode}`}
</p> >
</div> Join {process.env.PRODUCT_NAME}
</td> </a>
</tr> </p>
<p
style={{
fontSize: '0.8rem',
opacity: '80%',
}}
>
If above button does not work, copy the link below into a new browser tab:
<br />
{`${process.env.ROOT_URL}/invitation/${props.inviteCode}`}
</p>
</div>
</td>
</tr>
</tbody>
</table> </table>
</div> </div>
); );

View File

@@ -189,7 +189,7 @@ export class MatchOrm {
const player = await orm.players.get(playerId); const player = await orm.players.get(playerId);
sql.transaction(async (tx) => { await sql.transaction(async (tx) => {
const eloRefund = parseInt( const eloRefund = parseInt(
( (
await tx`SELECT elo_change FROM public.match_players WHERE match_id=${matchId.raw} AND player_id = ${playerId.raw}` await tx`SELECT elo_change FROM public.match_players WHERE match_id=${matchId.raw} AND player_id = ${playerId.raw}`

View File

@@ -24,9 +24,15 @@ export class User {
} }
export class UsersOrm { export class UsersOrm {
async create( async create({
{ email, password, playerId }: { email: string; password: string; playerId: PlayerId }, email,
): Promise<User> { password,
playerId,
}: {
email: string;
password: string;
playerId: PlayerId;
}): Promise<User> {
const existingUser: any = first( const existingUser: any = first(
await sql`SELECT id await sql`SELECT id
FROM users FROM users
@@ -100,10 +106,18 @@ export class UsersOrm {
userToUpdate.isAdmin = patch.isAdmin ?? userToUpdate.isAdmin; userToUpdate.isAdmin = patch.isAdmin ?? userToUpdate.isAdmin;
} }
await sql`UPDATE users await sql.transaction(async (tx) => {
SET is_active=${userToUpdate.isActive}, await tx`UPDATE users
is_admin=${userToUpdate.isAdmin} SET is_active=${userToUpdate.isActive},
WHERE id = ${id.raw}`; is_admin=${userToUpdate.isAdmin}
WHERE id = ${id.raw}`;
if (id === claims?.userId && patch.password) {
const passwordHash = await argon2.hash(patch.password);
await tx`UPDATE users
SET pass_hash = ${passwordHash}`;
}
});
return await this.get(id); return await this.get(id);
} }
@@ -133,10 +147,7 @@ export class UsersOrm {
return; return;
} }
async verifyCredentials( async verifyCredentials(email: string, password: string): Promise<{ userId: UserId; refreshCount: string } | null> {
email: string,
password: string,
): Promise<{ userId: UserId; refreshCount: string } | null> {
const dbResult: any = first( const dbResult: any = first(
await sql`SELECT * await sql`SELECT *
FROM users FROM users
@@ -168,12 +179,7 @@ export class UsersOrm {
return dbResult.refresh_count === refreshCount; return dbResult.refresh_count === refreshCount;
} }
async changePassword( async changePassword(id: UserId, oldPassword: string | null, newPassword: string, claims?: Claims): Promise<void> {
id: UserId,
oldPassword: string | null,
newPassword: string,
claims?: Claims,
): Promise<void> {
const isAdmin = Claims.test(Claims.ADMIN, claims); const isAdmin = Claims.test(Claims.ADMIN, claims);
if (!(isAdmin || (Claims.test(Claims.USERS.SELF.UPDATE, claims) && id === claims?.userId))) { if (!(isAdmin || (Claims.test(Claims.USERS.SELF.UPDATE, claims) && id === claims?.userId))) {
throw new UnauthorizedError(); throw new UnauthorizedError();

View File

@@ -66,6 +66,6 @@ export function unwrapMethod<T = {}>(
): (r: Request) => Promise<Response> { ): (r: Request) => Promise<Response> {
return async (request: Request) => { return async (request: Request) => {
const unwrappedRequest = await unwrap<T>(request); const unwrappedRequest = await unwrap<T>(request);
return await methodToUnwrap(unwrappedRequest); return methodToUnwrap(unwrappedRequest);
}; };
} }

View File

@@ -14,6 +14,7 @@ export interface CreateUserRequest {
export interface UpdateUserRequest { export interface UpdateUserRequest {
isActive?: boolean; isActive?: boolean;
isAdmin?: boolean; isAdmin?: boolean;
password?: string;
} }
export interface InviteUserRequest { export interface InviteUserRequest {
email: string; email: string;