forked from NotNet/gluestick
add user page, changing information
This commit is contained in:
parent
a3706faa42
commit
d0310ea0eb
13 changed files with 708 additions and 80 deletions
73
src/app/api/changePassword/route.ts
Normal file
73
src/app/api/changePassword/route.ts
Normal file
|
@ -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 prisma from "@/prisma";
|
||||
import { getUserFromRequest } from "@/auth";
|
||||
import { getUser } from "@/auth";
|
||||
import { getDiscordAvatar } from "@/app/oauth/discord/oauth";
|
||||
import { getLogger } from "@/logger";
|
||||
|
||||
|
@ -15,7 +15,7 @@ type RequestBody = {
|
|||
export async function POST(request: Request) {
|
||||
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.username !== null) {
|
||||
|
|
93
src/app/api/update/route.ts
Normal file
93
src/app/api/update/route.ts
Normal file
|
@ -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;
|
||||
}
|
||||
|
||||
@property --bg-dark {
|
||||
syntax: "<color>";
|
||||
inherits: true;
|
||||
initial-value: #403e41;
|
||||
}
|
||||
|
||||
@property --bg-darker {
|
||||
syntax: "<color>";
|
||||
inherits: true;
|
||||
initial-value: #221f22;
|
||||
}
|
||||
|
||||
@property --fg {
|
||||
syntax: "<color>";
|
||||
inherits: true;
|
||||
initial-value: #fcfcfa;
|
||||
}
|
||||
|
||||
@property --bg-alt {
|
||||
syntax: "<color>";
|
||||
inherits: true;
|
||||
initial-value: #403e41;
|
||||
}
|
||||
|
||||
@property --fg-alt {
|
||||
@property --fg-dark {
|
||||
syntax: "<color>";
|
||||
inherits: true;
|
||||
initial-value: #727072;
|
||||
}
|
||||
|
||||
@property --fg-darker {
|
||||
syntax: "<color>";
|
||||
inherits: true;
|
||||
initial-value: #5b595c;
|
||||
}
|
||||
|
||||
@property --error {
|
||||
syntax: "<color>";
|
||||
inherits: true;
|
||||
|
@ -69,7 +81,7 @@ button {
|
|||
}
|
||||
|
||||
input::placeholder {
|
||||
color: var(--fg-alt);
|
||||
color: var(--fg-dark);
|
||||
transition: color var(--theme-transition);
|
||||
}
|
||||
|
||||
|
|
103
src/app/me/AboutMe.module.css
Normal file
103
src/app/me/AboutMe.module.css
Normal file
|
@ -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;
|
||||
}
|
283
src/app/me/AboutMe.tsx
Normal file
283
src/app/me/AboutMe.tsx
Normal file
|
@ -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() {
|
||||
const user = await getUserFromPage();
|
||||
|
||||
const user = await getUser();
|
||||
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"] {
|
||||
padding: 1rem 1.5rem;
|
||||
font-size: 140%;
|
||||
background: var(--bg-alt);
|
||||
background: var(--bg-dark);
|
||||
border: 0;
|
||||
border-radius: 0.15rem;
|
||||
cursor: pointer;
|
||||
|
@ -20,7 +20,7 @@
|
|||
|
||||
.buttonContainer input:disabled {
|
||||
cursor: not-allowed;
|
||||
color: var(--fg-alt);
|
||||
color: var(--fg-dark);
|
||||
}
|
||||
|
||||
.formRow {
|
||||
|
@ -40,11 +40,11 @@
|
|||
margin: 0.5rem 0;
|
||||
width: 250px;
|
||||
display: block;
|
||||
background: var(--bg-alt);
|
||||
background: var(--bg-dark);
|
||||
}
|
||||
|
||||
.hint {
|
||||
color: var(--fg-alt);
|
||||
color: var(--fg-dark);
|
||||
font-size: 80%;
|
||||
transition: color var(--theme-transition);
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
import React, { InputHTMLAttributes } from "react";
|
||||
import { HTMLInputTypeAttribute } from "react";
|
||||
import styles from "./RegisterForm.module.css";
|
||||
import { cookies } from "next/dist/client/components/headers";
|
||||
|
||||
type RegisterResponse = {
|
||||
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 displayNameRef = 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`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Authorization: "Bearer " + ticket
|
||||
"Content-Type": "application/json"
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username,
|
||||
|
|
|
@ -6,12 +6,13 @@ export default function Page() {
|
|||
const cookieStore = cookies();
|
||||
const ticket = cookieStore.get("ticket");
|
||||
if (ticket === null) {
|
||||
return <div>Ticket is null?</div>;
|
||||
window.location.href = "/";
|
||||
return;
|
||||
}
|
||||
|
||||
return (
|
||||
<main className={styles.main}>
|
||||
<RegisterForm ticket={ticket!.value} />
|
||||
<RegisterForm />
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
|
31
src/auth.ts
31
src/auth.ts
|
@ -6,15 +6,14 @@ import { getLogger } from "./logger";
|
|||
|
||||
const logger = getLogger("auth.ts");
|
||||
|
||||
export async function getUserFromRequest(request: Request) {
|
||||
const authorization = request.headers
|
||||
.get("authorization")
|
||||
?.replace("Bearer ", "");
|
||||
if (authorization === null) return null;
|
||||
export async function getUser() {
|
||||
const cookieStore = cookies();
|
||||
const cookieTicket = cookieStore.get("ticket");
|
||||
if (cookieTicket === null) return null;
|
||||
|
||||
const ticket = await prisma.authTicket.findFirst({
|
||||
where: {
|
||||
ticket: authorization
|
||||
ticket: cookieTicket?.value
|
||||
}
|
||||
});
|
||||
if (ticket === null) return null;
|
||||
|
@ -49,26 +48,6 @@ export async function getUserFromRequest(request: Request) {
|
|||
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) {
|
||||
let user = await prisma.user.findFirst({
|
||||
where: {
|
||||
|
|
|
@ -6,10 +6,15 @@ import Image from "next/image";
|
|||
|
||||
type ColorScheme = {
|
||||
name: string;
|
||||
|
||||
bg: string;
|
||||
bgAlt: string;
|
||||
bgDark: string;
|
||||
bgDarker?: string;
|
||||
|
||||
fg: string;
|
||||
fgAlt: string;
|
||||
fgDark: string;
|
||||
fgDarker?: string;
|
||||
|
||||
error?: string;
|
||||
warning?: string;
|
||||
};
|
||||
|
@ -18,88 +23,90 @@ const colors: ColorScheme[] = [
|
|||
{
|
||||
name: "Monokai Pro",
|
||||
bg: "#2d2a2e",
|
||||
bgAlt: "#403e41",
|
||||
bgDark: "#403e41",
|
||||
bgDarker: "#221f22",
|
||||
fg: "#fcfcfa",
|
||||
fgAlt: "#727072",
|
||||
fgDark: "#727072",
|
||||
fgDarker: "#5b595c",
|
||||
error: "#ff6188",
|
||||
warning: "#ffd866"
|
||||
},
|
||||
{
|
||||
name: "Gruvbox Dark",
|
||||
bg: "#282828",
|
||||
bgAlt: "#3c3836",
|
||||
bgDark: "#3c3836",
|
||||
fg: "#ebdbb2",
|
||||
fgAlt: "#a89984"
|
||||
fgDark: "#a89984"
|
||||
},
|
||||
{
|
||||
name: "Amora",
|
||||
bg: "#1a1a1a",
|
||||
bgAlt: "#171717",
|
||||
bgDark: "#171717",
|
||||
fg: "#DEDBEB",
|
||||
fgAlt: "#5c5c5c"
|
||||
fgDark: "#5c5c5c"
|
||||
},
|
||||
{
|
||||
name: "Amora Focus",
|
||||
bg: "#302838",
|
||||
bgAlt: "#2a2331",
|
||||
bgDark: "#2a2331",
|
||||
fg: "#dedbeb",
|
||||
fgAlt: "#5c5c5c"
|
||||
fgDark: "#5c5c5c"
|
||||
},
|
||||
{
|
||||
name: "Rosé Pine",
|
||||
bg: "#191724",
|
||||
bgAlt: "#26233a",
|
||||
bgDark: "#26233a",
|
||||
fg: "#e0def4",
|
||||
fgAlt: "#908caa"
|
||||
fgDark: "#908caa"
|
||||
},
|
||||
{
|
||||
name: "Rosé Pine Moon",
|
||||
bg: "#232136",
|
||||
bgAlt: "#393552",
|
||||
bgDark: "#393552",
|
||||
fg: "#e0def4",
|
||||
fgAlt: "#908caa"
|
||||
fgDark: "#908caa"
|
||||
},
|
||||
{
|
||||
name: "Nord",
|
||||
bg: "#2e3440",
|
||||
bgAlt: "#3b4252",
|
||||
bgDark: "#3b4252",
|
||||
fg: "#eceff4",
|
||||
fgAlt: "#d8dee9"
|
||||
fgDark: "#d8dee9"
|
||||
},
|
||||
{
|
||||
name: "lovelace",
|
||||
bg: "#1d1f28",
|
||||
bgAlt: "#282a36",
|
||||
bgDark: "#282a36",
|
||||
fg: "#fdfdfd",
|
||||
fgAlt: "#414458"
|
||||
fgDark: "#414458"
|
||||
},
|
||||
{
|
||||
name: "skyfall",
|
||||
bg: "#282f37",
|
||||
bgAlt: "#20262c",
|
||||
bgDark: "#20262c",
|
||||
fg: "#f1fcf9",
|
||||
fgAlt: "#465463"
|
||||
fgDark: "#465463"
|
||||
},
|
||||
{
|
||||
name: "Catppuccin Frappe",
|
||||
bg: "#303446",
|
||||
bgAlt: "#51576d",
|
||||
bgDark: "#51576d",
|
||||
fg: "#c6d0f5",
|
||||
fgAlt: "#a5adce"
|
||||
fgDark: "#a5adce"
|
||||
},
|
||||
{
|
||||
name: "Catppuccin Macchiato",
|
||||
bg: "#24273a",
|
||||
bgAlt: "#494d64",
|
||||
bgDark: "#494d64",
|
||||
fg: "#cad3f5",
|
||||
fgAlt: "#a5adcb"
|
||||
fgDark: "#a5adcb"
|
||||
},
|
||||
{
|
||||
name: "Catppuccin Mocha",
|
||||
bg: "#1e1e2e",
|
||||
bgAlt: "#45475a",
|
||||
bgDark: "#45475a",
|
||||
fg: "#cdd6f4",
|
||||
fgAlt: "#a6adc8"
|
||||
fgDark: "#a6adc8"
|
||||
}
|
||||
];
|
||||
|
||||
|
@ -122,9 +129,9 @@ export default function ColorChanger() {
|
|||
|
||||
const fixedColors = {
|
||||
"--bg": colorScheme.bg,
|
||||
"--bg-alt": colorScheme.bgAlt,
|
||||
"--bg-dark": colorScheme.bgDark,
|
||||
"--fg": colorScheme.fg,
|
||||
"--fg-alt": colorScheme.fgAlt,
|
||||
"--fg-dark": colorScheme.fgDark,
|
||||
"--error": colorScheme.error ?? fallback.error!,
|
||||
"--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 sharp from "sharp";
|
||||
import { BerWriter } from "asn1";
|
||||
import prisma from "./prisma";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
type LLDAPAuthResponse = {
|
||||
token: string;
|
||||
|
@ -14,6 +14,13 @@ type LLDAPRefreshResponse = {
|
|||
token: string;
|
||||
};
|
||||
|
||||
export type UserInfo = {
|
||||
username: string;
|
||||
displayName: string;
|
||||
email: string;
|
||||
avatar?: string;
|
||||
};
|
||||
|
||||
let ldapClient: Client | null = null;
|
||||
async function getLdapClient() {
|
||||
if (ldapClient === null) {
|
||||
|
@ -173,3 +180,67 @@ export async function checkUserExists(username: string) {
|
|||
(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 a new issue