Unfinished
This commit is contained in:
parent
18c6535d1c
commit
6b388729d9
25 changed files with 1598 additions and 2352 deletions
|
@ -46,7 +46,7 @@ To get started with li'l Judd, follow the instructions below.
|
||||||
Create a `.env` file in the root directory and add the following variables:
|
Create a `.env` file in the root directory and add the following variables:
|
||||||
|
|
||||||
```env
|
```env
|
||||||
VITE_DISCORD_CLIENT=your_discord_client_id
|
VITE_DISCORD_CLIENT_ID=your_discord_client_id
|
||||||
VITE_DISCORD_CLIENT_SECRET=your_discord_client_secret
|
VITE_DISCORD_CLIENT_SECRET=your_discord_client_secret
|
||||||
VITE_DISCORD_BOT_TOKEN=your_discord_bot_token
|
VITE_DISCORD_BOT_TOKEN=your_discord_bot_token
|
||||||
VITE_DISCORD_BOT_PERMISSIONS=18977581952080
|
VITE_DISCORD_BOT_PERMISSIONS=18977581952080
|
||||||
|
@ -56,16 +56,14 @@ To get started with li'l Judd, follow the instructions below.
|
||||||
VITE_DATABASE_URL=your_database_url
|
VITE_DATABASE_URL=your_database_url
|
||||||
```
|
```
|
||||||
|
|
||||||
Recieve your discord applications client id & secret, as well as the bot token from the [Applications dashboard](https://discord.com/developers/applications/).
|
Recieve your discord applications `CLIENT_ID` & `CLIENT_SECRET`, as well as the bot token from the [Applications dashboard](https://discord.com/developers/applications/).
|
||||||
|
|
||||||
How to generate your `VITE_AUTH_SECRET` with [`openssl rand -base64 32`](https://authjs.dev/reference/core#secret).
|
Your `VITE_AUTH_REDIRECT_URL` should look like this: `https://<hostname>/api/auth/callback/discord`.
|
||||||
|
|
||||||
Composite your `VITE_DATABASE_URL` like [`postgres://postgres:adminadmin@0.0.0.0:5432/db`](https://orm.drizzle.team/docs/get-started-postgresql#postgresjs).
|
Composite your `VITE_DATABASE_URL` like [`postgres://postgres:adminadmin@0.0.0.0:5432/db`](https://orm.drizzle.team/docs/get-started-postgresql#postgresjs).
|
||||||
|
|
||||||
#### Development
|
#### Development
|
||||||
|
|
||||||
Specify `VITE_AUTH_REDIRECT_PROXY_URL` only if necessary, particularly when setting up a reverse proxy to test authentication with callbacks to your development box. [Auth.js Docs Reference](https://authjs.dev/reference/nextjs/#redirectproxyurl)
|
|
||||||
|
|
||||||
The duplicate `DATABASE_URL` is only needed for Drizzle Studio.
|
The duplicate `DATABASE_URL` is only needed for Drizzle Studio.
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
|
@ -6,6 +6,6 @@ export default {
|
||||||
out: "./src/drizzle/migrations",
|
out: "./src/drizzle/migrations",
|
||||||
driver: "pg",
|
driver: "pg",
|
||||||
dbCredentials: {
|
dbCredentials: {
|
||||||
connectionString: process.env.DATABASE_URL ?? "",
|
connectionString: process.env.DATABASE_URL!,
|
||||||
},
|
},
|
||||||
} satisfies Config;
|
} satisfies Config;
|
||||||
|
|
24
package.json
24
package.json
|
@ -13,9 +13,6 @@
|
||||||
"drizzle-studio": "drizzle-kit studio"
|
"drizzle-studio": "drizzle-kit studio"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/core": "^0.19.0",
|
|
||||||
"@auth/drizzle-adapter": "^0.6.3",
|
|
||||||
"@auth/solid-start": "0.1.2",
|
|
||||||
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
"@fortawesome/fontawesome-svg-core": "^6.5.1",
|
||||||
"@fortawesome/pro-duotone-svg-icons": "^6.5.1",
|
"@fortawesome/pro-duotone-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/pro-light-svg-icons": "^6.5.1",
|
"@fortawesome/pro-light-svg-icons": "^6.5.1",
|
||||||
|
@ -23,29 +20,34 @@
|
||||||
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
|
"@fortawesome/pro-solid-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/pro-thin-svg-icons": "^6.5.1",
|
"@fortawesome/pro-thin-svg-icons": "^6.5.1",
|
||||||
"@fortawesome/sharp-solid-svg-icons": "^6.5.1",
|
"@fortawesome/sharp-solid-svg-icons": "^6.5.1",
|
||||||
|
"@lucia-auth/adapter-drizzle": "^1.0.2",
|
||||||
|
"@paralleldrive/cuid2": "^2.2.2",
|
||||||
"@solidjs/meta": "^0.29.3",
|
"@solidjs/meta": "^0.29.3",
|
||||||
"@solidjs/router": "^0.12.0",
|
"@solidjs/router": "^0.12.3",
|
||||||
"@solidjs/start": "^0.5.4",
|
"@solidjs/start": "^0.5.9",
|
||||||
|
"arctic": "^1.1.6",
|
||||||
"drizzle-orm": "^0.29.3",
|
"drizzle-orm": "^0.29.3",
|
||||||
|
"lucia": "^3.0.1",
|
||||||
"moment-timezone": "^0.5.45",
|
"moment-timezone": "^0.5.45",
|
||||||
"openapi-fetch": "^0.8.2",
|
"openapi-fetch": "^0.9.1",
|
||||||
"postgres": "^3.4.3",
|
"postgres": "^3.4.3",
|
||||||
"solid-js": "^1.8.14",
|
"solid-js": "^1.8.15",
|
||||||
"vinxi": "^0.2.1"
|
"vinxi": "^0.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@typescript-eslint/eslint-plugin": "^6.21.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.1",
|
||||||
"dotenv": "^16.4.2",
|
"dotenv": "^16.4.4",
|
||||||
"drizzle-kit": "^0.20.14",
|
"drizzle-kit": "^0.20.14",
|
||||||
"drizzle-zod": "^0.5.1",
|
"drizzle-zod": "^0.5.1",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-config-prettier": "^9.1.0",
|
"eslint-config-prettier": "^9.1.0",
|
||||||
"eslint-plugin-solid": "^0.13.1",
|
"eslint-plugin-solid": "^0.13.1",
|
||||||
|
"h3": "^1.10.1",
|
||||||
"openapi-typescript": "^6.7.4",
|
"openapi-typescript": "^6.7.4",
|
||||||
"pg": "^8.11.3",
|
"pg": "^8.11.3",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-organize-imports": "^3.2.4",
|
"prettier-plugin-organize-imports": "^3.2.4",
|
||||||
"sass": "^1.70.0",
|
"sass": "^1.71.0",
|
||||||
"typescript": "^5.3.3",
|
"typescript": "^5.3.3",
|
||||||
"zod": "3.22.4"
|
"zod": "3.22.4"
|
||||||
},
|
},
|
||||||
|
|
3363
pnpm-lock.yaml
3363
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
@ -36,7 +36,7 @@
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"bot_token": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -79,7 +79,7 @@
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"bot_token": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -113,7 +113,7 @@
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"bot_token": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -159,7 +159,7 @@
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"bot_token": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -203,7 +203,7 @@
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"bot_token": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -252,7 +252,7 @@
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"bot_token": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -308,7 +308,7 @@
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"bot_token": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -374,7 +374,7 @@
|
||||||
},
|
},
|
||||||
"security": [
|
"security": [
|
||||||
{
|
{
|
||||||
"api_key": []
|
"bot_token": []
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -390,9 +390,6 @@
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/guildConfig"
|
"$ref": "#/components/schemas/guildConfig"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"accessToken": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -536,10 +533,10 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securitySchemes": {
|
"securitySchemes": {
|
||||||
"api_key": {
|
"bot_token": {
|
||||||
"type": "apiKey",
|
"type": "http",
|
||||||
"name": "api_key",
|
"scheme": "bearer",
|
||||||
"in": "header"
|
"bearerFormat": "JWT"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,17 +6,13 @@ import NavUser from "./NavUser";
|
||||||
|
|
||||||
export function Li(props: {
|
export function Li(props: {
|
||||||
href: string;
|
href: string;
|
||||||
action?: () => void;
|
rel?: string;
|
||||||
name?: string;
|
name?: string;
|
||||||
children?: JSX.Element;
|
children?: JSX.Element;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<li class="navElem flex-row thick">
|
<li class="navElem flex-row thick">
|
||||||
<a
|
<a class="flex-row" href={props.href} rel={props.rel}>
|
||||||
class="flex-row"
|
|
||||||
href={props.href}
|
|
||||||
onClick={() => props.action && props.action()}
|
|
||||||
>
|
|
||||||
{props.children ?? <></>}
|
{props.children ?? <></>}
|
||||||
<Show when={props.name}>
|
<Show when={props.name}>
|
||||||
<span>{props.name}</span>
|
<span>{props.name}</span>
|
||||||
|
|
|
@ -1,50 +1,37 @@
|
||||||
import { getSession } from "@auth/solid-start";
|
|
||||||
import { signIn, signOut } from "@auth/solid-start/client";
|
|
||||||
import {
|
import {
|
||||||
faArrowRightFromBracket,
|
faArrowRightFromBracket,
|
||||||
faArrowRightToBracket,
|
faArrowRightToBracket,
|
||||||
faGear,
|
faGear,
|
||||||
} from "@fortawesome/pro-regular-svg-icons";
|
} from "@fortawesome/pro-regular-svg-icons";
|
||||||
import { eq } from "drizzle-orm";
|
import { User } from "lucia";
|
||||||
import { Show, createResource } from "solid-js";
|
import { Show, createResource } from "solid-js";
|
||||||
import { getRequestEvent } from "solid-js/web";
|
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 { FontAwesomeIcon } from "./FontAwesomeIcon";
|
||||||
import { Li } from "./NavBar";
|
import { Li } from "./NavBar";
|
||||||
|
|
||||||
const initialUser = {
|
async function getUser(): Promise<
|
||||||
id: "",
|
| ({
|
||||||
name: null as string | null,
|
success: false;
|
||||||
email: "",
|
message: string;
|
||||||
emailVerified: null as Date | null,
|
// user?: undefined;
|
||||||
image: null as string | null,
|
} & Partial<User>)
|
||||||
};
|
| ({
|
||||||
|
success: true;
|
||||||
async function getUser() {
|
message?: undefined;
|
||||||
|
} & User)
|
||||||
|
> {
|
||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
const event = getRequestEvent();
|
const event = getRequestEvent();
|
||||||
if (!event)
|
if (!event) return { success: false, message: "No request event!" };
|
||||||
return { success: false, message: "No request event!", ...initialUser };
|
|
||||||
|
|
||||||
const session = await getSession(event.request, authOptions);
|
const pathname = new URL(event.request.url).pathname;
|
||||||
if (!session?.user?.id)
|
const { user } = event.nativeEvent.context;
|
||||||
return { success: false, message: "No user with id!", ...initialUser };
|
if (!user) return { success: false, message: "User not logged in!" };
|
||||||
|
|
||||||
const user = (
|
console.log("userInfo", pathname, "success");
|
||||||
await db
|
|
||||||
.selectDistinct()
|
|
||||||
.from(users)
|
|
||||||
.where(eq(users.id, session.user?.id))
|
|
||||||
.limit(1)
|
|
||||||
.execute()
|
|
||||||
)[0];
|
|
||||||
|
|
||||||
console.log("userInfo", "success");
|
return { success: true, ...user };
|
||||||
|
|
||||||
return { success: true, message: "", ...user };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function NavUser() {
|
function NavUser() {
|
||||||
|
@ -55,16 +42,20 @@ function NavUser() {
|
||||||
|
|
||||||
return user;
|
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 (
|
return (
|
||||||
<Show
|
<Show
|
||||||
when={user()?.id}
|
when={user()?.id}
|
||||||
fallback={
|
fallback={
|
||||||
<Li
|
<Li href="/api/auth/login" name="Login" rel="external">
|
||||||
href="#"
|
|
||||||
name="Login"
|
|
||||||
action={() => signIn("discord", { callbackUrl: "/config" })}
|
|
||||||
>
|
|
||||||
<FontAwesomeIcon
|
<FontAwesomeIcon
|
||||||
class="secondary"
|
class="secondary"
|
||||||
icon={faArrowRightToBracket}
|
icon={faArrowRightToBracket}
|
||||||
|
@ -75,11 +66,11 @@ function NavUser() {
|
||||||
>
|
>
|
||||||
<Li href="/config">
|
<Li href="/config">
|
||||||
<div class="swap lower">
|
<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" />
|
<FontAwesomeIcon class="secondary" icon={faGear} size="xl" />
|
||||||
</div>
|
</div>
|
||||||
</Li>
|
</Li>
|
||||||
<Li href="#" action={() => signOut({ callbackUrl: "/" })}>
|
<Li href="/api/auth/logout" rel="external">
|
||||||
<FontAwesomeIcon icon={faArrowRightFromBracket} size="xl" />
|
<FontAwesomeIcon icon={faArrowRightFromBracket} size="xl" />
|
||||||
</Li>
|
</Li>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
import type { AdapterAccount } from "@auth/core/adapters";
|
|
||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import {
|
import {
|
||||||
boolean,
|
boolean,
|
||||||
integer,
|
integer,
|
||||||
pgTable,
|
pgTable,
|
||||||
primaryKey,
|
|
||||||
serial,
|
serial,
|
||||||
smallint,
|
smallint,
|
||||||
text,
|
text,
|
||||||
|
@ -13,56 +11,31 @@ import {
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
export const users = pgTable("user", {
|
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"),
|
name: text("name"),
|
||||||
email: text("email").notNull(),
|
|
||||||
emailVerified: timestamp("emailVerified", { mode: "date" }),
|
|
||||||
image: text("image"),
|
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", {
|
export const sessions = pgTable("session", {
|
||||||
sessionToken: text("sessionToken").notNull().primaryKey(),
|
id: varchar("id", { length: 24 }).primaryKey(),
|
||||||
userId: text("userId")
|
userId: varchar("user_id", { length: 24 })
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => users.id, { onDelete: "cascade" }),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
expires: timestamp("expires", { mode: "date" }).notNull(),
|
expiresAt: timestamp("expires_at", {
|
||||||
|
withTimezone: true,
|
||||||
|
mode: "date",
|
||||||
|
}).notNull(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const verificationTokens = pgTable(
|
export const discordTokens = pgTable("tokens", {
|
||||||
"verificationToken",
|
userId: varchar("user_id", { length: 24 })
|
||||||
{
|
.primaryKey()
|
||||||
identifier: text("identifier").notNull(),
|
.references(() => users.id, { onDelete: "cascade" }),
|
||||||
token: text("token").notNull(),
|
refreshToken: text("refresh_token").notNull(),
|
||||||
expires: timestamp("expires", { mode: "date" }).notNull(),
|
accessToken: text("access_token").notNull(),
|
||||||
},
|
expiresAt: timestamp("expires_at", { mode: "date" }).notNull(),
|
||||||
(vt) => ({
|
});
|
||||||
compoundKey: primaryKey({ columns: [vt.identifier, vt.token] }),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const matchPlannings = pgTable("match_planning", {
|
export const matchPlannings = pgTable("match_planning", {
|
||||||
id: serial("id").primaryKey(),
|
id: serial("id").primaryKey(),
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import { mount, StartClient } from "@solidjs/start/client";
|
import { mount, StartClient } from "@solidjs/start/client";
|
||||||
|
|
||||||
mount(() => <StartClient />, document.getElementById("app"));
|
mount(() => <StartClient />, document.getElementById("app")!);
|
||||||
|
|
46
src/lib/auth.ts
Normal file
46
src/lib/auth.ts
Normal 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
54
src/middleware.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,4 +0,0 @@
|
||||||
import { SolidAuth } from "@auth/solid-start"
|
|
||||||
import { authOptions } from "~/server/auth"
|
|
||||||
|
|
||||||
export const { GET, POST } = SolidAuth(authOptions)
|
|
132
src/routes/api/auth/callback/discord.ts
Normal file
132
src/routes/api/auth/callback/discord.ts
Normal 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
24
src/routes/api/auth/login.ts
Normal file
24
src/routes/api/auth/login.ts
Normal 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() },
|
||||||
|
});
|
||||||
|
}
|
19
src/routes/api/auth/logout.ts
Normal file
19
src/routes/api/auth/logout.ts
Normal 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: "/" },
|
||||||
|
});
|
||||||
|
};
|
|
@ -4,6 +4,19 @@ import db from "~/drizzle";
|
||||||
import { guilds } from "~/drizzle/schema";
|
import { guilds } from "~/drizzle/schema";
|
||||||
|
|
||||||
export const GET = async ({ params }: APIEvent) => {
|
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
|
const guild = await db.query.guilds
|
||||||
.findFirst({
|
.findFirst({
|
||||||
where: eq(guilds.id, params.guildId),
|
where: eq(guilds.id, params.guildId),
|
|
@ -1,4 +1,3 @@
|
||||||
import { getSession } from "@auth/solid-start";
|
|
||||||
import { faToggleOff, faToggleOn } from "@fortawesome/pro-regular-svg-icons";
|
import { faToggleOff, faToggleOn } from "@fortawesome/pro-regular-svg-icons";
|
||||||
import { useLocation, useNavigate, useParams } from "@solidjs/router";
|
import { useLocation, useNavigate, useParams } from "@solidjs/router";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
|
@ -16,8 +15,7 @@ import { getRequestEvent } from "solid-js/web";
|
||||||
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
||||||
import Layout from "~/components/Layout";
|
import Layout from "~/components/Layout";
|
||||||
import db from "~/drizzle";
|
import db from "~/drizzle";
|
||||||
import { accounts } from "~/drizzle/schema";
|
import { discordTokens } from "~/drizzle/schema";
|
||||||
import { authOptions } from "~/server/auth";
|
|
||||||
import { paths } from "~/types/discord";
|
import { paths } from "~/types/discord";
|
||||||
import "../../styles/pages/config.scss";
|
import "../../styles/pages/config.scss";
|
||||||
|
|
||||||
|
@ -47,26 +45,21 @@ const getPayload = async (
|
||||||
if (!event) return { success: false, message: "No request event!" };
|
if (!event) return { success: false, message: "No request event!" };
|
||||||
|
|
||||||
const pathname = new URL(event.request.url).pathname;
|
const pathname = new URL(event.request.url).pathname;
|
||||||
const session = await getSession(event.request, authOptions);
|
const { user } = event.nativeEvent.context;
|
||||||
if (!session?.user?.id)
|
if (!user) return { success: false, message: "User not logged in!" };
|
||||||
return { success: false, message: "No user with id!" };
|
|
||||||
|
|
||||||
const { DISCORD_ACCESS_TOKEN } = (
|
const tokens = await db.query.discordTokens
|
||||||
await db
|
.findFirst({
|
||||||
.selectDistinct({ DISCORD_ACCESS_TOKEN: accounts.access_token })
|
where: eq(discordTokens.userId, user.id),
|
||||||
.from(accounts)
|
})
|
||||||
.where(eq(accounts.userId, session.user?.id))
|
.execute();
|
||||||
.limit(1)
|
if (!tokens) return { success: false, message: "No discord access token!" };
|
||||||
.execute()
|
|
||||||
)[0];
|
|
||||||
if (!DISCORD_ACCESS_TOKEN)
|
|
||||||
return { success: false, message: "No discord access token!" };
|
|
||||||
|
|
||||||
const { GET } = createClient<paths>({
|
const { GET } = createClient<paths>({
|
||||||
baseUrl: "https://discord.com/api/v10",
|
baseUrl: "https://discord.com/api/v10",
|
||||||
});
|
});
|
||||||
const guildsRequest = await GET("/users/@me/guilds", {
|
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", {
|
const channelsRequest = await GET("/guilds/{guild_id}/channels", {
|
||||||
params: {
|
params: {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import { getSession } from "@auth/solid-start";
|
|
||||||
import {
|
import {
|
||||||
faBadgeCheck,
|
faBadgeCheck,
|
||||||
faCircleExclamation,
|
faCircleExclamation,
|
||||||
|
@ -12,8 +11,7 @@ import { getRequestEvent } from "solid-js/web";
|
||||||
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
import { FontAwesomeIcon } from "~/components/FontAwesomeIcon";
|
||||||
import Layout from "~/components/Layout";
|
import Layout from "~/components/Layout";
|
||||||
import db from "~/drizzle";
|
import db from "~/drizzle";
|
||||||
import { accounts } from "~/drizzle/schema";
|
import { discordTokens } from "~/drizzle/schema";
|
||||||
import { authOptions } from "~/server/auth";
|
|
||||||
import { paths } from "~/types/discord";
|
import { paths } from "~/types/discord";
|
||||||
import "../../styles/pages/config.scss";
|
import "../../styles/pages/config.scss";
|
||||||
|
|
||||||
|
@ -36,26 +34,21 @@ const getPayload = async (): Promise<
|
||||||
if (!event) return { success: false, message: "No request event!" };
|
if (!event) return { success: false, message: "No request event!" };
|
||||||
|
|
||||||
const pathname = new URL(event.request.url).pathname;
|
const pathname = new URL(event.request.url).pathname;
|
||||||
const session = await getSession(event.request, authOptions);
|
const { user } = event.nativeEvent.context;
|
||||||
if (!session?.user?.id)
|
if (!user) return { success: false, message: "User not logged in!" };
|
||||||
return { success: false, message: "No user with id!" };
|
|
||||||
|
|
||||||
const { DISCORD_ACCESS_TOKEN } = (
|
const tokens = await db.query.discordTokens
|
||||||
await db
|
.findFirst({
|
||||||
.selectDistinct({ DISCORD_ACCESS_TOKEN: accounts.access_token })
|
where: eq(discordTokens.userId, user.id),
|
||||||
.from(accounts)
|
})
|
||||||
.where(eq(accounts.userId, session.user?.id))
|
.execute();
|
||||||
.limit(1)
|
if (!tokens) return { success: false, message: "No discord access token!" };
|
||||||
.execute()
|
|
||||||
)[0];
|
|
||||||
if (!DISCORD_ACCESS_TOKEN)
|
|
||||||
return { success: false, message: "No discord access token!" };
|
|
||||||
|
|
||||||
const { GET } = createClient<paths>({
|
const { GET } = createClient<paths>({
|
||||||
baseUrl: "https://discord.com/api/v10",
|
baseUrl: "https://discord.com/api/v10",
|
||||||
});
|
});
|
||||||
const { data: guilds, error } = await GET("/users/@me/guilds", {
|
const { data: guilds, error } = await GET("/users/@me/guilds", {
|
||||||
headers: { Authorization: `Bearer ${DISCORD_ACCESS_TOKEN}` },
|
headers: { Authorization: `Bearer ${tokens.accessToken}` },
|
||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
|
|
@ -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,
|
|
||||||
};
|
|
|
@ -1,3 +1,7 @@
|
||||||
import { defineConfig } from "@solidjs/start/config";
|
import { defineConfig } from "@solidjs/start/config";
|
||||||
|
|
||||||
export default defineConfig({});
|
export default defineConfig({
|
||||||
|
start: {
|
||||||
|
middleware: "./src/middleware.ts",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
Loading…
Reference in a new issue