Skip to content
This repository has been archived by the owner on Sep 22, 2024. It is now read-only.

Commit

Permalink
wip(plugins/auth): refactor auth module to use database adapters
Browse files Browse the repository at this point in the history
  • Loading branch information
miguelrk committed May 20, 2024
1 parent 65ff8cb commit 0112aed
Show file tree
Hide file tree
Showing 21 changed files with 292 additions and 241 deletions.
2 changes: 1 addition & 1 deletion lib/components/dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ export {
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger
DialogTrigger,
};

////////////////////////////////////////////////////////////////////////////////
Expand Down
1 change: 0 additions & 1 deletion lib/components/head.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ export type HeadProps = {
export const Head = (props: HeadProps) => {
return (
<_Head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<Meta
title={props.title}
Expand Down
1 change: 1 addition & 0 deletions lib/deps/@unocss/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "npm:@unocss/[email protected]";
27 changes: 16 additions & 11 deletions lib/plugins/auth/islands/auth-form.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ComponentChildren, JSX } from "preact";
import type { JSX } from "preact";
import {
Alert,
AlertDescription,
Expand Down Expand Up @@ -27,6 +27,11 @@ export function AuthForm(props: AuthFormProps) {
);
const showDivider = hasEmail && hasOAuth2;

const text = ({
en: "Sign In with",
es: "Inicia sesión con",
})[props.config.locale ?? "es"];

return (
<>
<div className="flex flex-col space-y-2 text-center">
Expand Down Expand Up @@ -74,13 +79,13 @@ export function AuthForm(props: AuthFormProps) {
)}

{!!providers?.netzo && (
<ButtonNetzo text="Sign In with Netzo" href="/auth/netzo/signin">
<ButtonNetzo text={`${text} Netzo`} href="/auth/netzo/signin">
<div className="mr-4 w-22px h-22px i-netzo-symbol" />
</ButtonNetzo>
)}

{!!providers?.google && (
<ButtonOAuth2 text="Sign In with Google" href="/auth/google/signin">
<ButtonOAuth2 text={`${text} Google`} href="/auth/google/signin">
{/* NOTE: use inline SVG instead of logos-google-icon to avoid having to load logos collection (7MB) */}
<svg
className="mr-4 w-20px h-20px"
Expand Down Expand Up @@ -108,19 +113,19 @@ export function AuthForm(props: AuthFormProps) {
)}

{!!providers?.github && (
<ButtonOAuth2 text="Sign In with GitHub" href="/auth/github/signin">
<ButtonOAuth2 text={`${text} GitHub`} href="/auth/github/signin">
<div className="mr-4 w-20px h-20px mdi-github" />
</ButtonOAuth2>
)}

{!!providers?.gitlab && (
<ButtonOAuth2 text="Sign In with GitLab" href="/auth/gitlab/signin">
<ButtonOAuth2 text={`${text} GitLab`} href="/auth/gitlab/signin">
<div className="mr-4 w-20px h-20px mdi-gitlab" />
</ButtonOAuth2>
)}

{!!providers?.slack && (
<ButtonOAuth2 text="Sign In with Slack" href="/auth/slack/signin">
<ButtonOAuth2 text={`${text} Slack`} href="/auth/slack/signin">
{/* NOTE: use inline SVG instead of logos-slack-icon to avoid having to load logos collection (7MB) */}
<svg
className="mr-4 w-20px h-20px"
Expand Down Expand Up @@ -148,7 +153,7 @@ export function AuthForm(props: AuthFormProps) {
)}

{!!providers?.auth0 && (
<ButtonOAuth2 text="Sign In with Auth0" href="/auth/auth0/signin">
<ButtonOAuth2 text={`${text} Auth0`} href="/auth/auth0/signin">
{/* NOTE: use inline SVG instead of simple-icons-auth0 to avoid having to load simple-icons collection (4.4MB) */}
<svg
className="mr-4 w-20px h-20px"
Expand All @@ -164,7 +169,7 @@ export function AuthForm(props: AuthFormProps) {
)}

{!!providers?.okta && (
<ButtonOAuth2 text="Sign In with Okta" href="/auth/okta/signin">
<ButtonOAuth2 text={`${text} Okta`} href="/auth/okta/signin">
{/* NOTE: use inline SVG instead of simple-icons-okta to avoid having to load simple-icons collection (4.4MB) */}
<svg
className="mr-4 w-20px h-20px"
Expand All @@ -181,7 +186,7 @@ export function AuthForm(props: AuthFormProps) {

{
/* {!!providers?.oauth2 && (
<ButtonOAuth2 text="Sign In with Custom" href="/auth/oauth2/signin">
<ButtonOAuth2 text={`${text} Custom`} href="/auth/oauth2/signin">
<div className="mr-4 w-20px h-20px mdi-code-json" />
</ButtonOAuth2>
)} */
Expand Down Expand Up @@ -218,7 +223,7 @@ function ButtonNetzo(
props: {
text: string;
href: string;
children: ComponentChildren;
children: React.ReactNode;
},
) {
return (
Expand All @@ -235,7 +240,7 @@ function ButtonOAuth2(
props: {
text: string;
href: string;
children: ComponentChildren;
children: React.ReactNode;
},
) {
return (
Expand Down
27 changes: 20 additions & 7 deletions lib/plugins/auth/middlewares/mod.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import type { FreshContext } from "$fresh/server.ts";
import { AuthState } from "netzo/plugins/auth/plugin.ts";
import { getSessionId } from "../../../deps/deno_kv_oauth/mod.ts";
import type { NetzoState } from "../../../mod.ts";
import { getUserBySession } from "../utils/db.ts";
import { createDatastoreAuth } from "../utils/adapters/datastore.ts";

const skip = (_req: Request, ctx: FreshContext<NetzoState>) => {
type NetzoStateWithAuth = NetzoState & {
auth: AuthState;
};

const skip = (_req: Request, ctx: FreshContext<NetzoStateWithAuth>) => {
if (!["route"].includes(ctx.destination)) return true;
if (ctx.url.pathname.startsWith("/auth/")) return true; // skip auth routes (signin, callback, signout)
if (ctx.url.pathname.startsWith("/database")) return true; // skip database routes
Expand All @@ -14,9 +19,17 @@ const skip = (_req: Request, ctx: FreshContext<NetzoState>) => {
return false;
};

export async function setAuthState(
_req: Request,
ctx: FreshContext<NetzoStateWithAuth>,
) {
ctx.state.auth ??= createDatastoreAuth();
return await ctx.next();
}

export async function setSessionState(
req: Request,
ctx: FreshContext<NetzoState>,
ctx: FreshContext<NetzoStateWithAuth>,
) {
if (skip(req, ctx)) return await ctx.next();

Expand All @@ -30,7 +43,7 @@ export async function setSessionState(

if (sessionId === undefined) return await ctx.next(); // A) not authenticated

const user = await getUserBySession(sessionId);
const user = await ctx.state.auth.getUserBySession(sessionId);
if (!user) return await ctx.next(); // B) user not found

// set authenticated state (sessionId could be set but expired,
Expand All @@ -43,7 +56,7 @@ export async function setSessionState(

export async function assertUserIsWorkspaceUserOfWorkspaceOfApiKeyIfProviderIsNetzo(
req: Request,
ctx: FreshContext<NetzoState>,
ctx: FreshContext<NetzoStateWithAuth>,
) {
if (skip(req, ctx)) return await ctx.next();

Expand Down Expand Up @@ -79,7 +92,7 @@ export async function assertUserIsWorkspaceUserOfWorkspaceOfApiKeyIfProviderIsNe

export async function setRequestState(
req: Request,
ctx: FreshContext<NetzoState>,
ctx: FreshContext<NetzoStateWithAuth>,
) {
if (skip(req, ctx)) return await ctx.next();

Expand All @@ -99,7 +112,7 @@ export async function setRequestState(

export async function ensureSignedIn(
req: Request,
ctx: FreshContext<NetzoState>,
ctx: FreshContext<NetzoStateWithAuth>,
) {
if (skip(req, ctx)) return await ctx.next();

Expand Down
13 changes: 10 additions & 3 deletions lib/plugins/auth/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import type { NetzoState } from "../../mod.ts";
import {
assertUserIsWorkspaceUserOfWorkspaceOfApiKeyIfProviderIsNetzo,
ensureSignedIn,
setAuthState,
setRequestState,
setSessionState,
} from "./middlewares/mod.ts";
import createAuth from "./routes/auth.tsx";
import { getRoutesByProvider } from "./routes/mod.ts";
import type { AuthUser } from "./utils/db.ts";
import type { EmailClientConfig } from "./utils/providers/email.ts";
import type { AuthProvider } from "./utils/providers/mod.ts";
import type { NetzoClientConfig } from "./utils/providers/netzo.ts";
import type { Auth, AuthProvider, AuthUser } from "./utils/types.ts";

export * from "../../deps/deno_kv_oauth/mod.ts";

Expand All @@ -23,6 +23,9 @@ export type AuthConfig = {
description?: string;
/** HTML content rendered below auth form e.g. to display a link to the terms of service via an a tag. */
caption?: string;
/** An image URL to display to the right side of the login form at /auth. */
image?: React.ImgHTMLAttributes<HTMLImageElement>;
locale?: "en" | "es";
providers: {
netzo?: NetzoClientConfig;
email?: EmailClientConfig;
Expand All @@ -35,7 +38,7 @@ export type AuthConfig = {
};
};

export type AuthState = {
export type AuthState = Auth & {
/* Session ID used internally by the auth plugin. */
sessionId?: string;
/* The user object associated with the session ID. */
Expand Down Expand Up @@ -92,6 +95,10 @@ export const auth = (config: AuthConfig): Plugin<NetzoState> => {
return {
name: "netzo.auth",
middlewares: [
{
path: "/",
middleware: { handler: setAuthState },
},
{
path: "/",
middleware: { handler: setSessionState },
Expand Down
31 changes: 29 additions & 2 deletions lib/plugins/auth/routes/auth.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { defineRoute, type RouteConfig } from "$fresh/server.ts";
import { cn } from "../../../components/utils.ts";
import { AuthForm } from "../islands/auth-form.tsx";
import { type AuthConfig } from "../plugin.ts";

Expand All @@ -13,11 +14,37 @@ export const config: RouteConfig = {

export default (config: AuthConfig) => {
return defineRoute((req, ctx) => {
const authFormWrapper = "grid gap-6 w-full xs:w-[350px] max-w-[350px]";
if (config.image) {
return (
<div className="w-full h-full lg:grid lg:min-h-[600px] lg:grid-cols-2 xl:min-h-[800px]">
<div className="h-full flex items-center justify-center py-12">
<div className={authFormWrapper}>
<AuthForm config={config} request={req} />
</div>
</div>
<div className="hidden bg-muted lg:block">
<img
src={config.image.src}
alt="Netzo Authentication"
width="1920"
height="1080"
className={cn(
"w-full h-full object-cover",
config.image?.className!,
)}
{...config.image}
/>
</div>
</div>
);
}

return (
<div
className={`h-full w-full grid place-items-center p-4 bg-background`}
className={`w-full h-full grid place-items-center p-4 bg-background`}
>
<div className="grid gap-6 w-full xs:w-[350px] max-w-[350px]">
<div className={authFormWrapper}>
<AuthForm config={config} request={req} />

{config?.caption && (
Expand Down
29 changes: 12 additions & 17 deletions lib/plugins/auth/routes/mod.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,11 @@
import type { PluginRoute } from "$fresh/server.ts";
import type { AuthConfig } from "../plugin.ts";
import {
type AuthUser,
createUser,
getUser,
updateUser,
updateUserSession,
} from "../utils/db.ts";
import {
type AuthProvider,
getAuthConfig,
getFunctionsByProvider,
getUserByProvider,
} from "../utils/providers/mod.ts";
import type { AuthProvider, AuthUser } from "../utils/types.ts";

export const getRoutesByProvider = (
provider: AuthProvider,
Expand All @@ -32,18 +25,19 @@ export const getRoutesByProvider = (
},
{
path: `/auth/${provider}/callback`,
handler: async (req, _ctx) => {
handler: async (req, ctx) => {
const authConfig = getAuthConfig(provider, providerOptions);
const { response, tokens, sessionId } = await handleCallback(
req,
authConfig,
);
const {
response,
tokens,
sessionId,
} = await handleCallback(req, authConfig);

const userProvider = await getUserByProvider(
provider,
tokens.accessToken,
);
const userCurrent = await getUser(userProvider.authId);
const userCurrent = await ctx.state.auth.getUser(userProvider.authId);

const user = {
sessionId,
Expand All @@ -56,10 +50,11 @@ export const getRoutesByProvider = (
} as unknown as AuthUser;

if (userCurrent === null) {
await createUser(user);
await ctx.state.auth.createUser(user);
} else {
await updateUser({ ...user, ...userCurrent });
await updateUserSession({ ...user, ...userCurrent }, sessionId);
const data = { ...user, ...userCurrent };
await ctx.state.auth.updateUser(data);
await ctx.state.auth.updateUserSession(data, sessionId);
}

return response;
Expand Down
Loading

0 comments on commit 0112aed

Please sign in to comment.