Unfinished

This commit is contained in:
Aron Malcher 2024-02-18 22:02:52 +01:00
parent 18c6535d1c
commit 6b388729d9
Signed by: aronmal
GPG key ID: 816B7707426FC612
25 changed files with 1598 additions and 2352 deletions

View file

@ -6,17 +6,13 @@ import NavUser from "./NavUser";
export function Li(props: {
href: string;
action?: () => void;
rel?: string;
name?: string;
children?: JSX.Element;
}) {
return (
<li class="navElem flex-row thick">
<a
class="flex-row"
href={props.href}
onClick={() => props.action && props.action()}
>
<a class="flex-row" href={props.href} rel={props.rel}>
{props.children ?? <></>}
<Show when={props.name}>
<span>{props.name}</span>

View file

@ -1,50 +1,37 @@
import { getSession } from "@auth/solid-start";
import { signIn, signOut } from "@auth/solid-start/client";
import {
faArrowRightFromBracket,
faArrowRightToBracket,
faGear,
} from "@fortawesome/pro-regular-svg-icons";
import { eq } from "drizzle-orm";
import { User } from "lucia";
import { Show, createResource } from "solid-js";
import { getRequestEvent } from "solid-js/web";
import db from "~/drizzle";
import { users } from "~/drizzle/schema";
import { authOptions } from "~/server/auth";
import { FontAwesomeIcon } from "./FontAwesomeIcon";
import { Li } from "./NavBar";
const initialUser = {
id: "",
name: null as string | null,
email: "",
emailVerified: null as Date | null,
image: null as string | null,
};
async function getUser() {
async function getUser(): Promise<
| ({
success: false;
message: string;
// user?: undefined;
} & Partial<User>)
| ({
success: true;
message?: undefined;
} & User)
> {
"use server";
const event = getRequestEvent();
if (!event)
return { success: false, message: "No request event!", ...initialUser };
if (!event) return { success: false, message: "No request event!" };
const session = await getSession(event.request, authOptions);
if (!session?.user?.id)
return { success: false, message: "No user with id!", ...initialUser };
const pathname = new URL(event.request.url).pathname;
const { user } = event.nativeEvent.context;
if (!user) return { success: false, message: "User not logged in!" };
const user = (
await db
.selectDistinct()
.from(users)
.where(eq(users.id, session.user?.id))
.limit(1)
.execute()
)[0];
console.log("userInfo", pathname, "success");
console.log("userInfo", "success");
return { success: true, message: "", ...user };
return { success: true, ...user };
}
function NavUser() {
@ -55,16 +42,20 @@ function NavUser() {
return user;
});
const pfp = () => {
const thisUser = user();
if (!thisUser?.success) return "";
return thisUser.image
? `https://cdn.discordapp.com/avatars/${thisUser.discord_id}/${thisUser.image}.png`
: `https://cdn.discordapp.com/embed/avatars/${(parseInt(thisUser.discord_id) >> 22) % 6}.png`;
};
return (
<Show
when={user()?.id}
fallback={
<Li
href="#"
name="Login"
action={() => signIn("discord", { callbackUrl: "/config" })}
>
<Li href="/api/auth/login" name="Login" rel="external">
<FontAwesomeIcon
class="secondary"
icon={faArrowRightToBracket}
@ -75,11 +66,11 @@ function NavUser() {
>
<Li href="/config">
<div class="swap lower">
<img class="primary" src={user()?.image ?? ""} alt="User pfp" />
<img class="primary" src={pfp()} alt="User pfp" />
<FontAwesomeIcon class="secondary" icon={faGear} size="xl" />
</div>
</Li>
<Li href="#" action={() => signOut({ callbackUrl: "/" })}>
<Li href="/api/auth/logout" rel="external">
<FontAwesomeIcon icon={faArrowRightFromBracket} size="xl" />
</Li>
</Show>

View file

@ -1,10 +1,8 @@
import type { AdapterAccount } from "@auth/core/adapters";
import { relations } from "drizzle-orm";
import {
boolean,
integer,
pgTable,
primaryKey,
serial,
smallint,
text,
@ -13,56 +11,31 @@ import {
} from "drizzle-orm/pg-core";
export const users = pgTable("user", {
id: text("id").notNull().primaryKey(),
id: varchar("id", { length: 24 }).primaryKey(),
discord_id: text("discord_id").notNull(),
name: text("name"),
email: text("email").notNull(),
emailVerified: timestamp("emailVerified", { mode: "date" }),
image: text("image"),
});
export const accounts = pgTable(
"account",
{
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
type: text("type").$type<AdapterAccount["type"]>().notNull(),
provider: text("provider").notNull(),
providerAccountId: text("providerAccountId").notNull(),
refresh_token: text("refresh_token"),
access_token: text("access_token"),
expires_at: integer("expires_at"),
token_type: text("token_type"),
scope: text("scope"),
id_token: text("id_token"),
session_state: text("session_state"),
},
(account) => ({
compoundKey: primaryKey({
columns: [account.provider, account.providerAccountId],
}),
}),
);
export const sessions = pgTable("session", {
sessionToken: text("sessionToken").notNull().primaryKey(),
userId: text("userId")
id: varchar("id", { length: 24 }).primaryKey(),
userId: varchar("user_id", { length: 24 })
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
expires: timestamp("expires", { mode: "date" }).notNull(),
expiresAt: timestamp("expires_at", {
withTimezone: true,
mode: "date",
}).notNull(),
});
export const verificationTokens = pgTable(
"verificationToken",
{
identifier: text("identifier").notNull(),
token: text("token").notNull(),
expires: timestamp("expires", { mode: "date" }).notNull(),
},
(vt) => ({
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
}),
);
export const discordTokens = pgTable("tokens", {
userId: varchar("user_id", { length: 24 })
.primaryKey()
.references(() => users.id, { onDelete: "cascade" }),
refreshToken: text("refresh_token").notNull(),
accessToken: text("access_token").notNull(),
expiresAt: timestamp("expires_at", { mode: "date" }).notNull(),
});
export const matchPlannings = pgTable("match_planning", {
id: serial("id").primaryKey(),

View file

@ -1,3 +1,3 @@
import { mount, StartClient } from "@solidjs/start/client";
mount(() => <StartClient />, document.getElementById("app"));
mount(() => <StartClient />, document.getElementById("app")!);

46
src/lib/auth.ts Normal file
View file

@ -0,0 +1,46 @@
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
import { Discord } from "arctic";
import { PgColumn, PgTableWithColumns } from "drizzle-orm/pg-core";
import { Lucia } from "lucia";
import db from "~/drizzle";
import { sessions, users } from "~/drizzle/schema";
const adapter = new DrizzlePostgreSQLAdapter(db, sessions, users);
export const lucia = new Lucia(adapter, {
sessionCookie: {
attributes: {
// set to `true` when using HTTPS
secure: import.meta.env.PROD,
},
},
getUserAttributes: (attributes) => attributes,
});
declare module "lucia" {
// eslint-disable-next-line no-unused-vars
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: DatabaseUserAttributes;
}
}
type GetColumns<T> =
T extends PgTableWithColumns<infer First> ? First["columns"] : never;
type ExtractDataTypes<T> = {
[K in keyof T]: T[K] extends PgColumn<infer DataType, any, any>
? DataType["data"]
: never;
};
interface DatabaseUserAttributes
extends ExtractDataTypes<GetColumns<typeof users>> {
warst: string;
}
export const discord = new Discord(
import.meta.env.VITE_DISCORD_CLIENT_ID,
import.meta.env.VITE_DISCORD_CLIENT_SECRET,
import.meta.env.VITE_AUTH_REDIRECT_URL,
);

54
src/middleware.ts Normal file
View file

@ -0,0 +1,54 @@
import { createMiddleware } from "@solidjs/start/middleware";
import { Session, User, verifyRequestOrigin } from "lucia";
import { appendHeader, getCookie, getHeader } from "vinxi/http";
import { lucia } from "./lib/auth";
export default createMiddleware({
onRequest: async (event) => {
if (event.nativeEvent.node.req.method !== "GET") {
const originHeader = getHeader(event, "Origin") ?? null;
const hostHeader = getHeader(event, "Host") ?? null;
if (
!originHeader ||
!hostHeader ||
!verifyRequestOrigin(originHeader, [hostHeader])
) {
event.nativeEvent.node.res.writeHead(403).end();
return;
}
}
const sessionId = getCookie(event, lucia.sessionCookieName) ?? null;
if (!sessionId) {
event.nativeEvent.context.session = null;
event.nativeEvent.context.user = null;
return;
}
const { session, user } = await lucia.validateSession(sessionId);
if (session && session.fresh) {
appendHeader(
event,
"Set-Cookie",
lucia.createSessionCookie(session.id).serialize(),
);
}
if (!session) {
appendHeader(
event,
"Set-Cookie",
lucia.createBlankSessionCookie().serialize(),
);
}
event.nativeEvent.context.session = session;
event.nativeEvent.context.user = user;
},
});
declare module "h3" {
// eslint-disable-next-line no-unused-vars
interface H3EventContext {
user: User | null;
session: Session | null;
}
}

View file

@ -1,4 +0,0 @@
import { SolidAuth } from "@auth/solid-start"
import { authOptions } from "~/server/auth"
export const { GET, POST } = SolidAuth(authOptions)

View file

@ -0,0 +1,132 @@
import { createId } from "@paralleldrive/cuid2";
import { APIEvent } from "@solidjs/start/server/types";
import { OAuth2RequestError } from "arctic";
import { eq } from "drizzle-orm";
import createClient from "openapi-fetch";
import { getCookie, setCookie } from "vinxi/http";
import db from "~/drizzle";
import { discordTokens, users } from "~/drizzle/schema";
import { discord, lucia } from "~/lib/auth";
import { paths } from "~/types/discord";
export async function GET(event: APIEvent): Promise<Response> {
const code = new URL(event.request.url).searchParams.get("code");
const state = new URL(event.request.url).searchParams.get("state");
const error = new URL(event.request.url).searchParams.get("error");
const error_description = new URL(event.request.url).searchParams.get(
"error_description",
);
if (error)
switch (error) {
case "access_denied":
return new Response(null, {
status: 302,
headers: { Location: "/" },
});
default:
console.log("Discord oauth error:", error_description);
return new Response(decodeURI(error_description ?? ""), {
status: 400,
});
}
const storedState = getCookie("discord_oauth_state") ?? null;
if (!code || !state || !storedState || state !== storedState) {
return new Response(null, {
status: 400,
});
}
try {
const tokens = await discord.validateAuthorizationCode(code);
const { GET } = createClient<paths>({
baseUrl: "https://discord.com/api/v10",
});
const discordUserResponse = await GET("/users/@me", {
headers: { Authorization: `Bearer ${tokens.accessToken}` },
});
if (discordUserResponse.error) throw discordUserResponse.error;
const discordUser = discordUserResponse.data;
const existingUser = await db.query.users
.findFirst({
where: eq(users.discord_id, discordUser.id),
})
.execute();
if (existingUser) {
const session = await lucia.createSession(
existingUser.id,
{},
{ sessionId: createId() },
);
const sessionCookie = lucia.createSessionCookie(session.id);
console.log(sessionCookie);
setCookie(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes,
);
await db
.update(users)
.set({
name: discordUser.global_name,
image: discordUser.avatar,
})
.where(eq(users.discord_id, discordUser.id))
.returning()
.execute();
return new Response(null, {
status: 302,
headers: { Location: "/config" },
});
}
const userId = createId();
await db.insert(users).values({
id: userId,
discord_id: discordUser.id,
name: discordUser.global_name,
image: discordUser.avatar,
});
await db
.insert(discordTokens)
.values({
userId,
accessToken: tokens.accessToken,
expiresAt: tokens.accessTokenExpiresAt,
refreshToken: tokens.refreshToken,
})
.returning()
.execute();
console.log(createId(), createId(), { warst: createId() });
const session = await lucia.createSession(
userId,
{},
{ sessionId: createId() },
);
const sessionCookie = lucia.createSessionCookie(session.id);
setCookie(
sessionCookie.name,
sessionCookie.value,
sessionCookie.attributes,
);
return new Response(null, {
status: 302,
headers: { Location: "/config" },
});
} catch (e) {
// the specific error message depends on the provider
if (e instanceof OAuth2RequestError) {
// invalid code
return new Response(null, {
status: 400,
});
}
console.error("Unknown error on callback.");
console.error(e);
return new Response(null, {
status: 500,
});
}
}

View file

@ -0,0 +1,24 @@
import { APIEvent } from "@solidjs/start/server/types";
import { generateState } from "arctic";
import { setCookie } from "vinxi/http";
import { discord } from "~/lib/auth";
export async function GET(event: APIEvent) {
const state = generateState();
const url = await discord.createAuthorizationURL(state, {
scopes: ["identify", "guilds", "guilds.members.read"],
});
setCookie(event, "discord_oauth_state", state, {
path: "/",
secure: import.meta.env.PROD,
httpOnly: true,
maxAge: 60 * 10,
sameSite: "lax",
});
return new Response(null, {
status: 302,
headers: { Location: url.toString() },
});
}

View file

@ -0,0 +1,19 @@
import { APIEvent } from "@solidjs/start/server/types";
import { appendHeader } from "vinxi/http";
import { lucia } from "~/lib/auth";
export const GET = async (event: APIEvent) => {
if (!event.nativeEvent.context.session) {
return new Error("Unauthorized");
}
await lucia.invalidateSession(event.nativeEvent.context.session.id);
appendHeader(
event,
"Set-Cookie",
lucia.createBlankSessionCookie().serialize(),
);
return new Response(null, {
status: 302,
headers: { Location: "/" },
});
};

View file

@ -4,6 +4,19 @@ import db from "~/drizzle";
import { guilds } from "~/drizzle/schema";
export const GET = async ({ params }: APIEvent) => {
if (params.guildId === "boot") {
const guilds = await db.query.guilds
.findMany({
with: {
timePlanning: { with: { messages: true } },
matches: true,
},
})
.execute();
return { guilds };
}
const guild = await db.query.guilds
.findFirst({
where: eq(guilds.id, params.guildId),

View file

@ -1,4 +1,3 @@
import { getSession } from "@auth/solid-start";
import { faToggleOff, faToggleOn } from "@fortawesome/pro-regular-svg-icons";
import { useLocation, useNavigate, useParams } from "@solidjs/router";
import { eq } from "drizzle-orm";
@ -16,8 +15,7 @@ import { getRequestEvent } from "solid-js/web";
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
import Layout from "~/components/Layout";
import db from "~/drizzle";
import { accounts } from "~/drizzle/schema";
import { authOptions } from "~/server/auth";
import { discordTokens } from "~/drizzle/schema";
import { paths } from "~/types/discord";
import "../../styles/pages/config.scss";
@ -47,26 +45,21 @@ const getPayload = async (
if (!event) return { success: false, message: "No request event!" };
const pathname = new URL(event.request.url).pathname;
const session = await getSession(event.request, authOptions);
if (!session?.user?.id)
return { success: false, message: "No user with id!" };
const { user } = event.nativeEvent.context;
if (!user) return { success: false, message: "User not logged in!" };
const { DISCORD_ACCESS_TOKEN } = (
await db
.selectDistinct({ DISCORD_ACCESS_TOKEN: accounts.access_token })
.from(accounts)
.where(eq(accounts.userId, session.user?.id))
.limit(1)
.execute()
)[0];
if (!DISCORD_ACCESS_TOKEN)
return { success: false, message: "No discord access token!" };
const tokens = await db.query.discordTokens
.findFirst({
where: eq(discordTokens.userId, user.id),
})
.execute();
if (!tokens) return { success: false, message: "No discord access token!" };
const { GET } = createClient<paths>({
baseUrl: "https://discord.com/api/v10",
});
const guildsRequest = await GET("/users/@me/guilds", {
headers: { Authorization: `Bearer ${DISCORD_ACCESS_TOKEN}` },
headers: { Authorization: `Bearer ${tokens.accessToken}` },
});
const channelsRequest = await GET("/guilds/{guild_id}/channels", {
params: {

View file

@ -1,4 +1,3 @@
import { getSession } from "@auth/solid-start";
import {
faBadgeCheck,
faCircleExclamation,
@ -12,8 +11,7 @@ import { getRequestEvent } from "solid-js/web";
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
import Layout from "~/components/Layout";
import db from "~/drizzle";
import { accounts } from "~/drizzle/schema";
import { authOptions } from "~/server/auth";
import { discordTokens } from "~/drizzle/schema";
import { paths } from "~/types/discord";
import "../../styles/pages/config.scss";
@ -36,26 +34,21 @@ const getPayload = async (): Promise<
if (!event) return { success: false, message: "No request event!" };
const pathname = new URL(event.request.url).pathname;
const session = await getSession(event.request, authOptions);
if (!session?.user?.id)
return { success: false, message: "No user with id!" };
const { user } = event.nativeEvent.context;
if (!user) return { success: false, message: "User not logged in!" };
const { DISCORD_ACCESS_TOKEN } = (
await db
.selectDistinct({ DISCORD_ACCESS_TOKEN: accounts.access_token })
.from(accounts)
.where(eq(accounts.userId, session.user?.id))
.limit(1)
.execute()
)[0];
if (!DISCORD_ACCESS_TOKEN)
return { success: false, message: "No discord access token!" };
const tokens = await db.query.discordTokens
.findFirst({
where: eq(discordTokens.userId, user.id),
})
.execute();
if (!tokens) return { success: false, message: "No discord access token!" };
const { GET } = createClient<paths>({
baseUrl: "https://discord.com/api/v10",
});
const { data: guilds, error } = await GET("/users/@me/guilds", {
headers: { Authorization: `Bearer ${DISCORD_ACCESS_TOKEN}` },
headers: { Authorization: `Bearer ${tokens.accessToken}` },
});
if (error) {

View file

@ -1,38 +0,0 @@
import Discord from "@auth/core/providers/discord";
import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { type SolidAuthConfig } from "@auth/solid-start";
import db from "~/drizzle";
export const authOptions: SolidAuthConfig = {
providers: [
{
...Discord({
clientId: import.meta.env.VITE_DISCORD_CLIENT_ID,
clientSecret: import.meta.env.VITE_DISCORD_CLIENT_SECRET,
}),
authorization:
"https://discord.com/api/oauth2/authorize?scope=identify+email+guilds+guilds.members.read",
},
],
adapter: DrizzleAdapter(db),
secret: import.meta.env.VITE_AUTH_SECRET,
callbacks: {
// @ts-ignore
session: ({ session, user }) => {
if (session?.user) {
session.user.id = user.id;
}
return session;
},
},
pages: {
// signIn: "/signin",
// signOut: "/signout",
// error: '/auth/error', // Error code passed in query string as ?error=
// verifyRequest: '/auth/verify-request', // (used for check email message)
// newUser: '/auth/new-user' // New users will be directed here on first sign in (leave the property out if not of interest)
},
redirectProxyUrl: import.meta.env.DEV
? import.meta.env.VITE_AUTH_REDIRECT_PROXY_URL
: undefined,
};