From d5ec97ed73dad12eae24d871c283a3f77867e805 Mon Sep 17 00:00:00 2001 From: NotNite Date: Tue, 25 Apr 2023 19:56:52 -0400 Subject: [PATCH] proper login flow --- src/app/api/login/route.ts | 41 ++++++++++++++++++++++++++++++ src/app/login/page.tsx | 52 ++++++++++++++++++++++++++++++++++++++ src/app/page.tsx | 12 +++++++-- src/auth.ts | 48 +++++++++++++++++++++++++++++++++++ src/ldap.ts | 15 +++++++++++ src/prisma.ts | 47 ++++++++++++++++++++++++++++++++++ 6 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 src/app/api/login/route.ts create mode 100644 src/app/login/page.tsx diff --git a/src/app/api/login/route.ts b/src/app/api/login/route.ts new file mode 100644 index 0000000..19a6f90 --- /dev/null +++ b/src/app/api/login/route.ts @@ -0,0 +1,41 @@ +import * as ldap from "@/ldap"; +import { createAuthTicket } from "@/auth"; + +type RequestBody = { + username: string; + password: string; +}; + +export async function POST(request: Request) { + const { username, password } = (await request.json()) as RequestBody; + + if ( + username == undefined || + typeof username !== "string" || + password == undefined || + typeof password !== "string" + ) { + return new Response( + JSON.stringify({ + ok: false, + error: "invalidBody" + }), + { status: 400 } + ); + } + + const valid = await ldap.validateUser(username, password); + if (!valid) { + return new Response( + JSON.stringify({ + ok: false, + error: "invalidCredentials" + }), + { status: 401 } + ); + } + + const ticket = await createAuthTicket(username); + // not confident if we can set-cookie and I cba to try + return new Response(JSON.stringify({ ok: true, ticket })); +} diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx new file mode 100644 index 0000000..538bd1c --- /dev/null +++ b/src/app/login/page.tsx @@ -0,0 +1,52 @@ +"use client"; + +import styles from "@/app/page.module.css"; +import React from "react"; + +// TODO: use input from register & un programmer art this +export default function Page() { + const usernameRef = React.useRef(null); + const passwordRef = React.useRef(null); + + return ( +
+
{ + e.preventDefault(); + + const username = usernameRef.current?.value ?? ""; + const password = passwordRef.current?.value ?? ""; + + const req = await fetch("/api/login", { + method: "POST", + headers: { + "Content-Type": "application/json" + }, + body: JSON.stringify({ + username, + password + }) + }); + + if (req.status === 200) { + const res: { ticket: string } = await req.json(); + document.cookie = `ticket=${res.ticket}; path=/;`; + window.location.href = "/me"; + } else { + // todo error handling lol + } + }} + > + + + +
+
+ ); +} diff --git a/src/app/page.tsx b/src/app/page.tsx index 6cd2921..cb019ad 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -6,8 +6,16 @@ export default function Home() {
gluestick logo -

- login debug +

+ login + me + discord + register

); diff --git a/src/auth.ts b/src/auth.ts index 2121d79..f1ee69d 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,5 +1,6 @@ import prisma from "@/prisma"; import { cookies } from "next/dist/client/components/headers"; +import { v4 } from "uuid"; export async function getUserFromRequest(request: Request) { const authorization = request.headers @@ -41,3 +42,50 @@ export async function getUserFromPage() { }); return user; } + +export async function createAuthTicket(username: string) { + let user = await prisma.user.findFirst({ + where: { + username: username + } + }); + + // It's possible we haven't made a user yet (already existing accounts) + if (user === null) { + user = await prisma.user.create({ + data: { + username: username + } + }); + } + + const authTicket = await prisma.authTicket.upsert({ + where: { + userId: user!.id + }, + create: { + userId: user!.id, + ticket: v4(), + expiresAt: new Date(Date.now() + 86400000) + }, + update: { + ticket: v4(), + expiresAt: new Date(Date.now() + 86400000) + } + }); + + await prisma.user.update({ + where: { + id: user!.id + }, + data: { + authTicket: { + connect: { + id: authTicket.id + } + } + } + }); + + return authTicket.ticket; +} diff --git a/src/ldap.ts b/src/ldap.ts index fe9e632..2ad8157 100644 --- a/src/ldap.ts +++ b/src/ldap.ts @@ -152,3 +152,18 @@ export async function setPassword(user: string, password: string) { await client.exop("1.3.6.1.4.1.4203.1.11.1", writer.buffer); } + +export async function validateUser(username: string, password: string) { + const client = new Client({ + url: `ldap://${process.env.LDAP_HOST}:3890` + }); + + try { + const dn = `uid=${username},ou=people,${process.env.LDAP_DC}`; + await client.bind(dn, password); + await client.unbind(); + return true; + } catch (e) { + return false; + } +} diff --git a/src/prisma.ts b/src/prisma.ts index c957ceb..66f2d50 100644 --- a/src/prisma.ts +++ b/src/prisma.ts @@ -1,3 +1,50 @@ import { PrismaClient } from "@prisma/client"; +import { DiscordAccessTokenResponse } from "./app/oauth/discord/oauth"; const prisma = new PrismaClient(); + +// refresh 6 hours before expiry +async function refreshDiscordTokens() { + const refreshWindow = 6 * 60 * 60 * 1000; + + const discordAuths = await prisma.discordAuth.findMany({ + where: { + expiresAt: { + lte: new Date(Date.now() + refreshWindow) + } + } + }); + + for (const discordAuth of discordAuths) { + const req = await fetch("https://discord.com/api/oauth2/token", { + method: "POST", + headers: { + "Content-Type": "application/x-www-form-urlencoded" + }, + body: new URLSearchParams({ + client_id: process.env.DISCORD_CLIENT_ID, + client_secret: process.env.DISCORD_CLIENT_SECRET, + grant_type: "refresh_token", + refresh_token: discordAuth.refreshToken + }).toString() + }); + + const res: DiscordAccessTokenResponse = await req.json(); + + await prisma.discordAuth.update({ + where: { + id: discordAuth.id + }, + data: { + accessToken: res.access_token, + refreshToken: res.refresh_token, + expiresAt: new Date(Date.now() + res.expires_in * 1000) + } + }); + } +} + +setInterval(async () => { + await refreshDiscordTokens(); +}, 60 * 1000); + export default prisma;