forked from NotNet/gluestick
add user page, changing information
This commit is contained in:
parent
a3706faa42
commit
d0310ea0eb
|
@ -0,0 +1,73 @@
|
||||||
|
import { getUser } from "@/auth";
|
||||||
|
import { getUserInfo, setPassword, validateUser } from "@/ldap";
|
||||||
|
import { getLogger } from "@/logger";
|
||||||
|
|
||||||
|
type RequestBody = {
|
||||||
|
currentPassword: string;
|
||||||
|
newPassword: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const logger = getLogger("/api/changePassword");
|
||||||
|
|
||||||
|
const user = await getUser();
|
||||||
|
if (user == null) return new Response(null, { status: 401 });
|
||||||
|
|
||||||
|
const userInfo = await getUserInfo(user);
|
||||||
|
if (userInfo == null) {
|
||||||
|
// no user info = hasn't registered yet
|
||||||
|
return new Response(null, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { currentPassword, newPassword } =
|
||||||
|
(await request.json()) as RequestBody;
|
||||||
|
|
||||||
|
if (
|
||||||
|
currentPassword == undefined ||
|
||||||
|
typeof currentPassword !== "string" ||
|
||||||
|
newPassword == undefined ||
|
||||||
|
typeof newPassword !== "string"
|
||||||
|
) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: false,
|
||||||
|
error: "invalidBody"
|
||||||
|
}),
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const passwordMatches = await validateUser(user.username!, currentPassword);
|
||||||
|
if (!passwordMatches) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: false,
|
||||||
|
error: "incorrectPassword"
|
||||||
|
}),
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newPassword.length < 12) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: false,
|
||||||
|
error: "passwordShort"
|
||||||
|
}),
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
await setPassword(user.username!, newPassword);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: true
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
// I would use 204, but Next doesn't like it, so lol
|
||||||
|
// https://github.com/vercel/next.js/pull/48354
|
||||||
|
status: 200
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
import * as ldap from "@/ldap";
|
import * as ldap from "@/ldap";
|
||||||
import prisma from "@/prisma";
|
import prisma from "@/prisma";
|
||||||
import { getUserFromRequest } from "@/auth";
|
import { getUser } from "@/auth";
|
||||||
import { getDiscordAvatar } from "@/app/oauth/discord/oauth";
|
import { getDiscordAvatar } from "@/app/oauth/discord/oauth";
|
||||||
import { getLogger } from "@/logger";
|
import { getLogger } from "@/logger";
|
||||||
|
|
||||||
|
@ -15,7 +15,7 @@ type RequestBody = {
|
||||||
export async function POST(request: Request) {
|
export async function POST(request: Request) {
|
||||||
const logger = getLogger("/api/register");
|
const logger = getLogger("/api/register");
|
||||||
|
|
||||||
const user = await getUserFromRequest(request);
|
const user = await getUser();
|
||||||
if (user == null) return new Response(null, { status: 401 });
|
if (user == null) return new Response(null, { status: 401 });
|
||||||
|
|
||||||
if (user.username !== null) {
|
if (user.username !== null) {
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
import { getUser } from "@/auth";
|
||||||
|
import { getUserInfo, updateUser } from "@/ldap";
|
||||||
|
import { getLogger } from "@/logger";
|
||||||
|
|
||||||
|
type RequestBody = {
|
||||||
|
displayName?: string;
|
||||||
|
email?: string;
|
||||||
|
avatarBase64?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const logger = getLogger("/api/update");
|
||||||
|
|
||||||
|
const user = await getUser();
|
||||||
|
if (user == null) return new Response(null, { status: 401 });
|
||||||
|
|
||||||
|
const userInfo = await getUserInfo(user);
|
||||||
|
if (userInfo == null) {
|
||||||
|
// no user info = hasn't registered yet
|
||||||
|
return new Response(null, { status: 409 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { displayName, email, avatarBase64 } =
|
||||||
|
(await request.json()) as RequestBody;
|
||||||
|
|
||||||
|
let changeDisplayName = false;
|
||||||
|
if (
|
||||||
|
displayName !== undefined &&
|
||||||
|
typeof displayName === "string" &&
|
||||||
|
displayName !== userInfo.displayName
|
||||||
|
) {
|
||||||
|
changeDisplayName = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: when we implement migadu, make sure to update the redirect
|
||||||
|
let changeEmail = false;
|
||||||
|
if (
|
||||||
|
email !== undefined &&
|
||||||
|
typeof email === "string" &&
|
||||||
|
email !== userInfo.email
|
||||||
|
) {
|
||||||
|
changeEmail = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let avatarBuf = undefined;
|
||||||
|
if (
|
||||||
|
avatarBase64 !== undefined &&
|
||||||
|
typeof avatarBase64 === "string" &&
|
||||||
|
avatarBase64 !== userInfo.avatar
|
||||||
|
) {
|
||||||
|
avatarBuf = Buffer.from(avatarBase64, "base64");
|
||||||
|
|
||||||
|
if (avatarBuf.length > 1_000_000) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: false,
|
||||||
|
error: "avatarBig"
|
||||||
|
}),
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!changeDisplayName && !changeEmail && !avatarBuf) {
|
||||||
|
return new Response(null, { status: 200 });
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateUser(
|
||||||
|
user,
|
||||||
|
changeDisplayName ? displayName : undefined,
|
||||||
|
changeEmail ? email : undefined,
|
||||||
|
avatarBuf ?? undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
{
|
||||||
|
username: user.username,
|
||||||
|
displayName: changeDisplayName ? displayName : null,
|
||||||
|
email: changeEmail ? email : null,
|
||||||
|
avatar: avatarBuf ? avatarBuf.toString("base64") : null
|
||||||
|
},
|
||||||
|
"updated user"
|
||||||
|
);
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: true
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
|
@ -4,24 +4,36 @@
|
||||||
initial-value: #2d2a2e;
|
initial-value: #2d2a2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property --bg-dark {
|
||||||
|
syntax: "<color>";
|
||||||
|
inherits: true;
|
||||||
|
initial-value: #403e41;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property --bg-darker {
|
||||||
|
syntax: "<color>";
|
||||||
|
inherits: true;
|
||||||
|
initial-value: #221f22;
|
||||||
|
}
|
||||||
|
|
||||||
@property --fg {
|
@property --fg {
|
||||||
syntax: "<color>";
|
syntax: "<color>";
|
||||||
inherits: true;
|
inherits: true;
|
||||||
initial-value: #fcfcfa;
|
initial-value: #fcfcfa;
|
||||||
}
|
}
|
||||||
|
|
||||||
@property --bg-alt {
|
@property --fg-dark {
|
||||||
syntax: "<color>";
|
|
||||||
inherits: true;
|
|
||||||
initial-value: #403e41;
|
|
||||||
}
|
|
||||||
|
|
||||||
@property --fg-alt {
|
|
||||||
syntax: "<color>";
|
syntax: "<color>";
|
||||||
inherits: true;
|
inherits: true;
|
||||||
initial-value: #727072;
|
initial-value: #727072;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@property --fg-darker {
|
||||||
|
syntax: "<color>";
|
||||||
|
inherits: true;
|
||||||
|
initial-value: #5b595c;
|
||||||
|
}
|
||||||
|
|
||||||
@property --error {
|
@property --error {
|
||||||
syntax: "<color>";
|
syntax: "<color>";
|
||||||
inherits: true;
|
inherits: true;
|
||||||
|
@ -69,7 +81,7 @@ button {
|
||||||
}
|
}
|
||||||
|
|
||||||
input::placeholder {
|
input::placeholder {
|
||||||
color: var(--fg-alt);
|
color: var(--fg-dark);
|
||||||
transition: color var(--theme-transition);
|
transition: color var(--theme-transition);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,103 @@
|
||||||
|
.content {
|
||||||
|
max-width: 500px;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 1rem;
|
||||||
|
padding-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form input[type="submit"] {
|
||||||
|
padding: 1rem 1.5rem;
|
||||||
|
font-size: 140%;
|
||||||
|
background: var(--bg-dark);
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0.15rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 2rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonContainer input:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
color: var(--fg-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formRow {
|
||||||
|
margin: 1rem 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formRow label {
|
||||||
|
font-variant: all-small-caps;
|
||||||
|
font-size: 105%;
|
||||||
|
width: 100px;
|
||||||
|
height: 50px;
|
||||||
|
|
||||||
|
/* center */
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formVert {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fancyInput {
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.15rem;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
width: 250px;
|
||||||
|
display: block;
|
||||||
|
background: var(--bg-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formRow input[name="avatar"] {
|
||||||
|
width: 190px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formRow .avatar {
|
||||||
|
margin-right: 10px;
|
||||||
|
border-radius: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formRow input:disabled {
|
||||||
|
cursor: not-allowed;
|
||||||
|
background: var(--bg-darker);
|
||||||
|
color: var(--fg-darker);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint {
|
||||||
|
color: var(--fg-dark);
|
||||||
|
font-size: 80%;
|
||||||
|
transition: color var(--theme-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: var(--error);
|
||||||
|
font-size: 80%;
|
||||||
|
transition: color var(--theme-transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
width: 400px;
|
||||||
|
margin: auto;
|
||||||
|
|
||||||
|
background-color: var(--fg-darker);
|
||||||
|
height: 1px;
|
||||||
|
border: none;
|
||||||
|
}
|
|
@ -0,0 +1,283 @@
|
||||||
|
/* eslint-disable @next/next/no-img-element */
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import { UserInfo } from "@/ldap";
|
||||||
|
import React, { HTMLInputTypeAttribute, InputHTMLAttributes } from "react";
|
||||||
|
import styles from "./AboutMe.module.css";
|
||||||
|
|
||||||
|
const fallbackAvatar = "https://i.clong.biz/i/oc4zjlqr.png";
|
||||||
|
|
||||||
|
type UpdateResponse = {
|
||||||
|
ok: boolean;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// TODO skip do your magic
|
||||||
|
type InputProps = {
|
||||||
|
label: string;
|
||||||
|
name: string;
|
||||||
|
type: HTMLInputTypeAttribute;
|
||||||
|
error?: string;
|
||||||
|
displayImage?: string;
|
||||||
|
} & InputHTMLAttributes<HTMLInputElement>;
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>((props, ref) => {
|
||||||
|
// get console to shut up
|
||||||
|
const inputProps = { ...props };
|
||||||
|
delete inputProps.displayImage;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.formRow}>
|
||||||
|
<label htmlFor={props.id}>{props.label}</label>
|
||||||
|
|
||||||
|
{props.displayImage && (
|
||||||
|
<img
|
||||||
|
src={props.displayImage}
|
||||||
|
className={styles.avatar}
|
||||||
|
alt={"Your avatar"}
|
||||||
|
width="50px"
|
||||||
|
height="50px"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className={styles.formVert}>
|
||||||
|
<input {...inputProps} ref={ref} className={styles.fancyInput} />
|
||||||
|
|
||||||
|
{props.error != null && <p className={styles.error}>{props.error}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function AboutMe({ info }: { info: UserInfo }) {
|
||||||
|
const displayNameRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const emailRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const avatarRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const submitRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [avatar, setAvatar] = React.useState<string | null>(
|
||||||
|
info.avatar ?? null
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentPasswordRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const newPasswordRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const confirmPasswordRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
const submitPasswordRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [incorrectPassword, setIncorrectPassword] = React.useState(false);
|
||||||
|
const [passwordMismatch, setPasswordMismatch] = React.useState(false);
|
||||||
|
const [avatarBig, setAvatarBig] = React.useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.content}>
|
||||||
|
<h2 className={styles.header}>User information</h2>
|
||||||
|
<form
|
||||||
|
onSubmit={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// turn the data uri into just base64
|
||||||
|
const avatarChanged = avatar !== null && avatar !== info.avatar;
|
||||||
|
const avatarData = avatarChanged ? avatar?.split(",")[1] : null;
|
||||||
|
|
||||||
|
submitRef.current!.disabled = true;
|
||||||
|
const req = await fetch("/api/update", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
displayName: displayNameRef.current?.value,
|
||||||
|
email: emailRef.current?.value,
|
||||||
|
avatar: avatarData
|
||||||
|
})
|
||||||
|
});
|
||||||
|
submitRef.current!.disabled = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: UpdateResponse = await req.json();
|
||||||
|
|
||||||
|
if (!res.ok && res.error !== null) {
|
||||||
|
switch (res.error) {
|
||||||
|
case "avatarBig":
|
||||||
|
setAvatarBig(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.error(req);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
label="Username"
|
||||||
|
defaultValue={info.username}
|
||||||
|
disabled
|
||||||
|
title="You can't change your username."
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
name="display-name"
|
||||||
|
label="Display name"
|
||||||
|
defaultValue={info.displayName}
|
||||||
|
ref={displayNameRef}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="email"
|
||||||
|
name="email"
|
||||||
|
label="Email"
|
||||||
|
defaultValue={info.email}
|
||||||
|
ref={emailRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* why, html gods, why? */}
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
name="avatar"
|
||||||
|
accept="image/png, image/jpeg"
|
||||||
|
ref={avatarRef}
|
||||||
|
style={{ display: "none" }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="button"
|
||||||
|
value="Choose file"
|
||||||
|
name="avatar"
|
||||||
|
label="Avatar"
|
||||||
|
accept="image/png, image/jpeg"
|
||||||
|
error={avatarBig ? "Avatar is too big." : undefined}
|
||||||
|
onClick={() => {
|
||||||
|
avatarRef.current?.click();
|
||||||
|
|
||||||
|
const eventListener = async () => {
|
||||||
|
avatarRef.current?.removeEventListener("change", eventListener);
|
||||||
|
|
||||||
|
const file = avatarRef.current?.files?.[0];
|
||||||
|
if (file == null) return;
|
||||||
|
|
||||||
|
if (file.size > 1_000_000) {
|
||||||
|
setAvatarBig(true);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
setAvatarBig(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
const b64 = await fileAsBase64(file);
|
||||||
|
setAvatar(`data:${file.type};base64,${b64}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
avatarRef.current?.addEventListener("change", eventListener);
|
||||||
|
}}
|
||||||
|
displayImage={avatar ?? fallbackAvatar}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.formRow}>
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
value="Save"
|
||||||
|
ref={submitRef}
|
||||||
|
className={styles.fancyInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<hr className={styles.divider} />
|
||||||
|
|
||||||
|
<h2 className={styles.header}>Change password</h2>
|
||||||
|
<form
|
||||||
|
onSubmit={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setIncorrectPassword(false);
|
||||||
|
setPasswordMismatch(false);
|
||||||
|
|
||||||
|
if (
|
||||||
|
newPasswordRef.current?.value !== confirmPasswordRef.current?.value
|
||||||
|
) {
|
||||||
|
setPasswordMismatch(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitPasswordRef.current!.disabled = true;
|
||||||
|
const req = await fetch("/api/changePassword", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
currentPassword: currentPasswordRef.current?.value,
|
||||||
|
newPassword: newPasswordRef.current?.value
|
||||||
|
})
|
||||||
|
});
|
||||||
|
submitPasswordRef.current!.disabled = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: UpdateResponse = await req.json();
|
||||||
|
|
||||||
|
if (!res.ok && res.error !== null) {
|
||||||
|
switch (res.error) {
|
||||||
|
case "incorrectPassword":
|
||||||
|
setIncorrectPassword(true);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
console.error(req);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
name="current-password"
|
||||||
|
label="Current"
|
||||||
|
minLength={12}
|
||||||
|
required
|
||||||
|
ref={currentPasswordRef}
|
||||||
|
error={incorrectPassword ? "Incorrect password." : undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
name="new-password"
|
||||||
|
label="New"
|
||||||
|
minLength={12}
|
||||||
|
required
|
||||||
|
ref={newPasswordRef}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
name="confirm-password"
|
||||||
|
label="Confirm"
|
||||||
|
ref={confirmPasswordRef}
|
||||||
|
minLength={12}
|
||||||
|
required
|
||||||
|
error={passwordMismatch ? "Passwords do not match." : undefined}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className={styles.formRow}>
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
value="Change password"
|
||||||
|
ref={submitPasswordRef}
|
||||||
|
className={styles.fancyInput}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,11 +1,19 @@
|
||||||
import { getUserFromPage } from "@/auth";
|
import { getUser } from "@/auth";
|
||||||
|
import { getUserInfo } from "@/ldap";
|
||||||
|
import AboutMe from "./AboutMe";
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const user = await getUserFromPage();
|
const user = await getUser();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return <p>Not logged in</p>;
|
window.location.href = "/login";
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return <p>{user.username}</p>;
|
const info = await getUserInfo(user);
|
||||||
|
if (info === null) {
|
||||||
|
window.location.href = "/login";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <AboutMe info={info} />;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
.form input[type="submit"] {
|
.form input[type="submit"] {
|
||||||
padding: 1rem 1.5rem;
|
padding: 1rem 1.5rem;
|
||||||
font-size: 140%;
|
font-size: 140%;
|
||||||
background: var(--bg-alt);
|
background: var(--bg-dark);
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 0.15rem;
|
border-radius: 0.15rem;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
.buttonContainer input:disabled {
|
.buttonContainer input:disabled {
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
color: var(--fg-alt);
|
color: var(--fg-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.formRow {
|
.formRow {
|
||||||
|
@ -40,11 +40,11 @@
|
||||||
margin: 0.5rem 0;
|
margin: 0.5rem 0;
|
||||||
width: 250px;
|
width: 250px;
|
||||||
display: block;
|
display: block;
|
||||||
background: var(--bg-alt);
|
background: var(--bg-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hint {
|
.hint {
|
||||||
color: var(--fg-alt);
|
color: var(--fg-dark);
|
||||||
font-size: 80%;
|
font-size: 80%;
|
||||||
transition: color var(--theme-transition);
|
transition: color var(--theme-transition);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@
|
||||||
import React, { InputHTMLAttributes } from "react";
|
import React, { InputHTMLAttributes } from "react";
|
||||||
import { HTMLInputTypeAttribute } from "react";
|
import { HTMLInputTypeAttribute } from "react";
|
||||||
import styles from "./RegisterForm.module.css";
|
import styles from "./RegisterForm.module.css";
|
||||||
import { cookies } from "next/dist/client/components/headers";
|
|
||||||
|
|
||||||
type RegisterResponse = {
|
type RegisterResponse = {
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
|
@ -47,7 +46,7 @@ async function fileAsBase64(f: File) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function RegisterForm({ ticket }: { ticket: string }) {
|
export default function RegisterForm() {
|
||||||
const usernameRef = React.useRef<HTMLInputElement>(null);
|
const usernameRef = React.useRef<HTMLInputElement>(null);
|
||||||
const displayNameRef = React.useRef<HTMLInputElement>(null);
|
const displayNameRef = React.useRef<HTMLInputElement>(null);
|
||||||
const emailRef = React.useRef<HTMLInputElement>(null);
|
const emailRef = React.useRef<HTMLInputElement>(null);
|
||||||
|
@ -92,8 +91,7 @@ export default function RegisterForm({ ticket }: { ticket: string }) {
|
||||||
const req = await fetch(`/api/register`, {
|
const req = await fetch(`/api/register`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json"
|
||||||
Authorization: "Bearer " + ticket
|
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
username,
|
username,
|
||||||
|
|
|
@ -6,12 +6,13 @@ export default function Page() {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies();
|
||||||
const ticket = cookieStore.get("ticket");
|
const ticket = cookieStore.get("ticket");
|
||||||
if (ticket === null) {
|
if (ticket === null) {
|
||||||
return <div>Ticket is null?</div>;
|
window.location.href = "/";
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={styles.main}>
|
<main className={styles.main}>
|
||||||
<RegisterForm ticket={ticket!.value} />
|
<RegisterForm />
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
31
src/auth.ts
31
src/auth.ts
|
@ -6,15 +6,14 @@ import { getLogger } from "./logger";
|
||||||
|
|
||||||
const logger = getLogger("auth.ts");
|
const logger = getLogger("auth.ts");
|
||||||
|
|
||||||
export async function getUserFromRequest(request: Request) {
|
export async function getUser() {
|
||||||
const authorization = request.headers
|
const cookieStore = cookies();
|
||||||
.get("authorization")
|
const cookieTicket = cookieStore.get("ticket");
|
||||||
?.replace("Bearer ", "");
|
if (cookieTicket === null) return null;
|
||||||
if (authorization === null) return null;
|
|
||||||
|
|
||||||
const ticket = await prisma.authTicket.findFirst({
|
const ticket = await prisma.authTicket.findFirst({
|
||||||
where: {
|
where: {
|
||||||
ticket: authorization
|
ticket: cookieTicket?.value
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
if (ticket === null) return null;
|
if (ticket === null) return null;
|
||||||
|
@ -49,26 +48,6 @@ export async function getUserFromRequest(request: Request) {
|
||||||
return user;
|
return user;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getUserFromPage() {
|
|
||||||
const cookieStore = cookies();
|
|
||||||
const cookieTicket = cookieStore.get("ticket");
|
|
||||||
if (cookieTicket === null) return null;
|
|
||||||
|
|
||||||
const ticket = await prisma.authTicket.findFirst({
|
|
||||||
where: {
|
|
||||||
ticket: cookieTicket?.value
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (ticket === null) return null;
|
|
||||||
|
|
||||||
const user = await prisma.user.findFirst({
|
|
||||||
where: {
|
|
||||||
id: ticket.userId
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return user;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createAuthTicket(username: string) {
|
export async function createAuthTicket(username: string) {
|
||||||
let user = await prisma.user.findFirst({
|
let user = await prisma.user.findFirst({
|
||||||
where: {
|
where: {
|
||||||
|
|
|
@ -6,10 +6,15 @@ import Image from "next/image";
|
||||||
|
|
||||||
type ColorScheme = {
|
type ColorScheme = {
|
||||||
name: string;
|
name: string;
|
||||||
|
|
||||||
bg: string;
|
bg: string;
|
||||||
bgAlt: string;
|
bgDark: string;
|
||||||
|
bgDarker?: string;
|
||||||
|
|
||||||
fg: string;
|
fg: string;
|
||||||
fgAlt: string;
|
fgDark: string;
|
||||||
|
fgDarker?: string;
|
||||||
|
|
||||||
error?: string;
|
error?: string;
|
||||||
warning?: string;
|
warning?: string;
|
||||||
};
|
};
|
||||||
|
@ -18,88 +23,90 @@ const colors: ColorScheme[] = [
|
||||||
{
|
{
|
||||||
name: "Monokai Pro",
|
name: "Monokai Pro",
|
||||||
bg: "#2d2a2e",
|
bg: "#2d2a2e",
|
||||||
bgAlt: "#403e41",
|
bgDark: "#403e41",
|
||||||
|
bgDarker: "#221f22",
|
||||||
fg: "#fcfcfa",
|
fg: "#fcfcfa",
|
||||||
fgAlt: "#727072",
|
fgDark: "#727072",
|
||||||
|
fgDarker: "#5b595c",
|
||||||
error: "#ff6188",
|
error: "#ff6188",
|
||||||
warning: "#ffd866"
|
warning: "#ffd866"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Gruvbox Dark",
|
name: "Gruvbox Dark",
|
||||||
bg: "#282828",
|
bg: "#282828",
|
||||||
bgAlt: "#3c3836",
|
bgDark: "#3c3836",
|
||||||
fg: "#ebdbb2",
|
fg: "#ebdbb2",
|
||||||
fgAlt: "#a89984"
|
fgDark: "#a89984"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Amora",
|
name: "Amora",
|
||||||
bg: "#1a1a1a",
|
bg: "#1a1a1a",
|
||||||
bgAlt: "#171717",
|
bgDark: "#171717",
|
||||||
fg: "#DEDBEB",
|
fg: "#DEDBEB",
|
||||||
fgAlt: "#5c5c5c"
|
fgDark: "#5c5c5c"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Amora Focus",
|
name: "Amora Focus",
|
||||||
bg: "#302838",
|
bg: "#302838",
|
||||||
bgAlt: "#2a2331",
|
bgDark: "#2a2331",
|
||||||
fg: "#dedbeb",
|
fg: "#dedbeb",
|
||||||
fgAlt: "#5c5c5c"
|
fgDark: "#5c5c5c"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Rosé Pine",
|
name: "Rosé Pine",
|
||||||
bg: "#191724",
|
bg: "#191724",
|
||||||
bgAlt: "#26233a",
|
bgDark: "#26233a",
|
||||||
fg: "#e0def4",
|
fg: "#e0def4",
|
||||||
fgAlt: "#908caa"
|
fgDark: "#908caa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Rosé Pine Moon",
|
name: "Rosé Pine Moon",
|
||||||
bg: "#232136",
|
bg: "#232136",
|
||||||
bgAlt: "#393552",
|
bgDark: "#393552",
|
||||||
fg: "#e0def4",
|
fg: "#e0def4",
|
||||||
fgAlt: "#908caa"
|
fgDark: "#908caa"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Nord",
|
name: "Nord",
|
||||||
bg: "#2e3440",
|
bg: "#2e3440",
|
||||||
bgAlt: "#3b4252",
|
bgDark: "#3b4252",
|
||||||
fg: "#eceff4",
|
fg: "#eceff4",
|
||||||
fgAlt: "#d8dee9"
|
fgDark: "#d8dee9"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "lovelace",
|
name: "lovelace",
|
||||||
bg: "#1d1f28",
|
bg: "#1d1f28",
|
||||||
bgAlt: "#282a36",
|
bgDark: "#282a36",
|
||||||
fg: "#fdfdfd",
|
fg: "#fdfdfd",
|
||||||
fgAlt: "#414458"
|
fgDark: "#414458"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "skyfall",
|
name: "skyfall",
|
||||||
bg: "#282f37",
|
bg: "#282f37",
|
||||||
bgAlt: "#20262c",
|
bgDark: "#20262c",
|
||||||
fg: "#f1fcf9",
|
fg: "#f1fcf9",
|
||||||
fgAlt: "#465463"
|
fgDark: "#465463"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Catppuccin Frappe",
|
name: "Catppuccin Frappe",
|
||||||
bg: "#303446",
|
bg: "#303446",
|
||||||
bgAlt: "#51576d",
|
bgDark: "#51576d",
|
||||||
fg: "#c6d0f5",
|
fg: "#c6d0f5",
|
||||||
fgAlt: "#a5adce"
|
fgDark: "#a5adce"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Catppuccin Macchiato",
|
name: "Catppuccin Macchiato",
|
||||||
bg: "#24273a",
|
bg: "#24273a",
|
||||||
bgAlt: "#494d64",
|
bgDark: "#494d64",
|
||||||
fg: "#cad3f5",
|
fg: "#cad3f5",
|
||||||
fgAlt: "#a5adcb"
|
fgDark: "#a5adcb"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Catppuccin Mocha",
|
name: "Catppuccin Mocha",
|
||||||
bg: "#1e1e2e",
|
bg: "#1e1e2e",
|
||||||
bgAlt: "#45475a",
|
bgDark: "#45475a",
|
||||||
fg: "#cdd6f4",
|
fg: "#cdd6f4",
|
||||||
fgAlt: "#a6adc8"
|
fgDark: "#a6adc8"
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
@ -122,9 +129,9 @@ export default function ColorChanger() {
|
||||||
|
|
||||||
const fixedColors = {
|
const fixedColors = {
|
||||||
"--bg": colorScheme.bg,
|
"--bg": colorScheme.bg,
|
||||||
"--bg-alt": colorScheme.bgAlt,
|
"--bg-dark": colorScheme.bgDark,
|
||||||
"--fg": colorScheme.fg,
|
"--fg": colorScheme.fg,
|
||||||
"--fg-alt": colorScheme.fgAlt,
|
"--fg-dark": colorScheme.fgDark,
|
||||||
"--error": colorScheme.error ?? fallback.error!,
|
"--error": colorScheme.error ?? fallback.error!,
|
||||||
"--warning": colorScheme.warning ?? fallback.warning!
|
"--warning": colorScheme.warning ?? fallback.warning!
|
||||||
};
|
};
|
||||||
|
|
73
src/ldap.ts
73
src/ldap.ts
|
@ -3,7 +3,7 @@ import { Client } from "ldapts";
|
||||||
import { gql } from "./__generated__";
|
import { gql } from "./__generated__";
|
||||||
import sharp from "sharp";
|
import sharp from "sharp";
|
||||||
import { BerWriter } from "asn1";
|
import { BerWriter } from "asn1";
|
||||||
import prisma from "./prisma";
|
import { User } from "@prisma/client";
|
||||||
|
|
||||||
type LLDAPAuthResponse = {
|
type LLDAPAuthResponse = {
|
||||||
token: string;
|
token: string;
|
||||||
|
@ -14,6 +14,13 @@ type LLDAPRefreshResponse = {
|
||||||
token: string;
|
token: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type UserInfo = {
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
email: string;
|
||||||
|
avatar?: string;
|
||||||
|
};
|
||||||
|
|
||||||
let ldapClient: Client | null = null;
|
let ldapClient: Client | null = null;
|
||||||
async function getLdapClient() {
|
async function getLdapClient() {
|
||||||
if (ldapClient === null) {
|
if (ldapClient === null) {
|
||||||
|
@ -173,3 +180,67 @@ export async function checkUserExists(username: string) {
|
||||||
(u) => u.id.toLowerCase() === username.toLowerCase()
|
(u) => u.id.toLowerCase() === username.toLowerCase()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getUserInfo(user: User) {
|
||||||
|
if (user.username === null) return null;
|
||||||
|
|
||||||
|
const client = await getGraphQLClient();
|
||||||
|
const mutation = await client.query({
|
||||||
|
query: gql(`
|
||||||
|
query GetUser($userId: String!) {
|
||||||
|
user(userId: $userId) {
|
||||||
|
id
|
||||||
|
email
|
||||||
|
displayName
|
||||||
|
avatar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
variables: {
|
||||||
|
userId: user.username
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mutationAvatar = mutation.data.user.avatar;
|
||||||
|
|
||||||
|
const avatar = mutationAvatar
|
||||||
|
? `data:image/jpeg;base64,${mutationAvatar}`
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const userInfo: UserInfo = {
|
||||||
|
username: mutation.data.user.id,
|
||||||
|
displayName: mutation.data.user.displayName,
|
||||||
|
email: mutation.data.user.email,
|
||||||
|
avatar
|
||||||
|
};
|
||||||
|
|
||||||
|
return userInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateUser(
|
||||||
|
user: User,
|
||||||
|
displayName?: string,
|
||||||
|
email?: string,
|
||||||
|
avatar?: Buffer
|
||||||
|
) {
|
||||||
|
if (user.username === null) return;
|
||||||
|
|
||||||
|
const client = await getGraphQLClient();
|
||||||
|
const mutation = await client.mutate({
|
||||||
|
mutation: gql(`
|
||||||
|
mutation UpdateUser($user: UpdateUserInput!) {
|
||||||
|
updateUser(user: $user) {
|
||||||
|
ok
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`),
|
||||||
|
variables: {
|
||||||
|
user: {
|
||||||
|
id: user.username,
|
||||||
|
displayName,
|
||||||
|
email,
|
||||||
|
avatar: avatar ? await ensureJpg(avatar) : undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue