forked from NotNet/gluestick
Compare commits
1 Commits
Author | SHA1 | Date |
---|---|---|
Julian | 41033d2b6f |
|
@ -0,0 +1,5 @@
|
||||||
|
User-agent: *
|
||||||
|
Disallow: /
|
||||||
|
|
||||||
|
User-agent: LUN-4
|
||||||
|
Allow: *
|
|
@ -0,0 +1,17 @@
|
||||||
|
body,
|
||||||
|
html {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
width: 100vw;
|
||||||
|
height: 100vh;
|
||||||
|
border: none;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
{{ template "header.html" . }}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let source = null;
|
||||||
|
function sendToGluestick(msg) {
|
||||||
|
source.postMessage(msg, {
|
||||||
|
targetOrigin: '{{ print (extra "gluestick_url") }}'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
window.addEventListener(
|
||||||
|
"message",
|
||||||
|
(e) => {
|
||||||
|
if (e.origin != '{{ print (extra "gluestick_url") }}') return;
|
||||||
|
|
||||||
|
switch (e.data.type) {
|
||||||
|
case "appResult":
|
||||||
|
submitApproval(e.data.success);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const iframe = document.querySelector("iframe");
|
||||||
|
iframe.addEventListener("load", () => {
|
||||||
|
source = iframe.contentWindow;
|
||||||
|
sendToGluestick({ type: "hello" });
|
||||||
|
sendToGluestick({
|
||||||
|
type: "appInfo",
|
||||||
|
client: "{{ .Client }}",
|
||||||
|
scopes:
|
||||||
|
{{ if .Scopes }}
|
||||||
|
[
|
||||||
|
{{ range $scope := .Scopes }}
|
||||||
|
"{{ $scope }}",
|
||||||
|
{{ end }}
|
||||||
|
]
|
||||||
|
{{ else }}
|
||||||
|
null
|
||||||
|
{{ end }}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function submitApproval(doesApprove) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("req", "{{ .AuthReqID }}");
|
||||||
|
params.append("approval", doesApprove ? "approve" : "rejected");
|
||||||
|
|
||||||
|
// cursed shit to work about cors
|
||||||
|
const form = document.createElement("form");
|
||||||
|
form.method = "POST";
|
||||||
|
|
||||||
|
const hiddenReq = document.createElement("input");
|
||||||
|
hiddenReq.type = "hidden";
|
||||||
|
hiddenReq.name = "req";
|
||||||
|
hiddenReq.value = "{{ .AuthReqID }}";
|
||||||
|
form.appendChild(hiddenReq);
|
||||||
|
|
||||||
|
const hiddenApproval = document.createElement("input");
|
||||||
|
hiddenApproval.type = "hidden";
|
||||||
|
hiddenApproval.name = "approval";
|
||||||
|
hiddenApproval.value = doesApprove ? "approve" : "rejected";
|
||||||
|
form.appendChild(hiddenApproval);
|
||||||
|
|
||||||
|
document.body.appendChild(form);
|
||||||
|
form.submit();
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<iframe src='{{ print (extra "gluestick_url") "/dex/approval" }}'></iframe>
|
||||||
|
|
||||||
|
{{ template "footer.html" . }}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ template "header.html" . }} {{ template "footer.html" . }}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ template "header.html" . }} {{ template "footer.html" . }}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ template "header.html" . }} {{ template "footer.html" . }}
|
|
@ -0,0 +1,2 @@
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,11 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>gluestick</title>
|
||||||
|
<link rel="icon" href='{{ print (extra "gluestick_url") "/icon.svg" }}' />
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
|
||||||
|
<link href='{{ url .ReqPath "static/main.css" }}' rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
|
@ -0,0 +1 @@
|
||||||
|
{{ template "header.html" . }} {{ template "footer.html" . }}
|
|
@ -0,0 +1 @@
|
||||||
|
{{ template "header.html" . }} {{ template "footer.html" . }}
|
|
@ -0,0 +1,61 @@
|
||||||
|
{{ template "header.html" . }}
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let source = null;
|
||||||
|
function sendToGluestick(msg) {
|
||||||
|
source.postMessage(msg, {
|
||||||
|
targetOrigin: '{{ print (extra "gluestick_url") }}'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
|
window.addEventListener(
|
||||||
|
"message",
|
||||||
|
(e) => {
|
||||||
|
if (e.origin != '{{ print (extra "gluestick_url") }}') return;
|
||||||
|
|
||||||
|
switch (e.data.type) {
|
||||||
|
case "passwordSubmit":
|
||||||
|
const { username, password } = e.data;
|
||||||
|
submitLogin(username, password);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
|
||||||
|
const iframe = document.querySelector("iframe");
|
||||||
|
iframe.addEventListener("load", () => {
|
||||||
|
source = iframe.contentWindow;
|
||||||
|
sendToGluestick({ type: "hello" });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
async function submitLogin(username, password) {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append("login", username);
|
||||||
|
params.append("password", password);
|
||||||
|
|
||||||
|
const req = await fetch("{{ .PostURL }}", {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded"
|
||||||
|
},
|
||||||
|
body: params,
|
||||||
|
method: "POST",
|
||||||
|
redirect: "follow"
|
||||||
|
});
|
||||||
|
|
||||||
|
if (req.ok && req.redirected) {
|
||||||
|
window.location.href = req.url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
sendToGluestick({
|
||||||
|
type: "passwordSubmitResult",
|
||||||
|
success: req.ok
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
<iframe src='{{ print (extra "gluestick_url") "/dex/password" }}'></iframe>
|
||||||
|
|
||||||
|
{{ template "footer.html" . }}
|
|
@ -27,7 +27,7 @@ declare global {
|
||||||
GITHUB_ORG: string;
|
GITHUB_ORG: string;
|
||||||
|
|
||||||
BASE_DOMAIN: string;
|
BASE_DOMAIN: string;
|
||||||
API_TOKEN?: string;
|
DEX_DOMAIN: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,7 @@
|
||||||
pname = "gluestick";
|
pname = "gluestick";
|
||||||
version = "0.1.0";
|
version = "0.1.0";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
npmDepsHash = "sha256-JPsXIPyiGycT/4dcg78qAz+qqIRYpSR24NWeu+5jLk0=";
|
npmDepsHash = "sha256-keOreamXKunlJzU2AKJo0J02ZxQrjLdoCIMCaiwEU4Y=";
|
||||||
|
|
||||||
nativeBuildInputs = inputs;
|
nativeBuildInputs = inputs;
|
||||||
buildInputs = inputs;
|
buildInputs = inputs;
|
||||||
|
|
|
@ -3,7 +3,18 @@ const nextConfig = {
|
||||||
experimental: {
|
experimental: {
|
||||||
appDir: true
|
appDir: true
|
||||||
},
|
},
|
||||||
output: "standalone"
|
output: "standalone",
|
||||||
|
|
||||||
|
// Allow Dex to use gluestick in an iframe
|
||||||
|
headers: async () => {
|
||||||
|
return [{
|
||||||
|
source: "/dex/(.*)",
|
||||||
|
headers: [{
|
||||||
|
key: "Content-Security-Policy",
|
||||||
|
value: `frame-ancestors 'self' ${process.env.DEX_DOMAIN}`
|
||||||
|
}]
|
||||||
|
}]
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
module.exports = nextConfig;
|
module.exports = nextConfig;
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -25,15 +25,14 @@
|
||||||
"formik": "^2.2.9",
|
"formik": "^2.2.9",
|
||||||
"graphql": "^16.6.0",
|
"graphql": "^16.6.0",
|
||||||
"ldapts": "^4.2.5",
|
"ldapts": "^4.2.5",
|
||||||
"next": "^13.4.2-canary.4",
|
"next": "13.3.1",
|
||||||
"pino": "^8.11.0",
|
"pino": "^8.11.0",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"sharp": "^0.32.0",
|
"sharp": "^0.32.0",
|
||||||
"typescript": "5.0.4",
|
"typescript": "5.0.4",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"zod": "^3.21.4",
|
"yup": "^1.1.1"
|
||||||
"zod-formik-adapter": "^1.2.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@graphql-codegen/cli": "^3.3.1",
|
"@graphql-codegen/cli": "^3.3.1",
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import { getUser } from "@/auth/auth";
|
|
||||||
import { getUserInfo, setPassword, validateUser } from "@/ldap";
|
|
||||||
import { ActionResponse } from ".";
|
|
||||||
import { PasswordUpdateSchema, passwordUpdateSchema } from "@/schemas";
|
|
||||||
|
|
||||||
export default async function changePassword(
|
|
||||||
data: PasswordUpdateSchema
|
|
||||||
): Promise<ActionResponse> {
|
|
||||||
const user = await getUser();
|
|
||||||
if (user == null) return { ok: false, error: "noUser" };
|
|
||||||
|
|
||||||
const userInfo = await getUserInfo(user);
|
|
||||||
if (userInfo == null) {
|
|
||||||
return { ok: false, error: "notRegisteredYet" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { password, newPassword } = passwordUpdateSchema.parse(data);
|
|
||||||
|
|
||||||
const passwordMatches = await validateUser(user.username!, password);
|
|
||||||
if (!passwordMatches) {
|
|
||||||
return { ok: false, error: "incorrectPassword" };
|
|
||||||
}
|
|
||||||
|
|
||||||
await setPassword(user.username!, newPassword);
|
|
||||||
|
|
||||||
return { ok: true };
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
export type ActionResponse = {
|
|
||||||
ok: boolean;
|
|
||||||
error?: string;
|
|
||||||
};
|
|
|
@ -1,26 +0,0 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import * as ldap from "@/ldap";
|
|
||||||
import { LoginSchema, loginSchema } from "@/schemas";
|
|
||||||
import { ActionResponse } from ".";
|
|
||||||
import { getLogger } from "@/logger";
|
|
||||||
import { authTicketLogin } from "@/auth/auth";
|
|
||||||
|
|
||||||
type Response = ActionResponse & {
|
|
||||||
ticket?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function login(data: LoginSchema): Promise<Response> {
|
|
||||||
const { username, password } = await loginSchema.parse(data);
|
|
||||||
|
|
||||||
const valid = await ldap.validateUser(username, password);
|
|
||||||
if (!valid) {
|
|
||||||
return {
|
|
||||||
ok: false,
|
|
||||||
error: "invalidCredentials"
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const [_, ticket] = await authTicketLogin(username);
|
|
||||||
return { ok: true, ticket: ticket.ticket };
|
|
||||||
}
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { authTicketLogin } from "@/auth/auth";
|
||||||
|
import * as ldap from "@/ldap";
|
||||||
|
import { loginSchema } from "@/schemas";
|
||||||
|
|
||||||
|
type RequestBody = {
|
||||||
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const { username, password } = await loginSchema.validate(
|
||||||
|
await request.json()
|
||||||
|
);
|
||||||
|
|
||||||
|
const valid = await ldap.validateUser(username, password);
|
||||||
|
if (!valid) {
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: false,
|
||||||
|
error: "invalidCredentials"
|
||||||
|
}),
|
||||||
|
{ status: 401 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [_, ticket] = await authTicketLogin(username);
|
||||||
|
// not confident if we can set-cookie and I cba to try
|
||||||
|
return new Response(JSON.stringify({ ok: true, ticket: ticket.ticket }));
|
||||||
|
}
|
|
@ -1,47 +1,38 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import * as ldap from "@/ldap";
|
import * as ldap from "@/ldap";
|
||||||
import prisma from "@/prisma";
|
import prisma from "@/prisma";
|
||||||
import { getUser } from "@/auth/auth";
|
import { getUser } from "@/auth/auth";
|
||||||
import { getLogger } from "@/logger";
|
import { getLogger } from "@/logger";
|
||||||
import { RegisterSchema, registerSchema } from "@/schemas";
|
import { registerServerSchema } from "@/schemas";
|
||||||
import { ActionResponse } from ".";
|
|
||||||
|
|
||||||
const logger = getLogger("/actions/register");
|
const logger = getLogger("/api/register");
|
||||||
|
|
||||||
export default async function register(
|
export async function POST(request: Request) {
|
||||||
data: RegisterSchema
|
|
||||||
): Promise<ActionResponse> {
|
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
|
if (user == null) return new Response(null, { status: 401 });
|
||||||
|
|
||||||
if (user == null) {
|
// user already has an account, don't re-register
|
||||||
return { ok: false, error: "invalidAuth" };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (user.username != null) {
|
if (user.username != null) {
|
||||||
logger.info(
|
logger.info(
|
||||||
{ username: user.username, id: user.id },
|
{ username: user.username, id: user.id },
|
||||||
`user tried to register twice`
|
`user tried to register twice`
|
||||||
);
|
);
|
||||||
return { ok: false, error: "invalidAuth" };
|
return new Response(null, { status: 403 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { username, displayName, email, password, avatar } =
|
const { username, displayName, email, password, avatar } =
|
||||||
await registerSchema.parse(data);
|
await registerServerSchema.validate(await request.json());
|
||||||
let avatarBuf = null;
|
let avatarBuf = avatar != null ? Buffer.from(avatar, "base64") : null;
|
||||||
if (avatar != null) {
|
|
||||||
const parts = avatar.split(",");
|
|
||||||
const data = parts.length === 2 ? parts[1] : parts[0];
|
|
||||||
avatarBuf = Buffer.from(data, "base64");
|
|
||||||
}
|
|
||||||
|
|
||||||
const users = await ldap.getUsers();
|
const users = await ldap.getUsers();
|
||||||
for (const user of users) {
|
for (const user of users) {
|
||||||
if (user.id.toLowerCase() === username.toLowerCase()) {
|
if (user.id.toLowerCase() === username.toLowerCase()) {
|
||||||
return {
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
ok: false,
|
ok: false,
|
||||||
error: "usernameTaken"
|
error: "usernameTaken"
|
||||||
};
|
}),
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,5 +49,10 @@ export default async function register(
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.info(outputUser, "registered user");
|
logger.info(outputUser, "registered user");
|
||||||
return { ok: true };
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: true
|
||||||
|
}),
|
||||||
|
{ status: 201 }
|
||||||
|
);
|
||||||
}
|
}
|
|
@ -1,6 +1,3 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import { ValidAuthProvider } from "@/auth/AuthProvider";
|
|
||||||
import {
|
import {
|
||||||
AuthState,
|
AuthState,
|
||||||
getAuthState,
|
getAuthState,
|
||||||
|
@ -38,7 +35,8 @@ async function deleteUser(id: number) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
export default async function unlink(provider?: ValidAuthProvider) {
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
const authState = await getAuthState();
|
const authState = await getAuthState();
|
||||||
|
|
||||||
if (authState == AuthState.Registering) {
|
if (authState == AuthState.Registering) {
|
||||||
|
@ -51,22 +49,30 @@ export default async function unlink(provider?: ValidAuthProvider) {
|
||||||
|
|
||||||
await deleteUser(registeringUser.id);
|
await deleteUser(registeringUser.id);
|
||||||
|
|
||||||
return;
|
return new Response(null, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
if (user == null) return;
|
if (user == null) return new Response(null, { status: 401 });
|
||||||
|
|
||||||
|
const { searchParams } = new URL(request.url);
|
||||||
|
const provider = searchParams.get("provider");
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
case "Discord":
|
case "discord":
|
||||||
const discord = await user.getDiscord();
|
const discord = await user.getDiscord();
|
||||||
if (discord == null) return;
|
if (discord == null) return new Response(null, { status: 400 });
|
||||||
await unlinkDiscord(await discord.getId());
|
await unlinkDiscord(await discord.getId());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case "GitHub":
|
case "github":
|
||||||
const github = await user.getGitHub();
|
const github = await user.getGitHub();
|
||||||
if (github == null) return;
|
if (github == null) return new Response(null, { status: 400 });
|
||||||
await unlinkGitHub(await github.getId());
|
await unlinkGitHub(await github.getId());
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return new Response(null, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new Response(null, { status: 200 });
|
||||||
}
|
}
|
|
@ -1,28 +1,30 @@
|
||||||
"use server";
|
|
||||||
|
|
||||||
import { AboutMeSchema, aboutMeSchema } from "@/schemas";
|
|
||||||
import { ActionResponse } from ".";
|
|
||||||
import { getLogger } from "@/logger";
|
|
||||||
import { getUser } from "@/auth/auth";
|
import { getUser } from "@/auth/auth";
|
||||||
import { getUserInfo, updateUser } from "@/ldap";
|
import { getUserInfo, updateUser } from "@/ldap";
|
||||||
|
import { getLogger } from "@/logger";
|
||||||
|
|
||||||
const logger = getLogger("/actions/update");
|
type RequestBody = {
|
||||||
|
displayName?: string;
|
||||||
|
email?: string;
|
||||||
|
avatar?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
const logger = getLogger("/api/update");
|
||||||
|
|
||||||
export default async function update(
|
|
||||||
data: AboutMeSchema
|
|
||||||
): Promise<ActionResponse> {
|
|
||||||
const user = await getUser();
|
const user = await getUser();
|
||||||
if (user == null) {
|
if (user == null) return new Response(null, { status: 401 });
|
||||||
return { ok: false, error: "invalidAuth" };
|
|
||||||
}
|
|
||||||
|
|
||||||
const userInfo = await getUserInfo(user);
|
const userInfo = await getUserInfo(user);
|
||||||
if (userInfo == null) {
|
if (userInfo == null) {
|
||||||
// no user info = hasn't registered yet
|
// no user info = hasn't registered yet
|
||||||
return { ok: false, error: "invalidAuth" };
|
return new Response(null, { status: 409 });
|
||||||
}
|
}
|
||||||
|
|
||||||
const { displayName, email, avatar } = await aboutMeSchema.parse(data);
|
const {
|
||||||
|
displayName,
|
||||||
|
email,
|
||||||
|
avatar: avatarBase64
|
||||||
|
} = (await request.json()) as RequestBody;
|
||||||
|
|
||||||
let changeDisplayName = false;
|
let changeDisplayName = false;
|
||||||
if (
|
if (
|
||||||
|
@ -45,24 +47,25 @@ export default async function update(
|
||||||
|
|
||||||
let avatarBuf = undefined;
|
let avatarBuf = undefined;
|
||||||
if (
|
if (
|
||||||
avatar !== undefined &&
|
avatarBase64 !== undefined &&
|
||||||
typeof avatar === "string" &&
|
typeof avatarBase64 === "string" &&
|
||||||
avatar !== userInfo.avatar
|
avatarBase64 !== userInfo.avatar
|
||||||
) {
|
) {
|
||||||
const parts = avatar.split(",");
|
avatarBuf = Buffer.from(avatarBase64, "base64");
|
||||||
const data = parts.length === 2 ? parts[1] : parts[0];
|
|
||||||
avatarBuf = Buffer.from(data, "base64");
|
|
||||||
|
|
||||||
if (avatarBuf.length > 2_000_000) {
|
if (avatarBuf.length > 2_000_000) {
|
||||||
return {
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
ok: false,
|
ok: false,
|
||||||
error: "avatarBig"
|
error: "avatarBig"
|
||||||
};
|
}),
|
||||||
|
{ status: 400 }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!changeDisplayName && !changeEmail && !avatarBuf) {
|
if (!changeDisplayName && !changeEmail && !avatarBuf) {
|
||||||
return { ok: true };
|
return new Response(null, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateUser(
|
await updateUser(
|
||||||
|
@ -82,5 +85,12 @@ export default async function update(
|
||||||
"updated user"
|
"updated user"
|
||||||
);
|
);
|
||||||
|
|
||||||
return { ok: true };
|
return new Response(
|
||||||
|
JSON.stringify({
|
||||||
|
ok: true
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
status: 200
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
|
@ -1,41 +0,0 @@
|
||||||
import { NextRequest } from "next/server";
|
|
||||||
import prisma from "@/prisma";
|
|
||||||
import * as ldap from "@/ldap";
|
|
||||||
|
|
||||||
export const dynamic = "force-dynamic";
|
|
||||||
|
|
||||||
export async function GET(
|
|
||||||
request: NextRequest,
|
|
||||||
{ params }: { params: { username: string } }
|
|
||||||
) {
|
|
||||||
const { username } = params;
|
|
||||||
|
|
||||||
if (
|
|
||||||
process.env.API_TOKEN == null ||
|
|
||||||
process.env.API_TOKEN !== request.headers.get("Authorization")
|
|
||||||
) {
|
|
||||||
return new Response(null, { status: 401 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: { username: username as string }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (user == null) {
|
|
||||||
return new Response(null, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
const ldapUser = await ldap.getUserInfo(user);
|
|
||||||
if (ldapUser == null) {
|
|
||||||
return new Response(null, { status: 404 });
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Response(
|
|
||||||
JSON.stringify({
|
|
||||||
...ldapUser,
|
|
||||||
avatar: ldapUser.avatar ?? null,
|
|
||||||
discordId: ldapUser.discordId ?? null,
|
|
||||||
githubId: ldapUser.githubId ?? null
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
.buttons {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.approvalText {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
|
@ -0,0 +1,59 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { AppInfo, useDex } from "../dex";
|
||||||
|
import styles from "./DexApprovalForm.module.css";
|
||||||
|
|
||||||
|
export default function DexApprovalForm({ domain }: { domain: string }) {
|
||||||
|
const [appInfo, setAppInfo] = React.useState<AppInfo | null>(null);
|
||||||
|
|
||||||
|
const sendToDex = useDex(domain, (msg) => {
|
||||||
|
switch (msg.type) {
|
||||||
|
case "appInfo":
|
||||||
|
setAppInfo(msg);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (appInfo === null) return <></>;
|
||||||
|
|
||||||
|
// Stolen from LoginForm
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<h1>Sign into {appInfo.client}</h1>
|
||||||
|
|
||||||
|
<div className={styles.approvalText}>
|
||||||
|
{appInfo.scopes != null ? (
|
||||||
|
<>
|
||||||
|
<p>{appInfo.client} would like to:</p>
|
||||||
|
<ul>
|
||||||
|
{appInfo.scopes.map((scope) => (
|
||||||
|
<li key={scope}>{scope}</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<p>{appInfo.client} doesn't have any special permissions.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
value="Allow"
|
||||||
|
onClick={() => {
|
||||||
|
sendToDex({ type: "appResult", success: true });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
value="Deny"
|
||||||
|
onClick={() => {
|
||||||
|
sendToDex({ type: "appResult", success: false });
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import styles from "@/app/page.module.css";
|
||||||
|
import DexApprovalForm from "./DexApprovalForm";
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
return (
|
||||||
|
<main className={styles.main}>
|
||||||
|
<DexApprovalForm domain={process.env.DEX_DOMAIN} />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export type AppInfo = {
|
||||||
|
type: "appInfo";
|
||||||
|
client: string;
|
||||||
|
scopes: string[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Message =
|
||||||
|
| { type: "hello" }
|
||||||
|
| { type: "passwordSubmit"; username: string; password: string }
|
||||||
|
| { type: "passwordSubmitResult"; success: boolean }
|
||||||
|
| AppInfo
|
||||||
|
| { type: "appResult"; success: boolean };
|
||||||
|
|
||||||
|
export function useDex(domain: string, handler: (msg: Message) => void) {
|
||||||
|
const [source, setSource] = React.useState<MessageEventSource | null>(null);
|
||||||
|
|
||||||
|
const sendToDex = (msg: Message, maybeSource?: MessageEventSource) => {
|
||||||
|
let realSource = maybeSource ?? source;
|
||||||
|
realSource!.postMessage(msg, {
|
||||||
|
targetOrigin: domain
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
window.addEventListener("message", (e) => {
|
||||||
|
if (e.origin !== domain) return;
|
||||||
|
const message: Message = e.data;
|
||||||
|
setSource(e.source);
|
||||||
|
|
||||||
|
if (message.type === "hello") {
|
||||||
|
sendToDex({ type: "hello" }, e.source!);
|
||||||
|
}
|
||||||
|
|
||||||
|
handler(message);
|
||||||
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return sendToDex;
|
||||||
|
}
|
|
@ -0,0 +1,60 @@
|
||||||
|
"use client";
|
||||||
|
|
||||||
|
import React from "react";
|
||||||
|
import { useDex } from "../dex";
|
||||||
|
import { LoginFormValues, loginSchema } from "@/schemas";
|
||||||
|
import { Form, Formik } from "formik";
|
||||||
|
import PrettyForm from "@/components/PrettyForm";
|
||||||
|
import Input from "@/components/Input";
|
||||||
|
|
||||||
|
export default function DexPasswordForm({ domain }: { domain: string }) {
|
||||||
|
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
||||||
|
const [submitting, setSubmitting] = React.useState(false);
|
||||||
|
|
||||||
|
const sendToDex = useDex(domain, (msg) => {
|
||||||
|
switch (msg.type) {
|
||||||
|
case "passwordSubmitResult":
|
||||||
|
setSubmitting(false);
|
||||||
|
if (!msg.success) setGlobalError("Invalid credentials.");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handleFormSubmit({ username, password }: LoginFormValues) {
|
||||||
|
setSubmitting(true);
|
||||||
|
sendToDex({
|
||||||
|
type: "passwordSubmit",
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stolen from LoginForm
|
||||||
|
return (
|
||||||
|
<PrettyForm globalError={globalError}>
|
||||||
|
<Formik
|
||||||
|
initialValues={{ username: "", password: "" }}
|
||||||
|
onSubmit={handleFormSubmit}
|
||||||
|
validationSchema={loginSchema}
|
||||||
|
>
|
||||||
|
{() => (
|
||||||
|
<Form>
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
placeholder="julian"
|
||||||
|
name="username"
|
||||||
|
label="Username"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="deeznuts47"
|
||||||
|
name="password"
|
||||||
|
label="Password"
|
||||||
|
/>
|
||||||
|
<input type="submit" value="Login" disabled={submitting} />
|
||||||
|
</Form>
|
||||||
|
)}
|
||||||
|
</Formik>
|
||||||
|
</PrettyForm>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
import styles from "@/app/page.module.css";
|
||||||
|
import DexPasswordForm from "./DexPasswordForm";
|
||||||
|
|
||||||
|
export default async function Page() {
|
||||||
|
return (
|
||||||
|
<main className={styles.main}>
|
||||||
|
<DexPasswordForm domain={process.env.DEX_DOMAIN} />
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,28 +1,43 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import login from "@/actions/login";
|
|
||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
import PrettyForm from "@/components/PrettyForm";
|
import PrettyForm from "@/components/PrettyForm";
|
||||||
import { LoginSchema, loginSchema } from "@/schemas";
|
import { LoginFormValues, loginSchema } from "@/schemas";
|
||||||
import { Form, Formik, FormikHelpers, FormikValues } from "formik";
|
import { Form, Formik, FormikHelpers, FormikValues } from "formik";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { toFormikValidationSchema } from "zod-formik-adapter";
|
|
||||||
|
|
||||||
export default function LoginForm() {
|
export default function LoginForm() {
|
||||||
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
||||||
|
|
||||||
async function handleFormSubmit(
|
async function handleFormSubmit(
|
||||||
data: LoginSchema,
|
{ username, password }: LoginFormValues,
|
||||||
{ setSubmitting }: FormikHelpers<LoginSchema>
|
{ setSubmitting }: FormikHelpers<LoginFormValues>
|
||||||
) {
|
) {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
|
|
||||||
if (data.username === "greets") {
|
if (username === "greets") {
|
||||||
window.location.href = "/sekrit";
|
window.location.href = "/sekrit";
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await login(data);
|
const req = await fetch("/api/login", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
password
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: {
|
||||||
|
ok: boolean;
|
||||||
|
error?: string;
|
||||||
|
ticket: string;
|
||||||
|
} = await req.json();
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
document.cookie = `ticket=${res.ticket}; path=/;`;
|
document.cookie = `ticket=${res.ticket}; path=/;`;
|
||||||
window.location.href = "/me";
|
window.location.href = "/me";
|
||||||
|
@ -30,6 +45,11 @@ export default function LoginForm() {
|
||||||
// only error is invalidCredentials, I am lazy
|
// only error is invalidCredentials, I am lazy
|
||||||
setGlobalError("Invalid credentials.");
|
setGlobalError("Invalid credentials.");
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setGlobalError("shits fucked up yo");
|
||||||
|
setSubmitting(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -37,7 +57,7 @@ export default function LoginForm() {
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{ username: "", password: "" }}
|
initialValues={{ username: "", password: "" }}
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
validationSchema={toFormikValidationSchema(loginSchema)}
|
validationSchema={loginSchema}
|
||||||
>
|
>
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
<Form>
|
<Form>
|
||||||
|
|
|
@ -7,7 +7,6 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 300px 1fr;
|
grid-template-columns: 300px 1fr;
|
||||||
column-gap: 2rem;
|
column-gap: 2rem;
|
||||||
max-width: 100vw;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.profileTower *:first-child {
|
.profileTower *:first-child {
|
||||||
|
@ -72,19 +71,3 @@
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* stack if we're out of space */
|
|
||||||
@media (max-width: 800px) {
|
|
||||||
.profileGrid {
|
|
||||||
grid-template-columns: 1fr;
|
|
||||||
}
|
|
||||||
|
|
||||||
.profileGrid > * {
|
|
||||||
max-width: 100vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.rightGrid {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -7,6 +7,12 @@ import styles from "./AboutMe.module.css";
|
||||||
import AvatarChanger from "@/components/AvatarChanger";
|
import AvatarChanger from "@/components/AvatarChanger";
|
||||||
import Input, { Hint, Label } from "@/components/Input";
|
import Input, { Hint, Label } from "@/components/Input";
|
||||||
import { Form, Formik, FormikHelpers } from "formik";
|
import { Form, Formik, FormikHelpers } from "formik";
|
||||||
|
import {
|
||||||
|
AboutMeFormValues,
|
||||||
|
PasswordUpdateFormValues,
|
||||||
|
aboutMeSchema,
|
||||||
|
passwordUpdateSchema
|
||||||
|
} from "@/schemas";
|
||||||
import PrettyForm from "@/components/PrettyForm";
|
import PrettyForm from "@/components/PrettyForm";
|
||||||
import Toast from "@/components/Toast";
|
import Toast from "@/components/Toast";
|
||||||
import { AuthProviderState } from "@/auth/AuthProvider";
|
import { AuthProviderState } from "@/auth/AuthProvider";
|
||||||
|
@ -15,10 +21,11 @@ import DiscordIcon from "@/components/icons/DiscordIcon";
|
||||||
import GitHubIcon from "@/components/icons/GitHubIcon";
|
import GitHubIcon from "@/components/icons/GitHubIcon";
|
||||||
import TailscaleIcon from "@/components/icons/TailscaleIcon";
|
import TailscaleIcon from "@/components/icons/TailscaleIcon";
|
||||||
import MigaduIcon from "@/components/icons/MigaduIcon";
|
import MigaduIcon from "@/components/icons/MigaduIcon";
|
||||||
import { AboutMeSchema, aboutMeSchema } from "@/schemas";
|
|
||||||
import update from "@/actions/update";
|
type UpdateResponse = {
|
||||||
import { toFormikValidationSchema } from "zod-formik-adapter";
|
ok: boolean;
|
||||||
import { useRouter } from "next/navigation";
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function AboutMe({
|
export default function AboutMe({
|
||||||
info,
|
info,
|
||||||
|
@ -31,9 +38,8 @@ export default function AboutMe({
|
||||||
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
||||||
const [madeProfileChanges, setMadeChanges] = React.useState(false);
|
const [madeProfileChanges, setMadeChanges] = React.useState(false);
|
||||||
const [madePasswordChanges, setMadePasswordChanges] = React.useState(false);
|
const [madePasswordChanges, setMadePasswordChanges] = React.useState(false);
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const initialValues: AboutMeSchema = {
|
const initialValues: AboutMeFormValues = {
|
||||||
username: info.username,
|
username: info.username,
|
||||||
displayName: info.displayName,
|
displayName: info.displayName,
|
||||||
email: info.email,
|
email: info.email,
|
||||||
|
@ -41,21 +47,78 @@ export default function AboutMe({
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handleFormSubmit(
|
async function handleFormSubmit(
|
||||||
data: AboutMeSchema,
|
{ displayName, email, avatar }: AboutMeFormValues,
|
||||||
{ setSubmitting }: FormikHelpers<AboutMeSchema>
|
{ setSubmitting }: FormikHelpers<AboutMeFormValues>
|
||||||
) {
|
) {
|
||||||
setMadeChanges(false);
|
setMadeChanges(false);
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
const res = await update(data);
|
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);
|
setSubmitting(false);
|
||||||
|
|
||||||
if (res.ok) {
|
try {
|
||||||
setMadeChanges(true);
|
const res: UpdateResponse = await req.json();
|
||||||
} else {
|
|
||||||
if (res.error != undefined) {
|
if (!res.ok && res.error !== null) {
|
||||||
setGlobalError("Unknown error: " + res.error);
|
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 (
|
return (
|
||||||
|
@ -64,7 +127,7 @@ export default function AboutMe({
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
validationSchema={toFormikValidationSchema(aboutMeSchema)}
|
validationSchema={aboutMeSchema}
|
||||||
>
|
>
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
<Form className={styles.profileGrid}>
|
<Form className={styles.profileGrid}>
|
||||||
|
@ -87,7 +150,7 @@ export default function AboutMe({
|
||||||
<div>
|
<div>
|
||||||
<h2 className={styles.userName}>{info.username}</h2>
|
<h2 className={styles.userName}>{info.username}</h2>
|
||||||
<div className={styles.rightGrid}>
|
<div className={styles.rightGrid}>
|
||||||
<div className={styles.profile}>
|
<div>
|
||||||
{madeProfileChanges ? (
|
{madeProfileChanges ? (
|
||||||
<Toast>Saved your changes.</Toast>
|
<Toast>Saved your changes.</Toast>
|
||||||
) : null}
|
) : null}
|
||||||
|
@ -122,14 +185,7 @@ export default function AboutMe({
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className={styles.multiButtons}>
|
<div className={styles.multiButtons}>
|
||||||
<button
|
<button type="button">Change password</button>
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
router.push("/me/change-password");
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Change password
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
@ -177,6 +233,69 @@ export default function AboutMe({
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</PrettyForm>
|
</PrettyForm>
|
||||||
|
{/*<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>
|
||||||
|
|
||||||
|
<h2 className={styles.header}>Connections</h2>
|
||||||
|
<div className={styles.authProviderList}>
|
||||||
|
{providers.map((provider) => (
|
||||||
|
<AuthProviderEntry provider={provider} key={provider.name} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="button"
|
||||||
|
value="Log out"
|
||||||
|
className={styles.logout}
|
||||||
|
onClick={async () => {
|
||||||
|
document.cookie =
|
||||||
|
"ticket=; expires=" + new Date().toUTCString() + "; path=/";
|
||||||
|
window.location.href = "/";
|
||||||
|
}}
|
||||||
|
/>*/}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,76 +0,0 @@
|
||||||
"use client";
|
|
||||||
|
|
||||||
import changePassword from "@/actions/changePassword";
|
|
||||||
import Input from "@/components/Input";
|
|
||||||
import PrettyForm from "@/components/PrettyForm";
|
|
||||||
import { PasswordUpdateSchema, passwordUpdateSchema } from "@/schemas";
|
|
||||||
import { Form, Formik, FormikHelpers } from "formik";
|
|
||||||
import { useRouter } from "next/navigation";
|
|
||||||
import React from "react";
|
|
||||||
import { toFormikValidationSchema } from "zod-formik-adapter";
|
|
||||||
|
|
||||||
export default function ChangePasswordForm({
|
|
||||||
onSuccess
|
|
||||||
}: {
|
|
||||||
onSuccess?: () => void;
|
|
||||||
}) {
|
|
||||||
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
|
||||||
const router = useRouter();
|
|
||||||
|
|
||||||
const initialValues: PasswordUpdateSchema = {
|
|
||||||
password: "",
|
|
||||||
newPassword: "",
|
|
||||||
confirmPassword: ""
|
|
||||||
};
|
|
||||||
|
|
||||||
async function handleFormSubmit(
|
|
||||||
data: PasswordUpdateSchema,
|
|
||||||
helpers: FormikHelpers<PasswordUpdateSchema>
|
|
||||||
) {
|
|
||||||
helpers.setSubmitting(true);
|
|
||||||
setGlobalError(null);
|
|
||||||
const res = await changePassword(data);
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
setGlobalError(res.error!); // should probably make this more human readable :trolley:
|
|
||||||
} else {
|
|
||||||
if (onSuccess == null) {
|
|
||||||
console.log("changed password :3");
|
|
||||||
router.push("/me");
|
|
||||||
} else {
|
|
||||||
onSuccess();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
helpers.setSubmitting(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PrettyForm globalError={globalError}>
|
|
||||||
<Formik
|
|
||||||
initialValues={initialValues}
|
|
||||||
onSubmit={handleFormSubmit}
|
|
||||||
validationSchema={toFormikValidationSchema(passwordUpdateSchema)}
|
|
||||||
>
|
|
||||||
{({ isSubmitting }) => (
|
|
||||||
<Form>
|
|
||||||
<Input type="password" name="password" label="Current Password" />
|
|
||||||
<Input type="password" name="newPassword" label="New Password" />
|
|
||||||
<Input
|
|
||||||
type="password"
|
|
||||||
name="confirmPassword"
|
|
||||||
label="Confirm New Password"
|
|
||||||
hint="Re-enter your new password. Better safe than sorry!"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<button type="submit" disabled={isSubmitting}>
|
|
||||||
Change Password
|
|
||||||
</button>
|
|
||||||
</Form>
|
|
||||||
)}
|
|
||||||
</Formik>
|
|
||||||
</PrettyForm>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,11 +0,0 @@
|
||||||
import ChangePasswordForm from "./ChangePasswordForm";
|
|
||||||
|
|
||||||
export default function ChangePassword() {
|
|
||||||
return (
|
|
||||||
// fuck it im lazy
|
|
||||||
<div style={{ maxWidth: "400px", margin: "2rem auto" }}>
|
|
||||||
<h1>Change Password</h1>
|
|
||||||
<ChangePasswordForm />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -2,17 +2,20 @@
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styles from "./RegisterForm.module.css";
|
import styles from "./RegisterForm.module.css";
|
||||||
import { Form, Formik, FormikHelpers } from "formik";
|
import { Form, Formik, FormikHelpers, yupToFormErrors } from "formik";
|
||||||
import { registerSchema, RegisterSchema } from "@/schemas";
|
import { RegisterFormValues, registerSchema } from "@/schemas";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { fileAsBase64 } from "@/forms";
|
||||||
import Input from "@/components/Input";
|
import Input from "@/components/Input";
|
||||||
import PrettyForm from "@/components/PrettyForm";
|
import PrettyForm from "@/components/PrettyForm";
|
||||||
import HugeSubmit from "@/components/HugeSubmit";
|
import HugeSubmit from "@/components/HugeSubmit";
|
||||||
import AvatarChanger from "@/components/AvatarChanger";
|
import AvatarChanger from "@/components/AvatarChanger";
|
||||||
import { ValidAuthProvider } from "@/auth/AuthProvider";
|
import { ValidAuthProvider } from "@/auth/AuthProvider";
|
||||||
import { toFormikValidationSchema } from "zod-formik-adapter";
|
|
||||||
import register from "@/actions/register";
|
type RegisterResponse = {
|
||||||
import unlink from "@/actions/unlink";
|
ok: boolean;
|
||||||
|
error?: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function RegisterForm({
|
export default function RegisterForm({
|
||||||
initialDisplayName,
|
initialDisplayName,
|
||||||
|
@ -28,7 +31,7 @@ export default function RegisterForm({
|
||||||
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
const [globalError, setGlobalError] = React.useState<string | null>(null);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const initialValues: RegisterSchema = {
|
const initialValues: RegisterFormValues = {
|
||||||
username: "",
|
username: "",
|
||||||
displayName: initialDisplayName ?? "",
|
displayName: initialDisplayName ?? "",
|
||||||
email: initialEmail ?? "",
|
email: initialEmail ?? "",
|
||||||
|
@ -38,12 +41,28 @@ export default function RegisterForm({
|
||||||
};
|
};
|
||||||
|
|
||||||
async function handleFormSubmit(
|
async function handleFormSubmit(
|
||||||
data: RegisterSchema,
|
{ avatar, username, displayName, email, password }: RegisterFormValues,
|
||||||
{ setFieldError, setSubmitting }: FormikHelpers<RegisterSchema>
|
{ setFieldError, setSubmitting }: FormikHelpers<RegisterFormValues>
|
||||||
) {
|
) {
|
||||||
setSubmitting(true);
|
setSubmitting(true);
|
||||||
|
|
||||||
const res = await register(data);
|
const resp = await fetch(`/api/register`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
displayName,
|
||||||
|
email,
|
||||||
|
password,
|
||||||
|
avatar: avatar != null ? avatar.split(",")[1] : undefined
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res: RegisterResponse = await resp.json();
|
||||||
|
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
router.replace("/me");
|
router.replace("/me");
|
||||||
} else {
|
} else {
|
||||||
|
@ -59,13 +78,13 @@ export default function RegisterForm({
|
||||||
case "usernameTaken":
|
case "usernameTaken":
|
||||||
setFieldError("username", "Username is already taken.");
|
setFieldError("username", "Username is already taken.");
|
||||||
break;
|
break;
|
||||||
|
|
||||||
default:
|
|
||||||
setGlobalError("Unknown error: " + res.error);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
setGlobalError("you done fucked up kiddo");
|
||||||
|
}
|
||||||
|
|
||||||
setSubmitting(false);
|
setSubmitting(false);
|
||||||
}
|
}
|
||||||
|
@ -75,7 +94,7 @@ export default function RegisterForm({
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
onSubmit={handleFormSubmit}
|
onSubmit={handleFormSubmit}
|
||||||
validationSchema={toFormikValidationSchema(registerSchema)}
|
validationSchema={registerSchema}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
>
|
>
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
|
@ -144,7 +163,7 @@ export default function RegisterForm({
|
||||||
<a
|
<a
|
||||||
className={styles.bail}
|
className={styles.bail}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
await unlink();
|
await fetch("/api/unlink", { method: "POST" });
|
||||||
document.cookie =
|
document.cookie =
|
||||||
"ticket=; expires=" + new Date().toUTCString() + "; path=/";
|
"ticket=; expires=" + new Date().toUTCString() + "; path=/";
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
|
|
|
@ -2,7 +2,7 @@ export type ValidAuthProvider = "Discord" | "GitHub";
|
||||||
|
|
||||||
// Can't send the providers across the wire, do this instead
|
// Can't send the providers across the wire, do this instead
|
||||||
export type AuthProviderState = {
|
export type AuthProviderState = {
|
||||||
name: ValidAuthProvider;
|
name: string;
|
||||||
} & ({ connected: false } | { connected: true; id: string; username: string });
|
} & ({ connected: false } | { connected: true; id: string; username: string });
|
||||||
|
|
||||||
export abstract class AuthProvider {
|
export abstract class AuthProvider {
|
||||||
|
|
|
@ -41,7 +41,7 @@ export default function AvatarChanger({
|
||||||
alt="Your avatar"
|
alt="Your avatar"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<UserIcon className={styles.currentAvatar} />
|
<UserIcon />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
|
@ -18,7 +18,8 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.connection > svg {
|
.connection > svg {
|
||||||
height: 1.5rem;
|
width: 2rem;
|
||||||
|
height: 2rem;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,6 @@ import classnames from "classnames";
|
||||||
import CheckIcon from "./icons/CheckIcon";
|
import CheckIcon from "./icons/CheckIcon";
|
||||||
import { type AuthProviderState } from "@/auth/AuthProvider";
|
import { type AuthProviderState } from "@/auth/AuthProvider";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import unlink from "@/actions/unlink";
|
|
||||||
|
|
||||||
export default function Connection({
|
export default function Connection({
|
||||||
service,
|
service,
|
||||||
|
@ -24,21 +23,21 @@ export default function Connection({
|
||||||
const interval = React.useRef<NodeJS.Timeout | null>();
|
const interval = React.useRef<NodeJS.Timeout | null>();
|
||||||
|
|
||||||
const execute = async () => {
|
const execute = async () => {
|
||||||
const name = authState?.name;
|
const name = authState?.name.toLowerCase();
|
||||||
if (!authState?.connected) {
|
if (!authState?.connected) {
|
||||||
router.push(`/oauth/${name?.toLowerCase()}/login`);
|
router.push(`/oauth/${name}/login`);
|
||||||
} else {
|
} else {
|
||||||
await unlink(name);
|
await fetch(`/api/unlink?provider=${name}`, { method: "POST" });
|
||||||
router.refresh();
|
router.refresh();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const down = (e: React.MouseEvent | React.TouchEvent) => {
|
const mouseDown = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
interval.current = setTimeout(execute, holdTime);
|
interval.current = setTimeout(execute, holdTime);
|
||||||
};
|
};
|
||||||
|
|
||||||
const up = (e: React.MouseEvent | React.TouchEvent) => {
|
const mouseUp = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (interval.current) clearTimeout(interval.current);
|
if (interval.current) clearTimeout(interval.current);
|
||||||
};
|
};
|
||||||
|
@ -52,10 +51,8 @@ export default function Connection({
|
||||||
!authState?.connected ? styles.disconnected : styles.progress
|
!authState?.connected ? styles.disconnected : styles.progress
|
||||||
)}
|
)}
|
||||||
disabled={unavailable}
|
disabled={unavailable}
|
||||||
onMouseDown={down}
|
onMouseDown={mouseDown}
|
||||||
onMouseUp={up}
|
onMouseUp={mouseUp}
|
||||||
onTouchStart={down}
|
|
||||||
onTouchEnd={up}
|
|
||||||
>
|
>
|
||||||
<div className={styles.iconContainer}>
|
<div className={styles.iconContainer}>
|
||||||
{icon ? icon() : <span className={styles.dot}></span>}
|
{icon ? icon() : <span className={styles.dot}></span>}
|
||||||
|
|
|
@ -74,6 +74,7 @@ export default function Input<T>(
|
||||||
title={props.title}
|
title={props.title}
|
||||||
{...inputFields}
|
{...inputFields}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
|
console.log(event);
|
||||||
if (props.customOnChange) {
|
if (props.customOnChange) {
|
||||||
console.log("using custom on change");
|
console.log("using custom on change");
|
||||||
props.customOnChange(event, form);
|
props.customOnChange(event, form);
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
.logo {
|
.logo {
|
||||||
max-width: 700px;
|
width: 700px;
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,7 @@ export default function PrettyForm({
|
||||||
globalError,
|
globalError,
|
||||||
children
|
children
|
||||||
}: {
|
}: {
|
||||||
globalError?: string | null;
|
globalError: string | null;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,13 +1,12 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export default function UserIcon(props: React.SVGProps<SVGSVGElement>) {
|
export default function UserIcon() {
|
||||||
return (
|
return (
|
||||||
<svg
|
<svg
|
||||||
viewBox="0 0 128 128"
|
viewBox="0 0 128 128"
|
||||||
version="1.1"
|
version="1.1"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
{...props}
|
|
||||||
>
|
>
|
||||||
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
|
<g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd">
|
||||||
<circle fill="currentColor" cx="64" cy="48" r="32"></circle>
|
<circle fill="currentColor" cx="64" cy="48" r="32"></circle>
|
||||||
|
|
116
src/schemas.ts
116
src/schemas.ts
|
@ -1,71 +1,109 @@
|
||||||
import { z } from "zod";
|
import * as Yup from "yup";
|
||||||
|
|
||||||
const USERNAME = z
|
const REQUIRED = "Required.";
|
||||||
.string()
|
const USERNAME = Yup.string()
|
||||||
|
.required(REQUIRED)
|
||||||
.min(1, "Username is too short.")
|
.min(1, "Username is too short.")
|
||||||
.regex(/^[a-z0-9]+$/, "Username must be lowercase alphanumeric.");
|
.matches(/^[a-z0-9]+$/, "Username must be lowercase letters and numbers.");
|
||||||
const DISPLAY_NAME = z.string().min(1, "Display name is too short.");
|
|
||||||
const EMAIL = z.string().email("Not an email.");
|
const DISPLAY_NAME = Yup.string()
|
||||||
const PASSWORD = z
|
.required(REQUIRED)
|
||||||
.string()
|
.min(1, "Display name is too short.");
|
||||||
|
const EMAIL = Yup.string().required(REQUIRED).email("Not an email.");
|
||||||
|
|
||||||
|
const PASSWORD = Yup.string()
|
||||||
|
.required(REQUIRED)
|
||||||
.min(12, "Password must be at least 12 characters long.");
|
.min(12, "Password must be at least 12 characters long.");
|
||||||
const AVATAR = z.string().refine(
|
const CONFIRM_PASSWORD = (name: string) =>
|
||||||
(val) => {
|
Yup.string()
|
||||||
const parts = val.split(",");
|
.required(REQUIRED)
|
||||||
const data = parts.length === 2 ? parts[1] : parts[0];
|
.oneOf([Yup.ref(name, {})], "Passwords must match.");
|
||||||
|
|
||||||
|
const AVATAR = Yup.string().test(
|
||||||
|
"file-size",
|
||||||
|
"File is bigger than 2 MB.",
|
||||||
|
(value) => {
|
||||||
|
if (value == null) return true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buf = Buffer.from(data, "base64");
|
const buf = Buffer.from(value, "base64");
|
||||||
return buf.length <= 2_000_000;
|
return buf.length <= 2_000_000;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
message: "File is bigger than 2 MB.",
|
|
||||||
path: ["avatar"]
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const loginSchema = z.object({
|
export const loginSchema = Yup.object().shape({
|
||||||
username: USERNAME,
|
username: USERNAME,
|
||||||
password: PASSWORD
|
password: PASSWORD
|
||||||
});
|
});
|
||||||
export type LoginSchema = z.infer<typeof loginSchema>;
|
|
||||||
|
|
||||||
export const registerSchema = z
|
export type LoginFormValues = {
|
||||||
.object({
|
username: string;
|
||||||
|
password: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const registerSchema: Yup.Schema<RegisterFormValues> =
|
||||||
|
Yup.object().shape({
|
||||||
username: USERNAME,
|
username: USERNAME,
|
||||||
displayName: DISPLAY_NAME,
|
displayName: DISPLAY_NAME,
|
||||||
email: EMAIL,
|
email: EMAIL,
|
||||||
password: PASSWORD,
|
password: PASSWORD,
|
||||||
confirmPassword: PASSWORD,
|
confirmPassword: CONFIRM_PASSWORD("password"),
|
||||||
avatar: AVATAR.optional()
|
avatar: AVATAR
|
||||||
})
|
|
||||||
.refine((data) => data.password === data.confirmPassword, {
|
|
||||||
message: "Passwords do not match.",
|
|
||||||
path: ["confirmPassword"]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type RegisterSchema = z.infer<typeof registerSchema>;
|
export interface RegisterFormValues {
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
export const aboutMeSchema = z.object({
|
export const aboutMeSchema: Yup.Schema<AboutMeFormValues> = Yup.object().shape({
|
||||||
username: USERNAME,
|
username: USERNAME,
|
||||||
displayName: DISPLAY_NAME,
|
displayName: DISPLAY_NAME,
|
||||||
email: EMAIL,
|
email: EMAIL,
|
||||||
avatar: AVATAR.optional()
|
avatar: AVATAR
|
||||||
});
|
});
|
||||||
export type AboutMeSchema = z.infer<typeof aboutMeSchema>;
|
|
||||||
|
|
||||||
export const passwordUpdateSchema = z
|
export interface AboutMeFormValues {
|
||||||
.object({
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
email: string;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const passwordUpdateSchema: Yup.Schema<PasswordUpdateFormValues> =
|
||||||
|
Yup.object().shape({
|
||||||
password: PASSWORD,
|
password: PASSWORD,
|
||||||
newPassword: PASSWORD,
|
newPassword: PASSWORD,
|
||||||
confirmPassword: PASSWORD
|
confirmPassword: CONFIRM_PASSWORD("newPassword")
|
||||||
})
|
|
||||||
.refine((data) => data.newPassword === data.confirmPassword, {
|
|
||||||
message: "Passwords do not match.",
|
|
||||||
path: ["confirmPassword"]
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type PasswordUpdateSchema = z.infer<typeof passwordUpdateSchema>;
|
export interface PasswordUpdateFormValues {
|
||||||
|
password: string;
|
||||||
|
newPassword: string;
|
||||||
|
confirmPassword: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types specific to the server, because sometimes we omit fields (like confirmPassword)
|
||||||
|
export const registerServerSchema: Yup.Schema<RegisterServerFormValues> =
|
||||||
|
Yup.object().shape({
|
||||||
|
username: USERNAME,
|
||||||
|
displayName: DISPLAY_NAME,
|
||||||
|
email: EMAIL,
|
||||||
|
password: PASSWORD,
|
||||||
|
avatar: AVATAR
|
||||||
|
});
|
||||||
|
|
||||||
|
export interface RegisterServerFormValues {
|
||||||
|
username: string;
|
||||||
|
displayName: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
avatar?: string;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue