Skip to content

Commit

Permalink
a user can create an account and sign in via Oauth
Browse files Browse the repository at this point in the history
  • Loading branch information
mcstepp committed Nov 29, 2024
1 parent 4d61073 commit 7f0ed5c
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 141 deletions.
13 changes: 4 additions & 9 deletions apps/dashboard/app/auth/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,14 @@
import { auth } from "@/lib/auth/index";
import { OAuthStrategy } from "@/lib/auth/interface";

export async function initiateOAuthSignIn(provider: OAuthStrategy) {
// this just serves as a flyweight between the pure client-side and the pure server-side auth provider.
export async function initiateOAuthSignIn({provider, redirectUrlComplete} : {provider: OAuthStrategy, redirectUrlComplete: string}) {
try {
// WorkOS - will redirect internally
// No Auth -
// X Provider - should handle internally, whether its a redirect to an authorization url or returning a Response
const response = auth.signInViaOAuth({
return await auth.signInViaOAuth({
provider,
redirectUri: '/auth/sso-callback'
redirectUrlComplete
});

// If the provider's implementation returns a Response, use it
return response;

} catch (error) {
console.error('OAuth initialization error:', error);
return { error: error instanceof Error ? error.message : 'Authentication failed' };
Expand Down
10 changes: 5 additions & 5 deletions apps/dashboard/app/auth/sign-in/oauth-signin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ import { OAuthStrategy } from "@/lib/auth/interface";
import * as React from "react";
import { OAuthButton } from "../oauth-button";
import { LastUsed, useLastUsed } from "./last_used";
import { useSearchParams } from "next/navigation";
import { initiateOAuthSignIn } from "../actions";

export const OAuthSignIn: React.FC = () => {
const [isLoading, setIsLoading] = React.useState<OAuthStrategy | null>(null);
const [lastUsed, setLastUsed] = useLastUsed();
const searchParams = useSearchParams();
const redirectUrlComplete = searchParams?.get("redirect") ?? "/apis";

const oauthSignIn = async (provider: OAuthStrategy) => {
try {
setIsLoading(provider);
setLastUsed(provider);

const result = await initiateOAuthSignIn(provider);

if (result?.error) {
throw new Error(result.error);
}
const authorizationURL = await initiateOAuthSignIn({provider, redirectUrlComplete});
window.location.assign(authorizationURL);

} catch (err) {
console.error(err);
Expand Down
33 changes: 17 additions & 16 deletions apps/dashboard/app/auth/sign-up/email-signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,22 +89,23 @@ export const EmailSignUp: React.FC<Props> = ({ setError, setVerification }) => {

try {
setIsLoading(true);
await auth
.signUpViaEmail(email)
.then(() => {
setIsLoading(false);
// set verification to true so we can show the code input
setVerification(true);
})
.catch((err) => {
setIsLoading(false);
console.log("sign up via email errors", err);
// if (err.errors[0].code === "form_identifier_exists") {
// toast.error("It looks like you have an account. Please use sign in");
// } else {
// toast.error("We couldn't sign you up. Please try again later");
// }
});
// TODO: `auth` is server-side, you have to call it through a server action dummy
// await auth
// .signUpViaEmail(email)
// .then(() => {
// setIsLoading(false);
// // set verification to true so we can show the code input
// setVerification(true);
// })
// .catch((err: any) => {
// setIsLoading(false);
// console.log("sign up via email errors", err);
// // if (err.errors[0].code === "form_identifier_exists") {
// // toast.error("It looks like you have an account. Please use sign in");
// // } else {
// // toast.error("We couldn't sign you up. Please try again later");
// // }
// });
} catch (error) {
setIsLoading(false);
console.error(error);
Expand Down
74 changes: 35 additions & 39 deletions apps/dashboard/app/auth/sign-up/oauth-signup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,53 +2,49 @@
import { Loading } from "@/components/dashboard/loading";
import { GitHub, Google } from "@/components/ui/icons";
import { toast } from "@/components/ui/toaster";
import { useSignUp } from "@clerk/nextjs";
import type { OAuthStrategy } from "@clerk/types";
import type { OAuthStrategy } from "@/lib/auth/interface"
import { useRouter } from 'next/navigation';
import * as React from "react";
import { OAuthButton } from "../oauth-button";
import { initiateOAuthSignIn } from "../actions";

export function OAuthSignUp() {
const [isLoading, setIsLoading] = React.useState<OAuthStrategy | null>(null);

//TODO: check if auth provider is available, otherwise we can't do anything
// const { signUp, isLoaded: signupLoaded } = useSignUp();
const router = useRouter();
const redirectUrlComplete = "/new";

const oauthSignIn = async (provider: OAuthStrategy) => {
if (!signupLoaded) {
return null;
}
try {
setIsLoading(provider);
await signUp.authenticateWithRedirect({
strategy: provider,
redirectUrl: "/auth/sso-callback",
redirectUrlComplete: "/new",
});
} catch (cause) {
console.error(cause);
setIsLoading(null);
toast.error("Something went wrong, please try again.");
}
};

const authorizationURL = await initiateOAuthSignIn({provider, redirectUrlComplete});
window.location.assign(authorizationURL);

} catch (err) {
console.error(err);
setIsLoading(null);
toast.error((err as Error).message);
}
};

return (
<div className="flex flex-col gap-2">
<OAuthButton onClick={() => oauthSignIn("oauth_github")}>
{isLoading === "oauth_github" ? (
<Loading className="w-6 h-6" />
) : (
<GitHub className="w-6 h-6" />
)}
GitHub
</OAuthButton>
<OAuthButton onClick={() => oauthSignIn("oauth_google")}>
{isLoading === "oauth_google" ? (
<Loading className="w-6 h-6" />
) : (
<Google className="w-6 h-6" />
)}
Google
</OAuthButton>
</div>
);
return (
<div className="flex flex-col gap-2">
<OAuthButton onClick={() => oauthSignIn("github")}>
{isLoading === "github" ? (
<Loading className="w-6 h-6" />
) : (
<GitHub className="w-6 h-6" />
)}
GitHub
</OAuthButton>
<OAuthButton onClick={() => oauthSignIn("google")}>
{isLoading === "google" ? (
<Loading className="w-6 h-6" />
) : (
<Google className="w-6 h-6" />
)}
Google
</OAuthButton>
</div>
);
}
13 changes: 0 additions & 13 deletions apps/dashboard/app/auth/sso-callback/[[...sso-callback]]/page.tsx

This file was deleted.

20 changes: 20 additions & 0 deletions apps/dashboard/app/auth/sso-callback/[[...sso-callback]]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { NextRequest, NextResponse } from 'next/server';
import { auth } from '@/lib/auth/index'
import { cookies } from 'next/headers';

export async function GET(request: NextRequest) {
const authResult = await auth.completeOAuthSignIn(request);

// Get base URL from request because Next.js wants it
const baseUrl = new URL(request.url).origin;
console.log("base url", baseUrl)

const response = NextResponse.redirect(new URL(authResult.redirectTo, baseUrl));

// Set actual session cookies
for (const cookie of authResult.cookies) {
cookies().set(cookie.name, cookie.value, cookie.options);
}

return response;
}
53 changes: 15 additions & 38 deletions apps/dashboard/lib/auth/interface.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { StringLike } from "@visx/scale";
import { User } from "@workos-inc/node";
import { NextRequest, NextResponse } from "next/server";

export const UNKEY_SESSION_COOKIE = "unkey-session";

export type OAuthStrategy = "google" | "github";

export interface SignInViaOAuthOptions {
redirectUri?: string,
redirectUrl?: string,
redirectUrlComplete: string,
provider: OAuthStrategy
}

Expand All @@ -27,8 +28,8 @@ export interface AuthSession {
// Default middleware configuration
export const DEFAULT_MIDDLEWARE_CONFIG: MiddlewareConfig = {
enabled: true,
publicPaths: ['/auth/sign-in', '/auth/sign-up', '/favicon.ico'],
cookieName: 'auth_token',
publicPaths: ['/auth/sign-in', '/auth/sign-up', '/favicon.ico'], // TODO: allow glob matching
cookieName: UNKEY_SESSION_COOKIE,
loginPath: '/auth/sign-in'
};

Expand All @@ -50,7 +51,9 @@ export interface AuthProvider<T = any> {
// sign the user into a different workspace/organisation
signIn(orgId?: string): Promise<T>;

signInViaOAuth({}: SignInViaOAuthOptions): Response;
signInViaOAuth({}: SignInViaOAuthOptions): String;

completeOAuthSignIn(callbackRequest: Request): void;

signOut(): Promise<T>;

Expand All @@ -74,7 +77,9 @@ export abstract class BaseAuthProvider implements AuthProvider {

abstract signUpViaEmail(email: string): Promise<any>;

abstract signInViaOAuth({}: SignInViaOAuthOptions): Response;
abstract signInViaOAuth({}: SignInViaOAuthOptions): String;

abstract completeOAuthSignIn(callbackRequest: Request): void;

// sign the user into a different workspace/organisation
abstract signIn(orgId?: string): Promise<any>;
Expand Down Expand Up @@ -134,45 +139,17 @@ export abstract class BaseAuthProvider implements AuthProvider {
};
}

protected async handleMiddlewareRequest(
request: NextRequest,
config: MiddlewareConfig
): Promise<NextResponse> {
const { pathname } = request.nextUrl;

// Add more detailed logging
console.debug('Handle middleware request:', {
pathname,
publicPaths: config.publicPaths,
isPublicPath: this.isPublicPath(pathname, config.publicPaths)
});

if (this.isPublicPath(pathname, config.publicPaths)) {
console.debug('Public path detected, proceeding');
return NextResponse.next();
}

console.debug("Validating session");
const session = await this.validateSession(request, config);
if (!session) {
console.debug("No session found, redirecting to login");
return this.redirectToLogin(request, config);
}

return NextResponse.next();
}

protected isPublicPath(pathname: string, publicPaths: string[]): boolean {
const isPublic = publicPaths.some(path => pathname.startsWith(path));
console.debug('Checking public path:', { pathname, publicPaths, isPublic });
return isPublic;
}

protected async validateSession(request: NextRequest, config: MiddlewareConfig) {
const token = request.cookies.get(config.cookieName)?.value;
if (!token) return null;
const sessionData = request.cookies.get(config.cookieName)?.value;
if (!sessionData) return null;

return this.getSession(token);
return this.getSession(sessionData);
}

protected redirectToLogin(request: NextRequest, config: MiddlewareConfig): NextResponse {
Expand Down
Loading

0 comments on commit 7f0ed5c

Please sign in to comment.