310 lines
8.3 KiB
TypeScript
310 lines
8.3 KiB
TypeScript
/* eslint-disable @next/next/no-img-element */
|
|
"use client";
|
|
|
|
import { UserInfo } from "@/ldap";
|
|
import React from "react";
|
|
import styles from "./AboutMe.module.css";
|
|
import AvatarChanger from "@/components/AvatarChanger";
|
|
import Input from "@/components/Input";
|
|
import { Form, Formik, FormikHelpers } from "formik";
|
|
import {
|
|
AboutMeFormValues,
|
|
PasswordUpdateFormValues,
|
|
aboutMeSchema,
|
|
passwordUpdateSchema
|
|
} from "@/schemas";
|
|
import PrettyForm from "@/components/PrettyForm";
|
|
import Toast from "@/components/Toast";
|
|
import { AuthProviderState } from "@/auth/AuthProvider";
|
|
import { exec } from "child_process";
|
|
|
|
type UpdateResponse = {
|
|
ok: boolean;
|
|
error?: string;
|
|
};
|
|
|
|
async function fileAsBase64(f: File) {
|
|
const reader = new FileReader();
|
|
reader.readAsArrayBuffer(f);
|
|
return new Promise<string>((resolve, reject) => {
|
|
reader.onload = () => {
|
|
const result = reader.result as ArrayBuffer;
|
|
const buffer = Buffer.from(result);
|
|
resolve(buffer.toString("base64"));
|
|
};
|
|
reader.onerror = () => reject(reader.error);
|
|
});
|
|
}
|
|
|
|
function AuthProviderButton({ provider }: { provider: AuthProviderState }) {
|
|
// bullshit hack
|
|
const holdTime = provider.connected ? 3000 : 0;
|
|
const interval = React.useRef<NodeJS.Timeout | null>();
|
|
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
|
|
const execute = async () => {
|
|
const name = provider.name.toLowerCase();
|
|
if (!provider.connected) {
|
|
window.location.href = `/oauth/${name}/login`;
|
|
} else {
|
|
await fetch(`/api/unlink?provider=${name}`, { method: "POST" });
|
|
window.location.reload();
|
|
}
|
|
};
|
|
|
|
const mouseDown = () => {
|
|
interval.current = setTimeout(execute, holdTime);
|
|
};
|
|
|
|
const mouseUp = () => {
|
|
if (interval.current) clearTimeout(interval.current);
|
|
};
|
|
|
|
return (
|
|
<input
|
|
type="submit"
|
|
className={
|
|
styles.fancyInput + " " + (provider.connected ? styles.progress : "")
|
|
}
|
|
onMouseDown={mouseDown}
|
|
onMouseUp={mouseUp}
|
|
value={provider.connected ? "Disconnect" : "Connect"}
|
|
ref={inputRef}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function AuthProviderEntry({ provider }: { provider: AuthProviderState }) {
|
|
return (
|
|
<>
|
|
<p>
|
|
{provider.name}:{" "}
|
|
{provider.connected ? provider.username : "Not connected"}
|
|
</p>
|
|
|
|
<AuthProviderButton provider={provider} />
|
|
</>
|
|
);
|
|
}
|
|
|
|
export default function AboutMe({
|
|
info,
|
|
providers
|
|
}: {
|
|
info: UserInfo;
|
|
providers: AuthProviderState[];
|
|
}) {
|
|
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
|
const [madeProfileChanges, setMadeChanges] = React.useState(false);
|
|
const [madePasswordChanges, setMadePasswordChanges] = React.useState(false);
|
|
|
|
const initialValues: AboutMeFormValues = {
|
|
username: info.username,
|
|
displayName: info.displayName,
|
|
email: info.email,
|
|
avatar: info.avatar
|
|
};
|
|
|
|
async function handleFormSubmit(
|
|
{ displayName, email, avatar }: AboutMeFormValues,
|
|
{ setSubmitting }: FormikHelpers<AboutMeFormValues>
|
|
) {
|
|
setMadeChanges(false);
|
|
setSubmitting(true);
|
|
const req = await fetch("/api/update", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
displayName,
|
|
email,
|
|
avatar: avatar != null ? avatar.split(",")[1] : null
|
|
})
|
|
});
|
|
setSubmitting(false);
|
|
|
|
try {
|
|
const res: UpdateResponse = await req.json();
|
|
|
|
if (!res.ok && res.error !== null) {
|
|
switch (res.error) {
|
|
case "avatarBig":
|
|
break;
|
|
}
|
|
}
|
|
setMadeChanges(true);
|
|
} catch {
|
|
console.error(req);
|
|
}
|
|
}
|
|
|
|
const [passwordError, setPasswordError] = React.useState<string | null>(null);
|
|
const initialPasswordValues: PasswordUpdateFormValues = {
|
|
password: "",
|
|
newPassword: "",
|
|
confirmPassword: ""
|
|
};
|
|
|
|
async function handlePasswordSubmit(
|
|
{ password, newPassword }: PasswordUpdateFormValues,
|
|
{ setFieldError, setSubmitting }: FormikHelpers<PasswordUpdateFormValues>
|
|
) {
|
|
setMadePasswordChanges(false);
|
|
setSubmitting(true);
|
|
const req = await fetch("/api/changePassword", {
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json"
|
|
},
|
|
body: JSON.stringify({
|
|
currentPassword: password,
|
|
newPassword: newPassword
|
|
})
|
|
});
|
|
setSubmitting(false);
|
|
|
|
try {
|
|
const res: UpdateResponse = await req.json();
|
|
|
|
if (!res.ok && res.error !== null) {
|
|
switch (res.error) {
|
|
case "incorrectPassword":
|
|
setFieldError("password", "Incorrect password.");
|
|
break;
|
|
}
|
|
}
|
|
setMadePasswordChanges(true);
|
|
} catch {
|
|
console.error(req);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className={styles.content}>
|
|
<h2 className={styles.userName}>{info.username}</h2>
|
|
<PrettyForm globalError={globalError}>
|
|
<Formik
|
|
initialValues={initialValues}
|
|
onSubmit={handleFormSubmit}
|
|
validationSchema={aboutMeSchema}
|
|
>
|
|
{({ isSubmitting }) => (
|
|
<Form className={styles.profileGrid}>
|
|
{madeProfileChanges ? <Toast>Saved your changes.</Toast> : null}
|
|
<Input
|
|
type="text"
|
|
name="username"
|
|
label="Username"
|
|
defaultValue={info.username}
|
|
disabled
|
|
title="You can't change your username."
|
|
/>
|
|
<Input
|
|
type="text"
|
|
name="displayName"
|
|
label="Display name"
|
|
defaultValue={info.displayName}
|
|
/>
|
|
<Input
|
|
type="email"
|
|
name="email"
|
|
label="Email"
|
|
defaultValue={info.email}
|
|
/>
|
|
|
|
<Input
|
|
type="file"
|
|
name="avatar"
|
|
label="Avatar"
|
|
accept="image/png, image/jpeg"
|
|
customRender={(fieldProps) => (
|
|
<AvatarChanger
|
|
currentAvatarBlob={fieldProps.field.value}
|
|
onChange={(newBlob) =>
|
|
fieldProps.form.setFieldValue("avatar", newBlob)
|
|
}
|
|
/>
|
|
)}
|
|
/>
|
|
|
|
<input
|
|
type="submit"
|
|
value="Save"
|
|
className={styles.fancyInput}
|
|
disabled={isSubmitting}
|
|
/>
|
|
</Form>
|
|
)}
|
|
</Formik>
|
|
</PrettyForm>
|
|
<hr className={styles.divider} />
|
|
<h2 className={styles.header}>Change password</h2>
|
|
<PrettyForm globalError={passwordError}>
|
|
<Formik
|
|
initialValues={initialPasswordValues}
|
|
onSubmit={handlePasswordSubmit}
|
|
validationSchema={passwordUpdateSchema}
|
|
>
|
|
{({ isSubmitting }) => (
|
|
<Form>
|
|
{madePasswordChanges ? (
|
|
<Toast>Changed your password.</Toast>
|
|
) : null}
|
|
<Input
|
|
type="password"
|
|
name="password"
|
|
label="Current"
|
|
minLength={12}
|
|
required
|
|
/>
|
|
|
|
<Input
|
|
type="password"
|
|
name="newPassword"
|
|
label="New"
|
|
minLength={12}
|
|
required
|
|
/>
|
|
|
|
<Input
|
|
type="password"
|
|
name="confirmPassword"
|
|
label="Confirm"
|
|
minLength={12}
|
|
required
|
|
/>
|
|
|
|
<input
|
|
type="submit"
|
|
value="Save"
|
|
className={styles.fancyInput}
|
|
disabled={isSubmitting}
|
|
/>
|
|
</Form>
|
|
)}
|
|
</Formik>
|
|
</PrettyForm>
|
|
|
|
<hr className={styles.divider} />
|
|
<h2 className={styles.header}>Connections</h2>
|
|
<div className={styles.authProviderList}>
|
|
{providers.map((provider) => (
|
|
<AuthProviderEntry provider={provider} key={provider.name} />
|
|
))}
|
|
</div>
|
|
|
|
<hr className={styles.divider} />
|
|
<input
|
|
type="button"
|
|
value="Log out"
|
|
className={styles.logout}
|
|
onClick={async () => {
|
|
document.cookie =
|
|
"ticket=; expires=" + new Date().toUTCString() + "; path=/";
|
|
window.location.href = "/";
|
|
}}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|