mirror of
https://github.com/SrIzan10/hctv.git
synced 2026-06-06 00:56:56 +00:00
feat: slack auth
This commit is contained in:
@@ -1,2 +0,0 @@
|
||||
# from the dev postgres containeer
|
||||
DATABASE_URL=postgresql://postgres:dfsjhkdswkjntelsmldbfvsgknl5t@localhost:5555/postgres
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -35,3 +35,5 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
certificates
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "docker compose --file dev/docker-compose.yml up -d && next dev",
|
||||
"dev": "docker compose --file dev/docker-compose.yml up -d && next dev --turbo --experimental-https",
|
||||
"setup": "docker compose --file dev/docker-compose.yml up -d && prisma migrate deploy",
|
||||
"build": "prisma generate && next build",
|
||||
"start": "next start",
|
||||
@@ -21,6 +21,7 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.1.2",
|
||||
"@radix-ui/react-label": "^2.1.1",
|
||||
"@radix-ui/react-slot": "^1.1.1",
|
||||
"arctic": "^3.1.1",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.0",
|
||||
"lucia": "^3.2.2",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- You are about to drop the column `hashed_password` on the `User` table. All the data in the column will be lost.
|
||||
- Added the required column `slack_id` to the `User` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" DROP COLUMN "hashed_password",
|
||||
ADD COLUMN "slack_id" TEXT NOT NULL;
|
||||
@@ -0,0 +1,8 @@
|
||||
/*
|
||||
Warnings:
|
||||
|
||||
- Added the required column `pfpUrl` to the `User` table without a default value. This is not possible if the table is not empty.
|
||||
|
||||
*/
|
||||
-- AlterTable
|
||||
ALTER TABLE "User" ADD COLUMN "pfpUrl" TEXT NOT NULL;
|
||||
@@ -1,3 +1,3 @@
|
||||
# Please do not edit this file manually
|
||||
# It should be added in your version-control system (i.e. Git)
|
||||
# It should be added in your version-control system (e.g., Git)
|
||||
provider = "postgresql"
|
||||
@@ -15,8 +15,9 @@ datasource db {
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
slack_id String
|
||||
pfpUrl String
|
||||
username String @unique
|
||||
hashed_password String
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
|
||||
118
src/app/(public)/auth/slack/callback/route.ts
Normal file
118
src/app/(public)/auth/slack/callback/route.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { slack, lucia } from '@/lib/auth';
|
||||
import { cookies as nextCookies } from 'next/headers';
|
||||
import { decodeIdToken, OAuth2RequestError } from 'arctic';
|
||||
import { generateIdFromEntropySize } from 'lucia';
|
||||
import prisma from '@/lib/db';
|
||||
|
||||
export async function GET(request: Request): Promise<Response> {
|
||||
const cookies = await nextCookies();
|
||||
const url = new URL(request.url);
|
||||
const code = url.searchParams.get("code");
|
||||
const state = url.searchParams.get("state");
|
||||
const storedState = cookies.get("slack_oauth_state")?.value ?? null;
|
||||
if (!code || !state || !storedState || state !== storedState) {
|
||||
return new Response(null, {
|
||||
status: 400
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
const tokens = await slack.validateAuthorizationCode(code);
|
||||
const accessToken = tokens.accessToken()
|
||||
const slackUserResponse = await fetch('https://slack.com/api/openid.connect.userInfo', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${accessToken}`,
|
||||
},
|
||||
});
|
||||
const slackUser: SlackUserInfo = await slackUserResponse.json();
|
||||
|
||||
const existingUser = await prisma.user.findFirst({
|
||||
where: {
|
||||
slack_id: slackUser.sub,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingUser) {
|
||||
const session = await lucia.createSession(existingUser.id, {});
|
||||
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||
cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: '/',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const userId = generateIdFromEntropySize(10);
|
||||
|
||||
await prisma.user.create({
|
||||
data: {
|
||||
id: userId,
|
||||
slack_id: slackUser.sub,
|
||||
username: slackUser.name,
|
||||
pfpUrl: slackUser.picture,
|
||||
},
|
||||
});
|
||||
|
||||
const session = await lucia.createSession(userId, {});
|
||||
const sessionCookie = lucia.createSessionCookie(session.id);
|
||||
cookies.set(sessionCookie.name, sessionCookie.value, sessionCookie.attributes);
|
||||
return new Response(null, {
|
||||
status: 302,
|
||||
headers: {
|
||||
Location: '/',
|
||||
},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
// the specific error message depends on the provider
|
||||
if (e instanceof OAuth2RequestError) {
|
||||
// invalid code
|
||||
return new Response(null, {
|
||||
status: 400,
|
||||
});
|
||||
}
|
||||
return new Response(null, {
|
||||
status: 500,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
interface SlackUserInfo {
|
||||
// OpenID Connect standard fields
|
||||
ok: boolean;
|
||||
sub: string;
|
||||
email: string;
|
||||
email_verified: boolean;
|
||||
date_email_verified: number;
|
||||
name: string;
|
||||
picture: string;
|
||||
given_name: string;
|
||||
family_name: string;
|
||||
locale: string;
|
||||
|
||||
// Slack-specific fields
|
||||
['https://slack.com/user_id']: string;
|
||||
['https://slack.com/team_id']: string;
|
||||
['https://slack.com/team_name']: string;
|
||||
['https://slack.com/team_domain']: string;
|
||||
|
||||
// User image URLs
|
||||
['https://slack.com/user_image_24']: string;
|
||||
['https://slack.com/user_image_32']: string;
|
||||
['https://slack.com/user_image_48']: string;
|
||||
['https://slack.com/user_image_72']: string;
|
||||
['https://slack.com/user_image_192']: string;
|
||||
['https://slack.com/user_image_512']: string;
|
||||
|
||||
// Team image URLs
|
||||
['https://slack.com/team_image_34']?: string;
|
||||
['https://slack.com/team_image_44']?: string;
|
||||
['https://slack.com/team_image_68']?: string;
|
||||
['https://slack.com/team_image_88']?: string;
|
||||
['https://slack.com/team_image_102']?: string;
|
||||
['https://slack.com/team_image_132']?: string;
|
||||
['https://slack.com/team_image_230']?: string;
|
||||
['https://slack.com/team_image_default']?: boolean;
|
||||
}
|
||||
18
src/app/(public)/auth/slack/route.ts
Normal file
18
src/app/(public)/auth/slack/route.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { generateState } from "arctic";
|
||||
import { slack } from "@/lib/auth";
|
||||
import { cookies } from "next/headers";
|
||||
|
||||
export async function GET(): Promise<Response> {
|
||||
const state = generateState();
|
||||
const url = slack.createAuthorizationURL(state, ['openid', 'profile']);
|
||||
|
||||
(await cookies()).set("slack_oauth_state", state, {
|
||||
path: "/",
|
||||
secure: process.env.NODE_ENV === "production",
|
||||
httpOnly: true,
|
||||
maxAge: 60 * 10,
|
||||
sameSite: "lax"
|
||||
});
|
||||
|
||||
return Response.redirect(url);
|
||||
}
|
||||
@@ -1,11 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
/**
|
||||
* v0 by Vercel.
|
||||
* @see https://v0.dev/t/igzEEdGqAvH
|
||||
* Documentation: https://v0.dev/docs#integrating-generated-code-into-your-nextjs-app
|
||||
*/
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
@@ -21,6 +16,7 @@ import { useSession } from '@/lib/providers/SessionProvider';
|
||||
import Link from 'next/link';
|
||||
import MobileNavbarLinks from '../MobileNavbarLinks/MobileNavbarLinks';
|
||||
import { ThemeSwitcher } from '../ThemeSwitcher/ThemeSwitcher';
|
||||
import { Slack } from 'lucide-react';
|
||||
|
||||
export const links = [
|
||||
{ href: '/', name: 'Home' },
|
||||
@@ -60,7 +56,7 @@ export default function Navbar() {
|
||||
<DropdownMenuTrigger asChild className="cursor-pointer">
|
||||
<Avatar>
|
||||
{/* TODO: Implement avatar system */}
|
||||
{/*<AvatarImage src={"https://srizan.dev/pfp.webp"} alt="@srizan" />*/}
|
||||
<AvatarImage src={user.pfpUrl} alt={`@${user.username}`} />
|
||||
<AvatarFallback>{user.username}</AvatarFallback>
|
||||
</Avatar>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -81,8 +77,8 @@ export default function Navbar() {
|
||||
</DropdownMenu>
|
||||
</>
|
||||
) : (
|
||||
<Link href="/auth/login">
|
||||
<Button variant="outline">Sign in</Button>
|
||||
<Link href="/auth/slack">
|
||||
<Button variant="outline" className='gap-2'><Slack className='w-4 h-4' />Sign in</Button>
|
||||
</Link>
|
||||
)}
|
||||
</nav>
|
||||
|
||||
@@ -3,8 +3,10 @@ import { Lucia, Session, User } from 'lucia';
|
||||
import prisma from '../db';
|
||||
import { cache } from 'react';
|
||||
import { cookies } from 'next/headers';
|
||||
import { Slack } from 'arctic';
|
||||
|
||||
const adapter = new PrismaAdapter(prisma.session, prisma.user);
|
||||
export const slack = new Slack(process.env.SLACK_ID!, process.env.SLACK_SECRET!, process.env.SLACK_REDIRECT_URI!);
|
||||
|
||||
export const lucia = new Lucia(adapter, {
|
||||
sessionCookie: {
|
||||
@@ -18,7 +20,9 @@ export const lucia = new Lucia(adapter, {
|
||||
},
|
||||
getUserAttributes: (attributes) => {
|
||||
return {
|
||||
slack_id: attributes.slack_id,
|
||||
username: attributes.username,
|
||||
pfpUrl: attributes.pfpUrl,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -58,5 +62,7 @@ declare module 'lucia' {
|
||||
}
|
||||
|
||||
interface DatabaseUserAttributes {
|
||||
slack_id: string;
|
||||
username: string;
|
||||
pfpUrl: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user