Created
November 22, 2023 23:19
-
-
Save cayblood/6d631327d08f21c8faef755467fae310 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// app/services/auth.server.ts | |
import { parse } from "cookie"; | |
import { verify } from "jsonwebtoken"; | |
import { findOrCreateUser } from "~/models/user.server"; | |
import { createUserSession, logout } from "~/services/session.server"; | |
import type { HankoAuthInfo } from "~/routes/login"; | |
import { JwksClient } from "jwks-rsa"; | |
export const extractHankoCookie = (request: Request) => { | |
const cookies = parse(request.headers.get("Cookie") || ""); | |
return cookies.hanko; | |
}; | |
export async function requireValidJwt(request: Request) { | |
const hankoBackend = `${process.env.HANKO_BACKEND_URL}/.well-known/jwks.json`; | |
const token = extractHankoCookie(request); | |
const client = new JwksClient({ | |
cache: true, | |
rateLimit: true, | |
jwksRequestsPerMinute: 5, | |
jwksUri: hankoBackend, | |
}); | |
const key = await client.getSigningKey(); | |
const publicKey = key.getPublicKey(); | |
try { | |
return verify(token, publicKey, { complete: true }); | |
} catch (err) { | |
throw await logout(request); | |
} | |
} | |
export async function loginUserFromSuccessfulHankoAuth( | |
request: Request, | |
authInfo: HankoAuthInfo, | |
redirectTo: string | |
) { | |
const user = await findOrCreateUser(authInfo.hankoId, authInfo.email); | |
return createUserSession({ | |
request, | |
userId: user.id, | |
remember: false, | |
redirectTo, | |
}); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// app/utils/hanko.client.ts | |
import { register } from "@teamhanko/hanko-elements"; | |
import type { RegisterResult } from "@teamhanko/hanko-elements/dist/Elements"; | |
import { Hanko } from "@teamhanko/hanko-frontend-sdk"; | |
export { register, Hanko, RegisterResult }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// app/routes/login.tsx | |
import { | |
type ReactElement, | |
Suspense, | |
useCallback, | |
useEffect, | |
useState, | |
} from "react"; | |
import { | |
register, | |
type Hanko, | |
type RegisterResult, | |
} from "~/utils/hanko.client.ts"; | |
import type { ActionArgs } from "@remix-run/node"; | |
import { useFetcher } from "@remix-run/react"; | |
import { loginUserFromSuccessfulHankoAuth } from "~/services/auth.server"; | |
import { z } from "zod"; | |
import { useMatchesData } from "~/utils/utils"; | |
const hankoAuthInfo = z | |
.object({ | |
hankoId: z.string().uuid(), | |
email: z.string().email(), | |
}) | |
.required(); | |
export type HankoAuthInfo = z.infer<typeof hankoAuthInfo>; | |
export const action = async ({ request }: ActionArgs) => { | |
const formData = await request.formData(); | |
const authInfo: HankoAuthInfo = hankoAuthInfo.parse( | |
Object.fromEntries(formData), | |
); | |
const url = new URL(request.url); | |
const redirectTo = url.searchParams.get("redirectTo") || "/"; | |
return loginUserFromSuccessfulHankoAuth(request, authInfo, redirectTo); | |
}; | |
const userSchema = z.object({ | |
id: z.string().uuid(), | |
email: z.string().email(), | |
}); | |
const LoginForm = () => { | |
const [hanko, setHanko] = useState<Hanko>(); | |
const fetcher = useFetcher(); | |
const data = useMatchesData("root"); | |
const schema = z.object({ | |
ENV: z.object({ | |
HANKO_URL: z.string().url(), | |
}), | |
}); | |
const hankoUrl = schema.parse(data).ENV.HANKO_URL; | |
const redirectAfterLogin = useCallback( | |
async (hanko: Hanko) => { | |
// successfully logged in, redirect to a page in your application | |
if (hanko) { | |
const user = userSchema.parse(await hanko.user.getCurrent()); | |
const d = { hankoId: user.id, email: user.email }; | |
fetcher.submit(d, { method: "post" }); | |
} | |
}, | |
[fetcher], | |
); | |
useEffect(() => { | |
if (hanko) { | |
hanko.onAuthFlowCompleted(() => { | |
redirectAfterLogin(hanko); | |
}); | |
} | |
}, [hanko, redirectAfterLogin]); | |
useEffect(() => { | |
register(hankoUrl) | |
.catch((error: Error) => { | |
console.error(error.message); | |
}) | |
.then((result: RegisterResult | void) => { | |
if (result) { | |
setHanko(result.hanko); | |
} | |
}); | |
}, [hankoUrl]); | |
return ( | |
<div className=""> | |
<Suspense fallback={"Loading..."}> | |
<hanko-auth /> | |
</Suspense> | |
</div> | |
); | |
}; | |
export default function Login() { | |
return ( | |
<div className="fixed left-0 right-0 top-0 z-10 h-full"> | |
<div className="flex min-h-full flex-col justify-center p-4"> | |
<div className="w-full self-center rounded-lg bg-stone-100/80 py-8 backdrop-blur-sm sm:w-3/4 md:w-1/2 lg:w-1/3"> | |
<LoginForm /> | |
</div> | |
</div> | |
</div> | |
); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment