Skip to content

Commit

Permalink
Merge pull request #45 from Derick80/code-29-integrate-otp-flow-ii
Browse files Browse the repository at this point in the history
Code 29 integrate otp flow ii
  • Loading branch information
Derick80 authored Jan 10, 2024
2 parents e38ceea + dbf2709 commit fa064f5
Show file tree
Hide file tree
Showing 11 changed files with 239 additions and 15 deletions.
6 changes: 3 additions & 3 deletions app/components/auth/auth-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,8 @@ const actionMap: Record<
'You can login using your social media account and there is no need to register a new email and password.'
},
OTP: {
url: '/login-otp',
button: 'Confirm OTP',
url: '/totp',
button: 'Email me a OTP',
emailCaption:
'Enter your email address and we will send you a link to reset your password.',
socialsCaption:
Expand Down Expand Up @@ -244,7 +244,7 @@ export const AuthForm = () => {
onClick={ () => setMode(mode === 'OTP' ? 'login' : 'OTP') }>
<Muted
className='text-base'>
{ mode === 'OTP' ? 'Login using password' : 'Send OTP code' }
{ mode === 'OTP' ? 'Login using password' : 'Login or Register using OTP code' }
</Muted>
</Button>

Expand Down
3 changes: 2 additions & 1 deletion app/routes/_auth.$provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ export async function action ({ request, params }: ActionFunctionArgs) {

})

const isCode = provider === 'totp'

if (!provider) return redirect('/login')

return authenticator.authenticate(provider, request, {
successRedirect: '/',
successRedirect: isCode ? '/verify' : '/',
failureRedirect: '/login'
})
}
7 changes: 5 additions & 2 deletions app/routes/_auth.login.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import type { ActionFunctionArgs } from '@remix-run/node'
import { json } from '@remix-run/node'
import { useActionData } from '@remix-run/react'
import { Form, useActionData } from '@remix-run/react'
import { AuthForm } from '~/components/auth/auth-form'

import { authenticator } from '~/server/auth/auth.server'

import { validateAction } from '~/utilities'
import { z } from 'zod'
import { Label } from '~/components/ui/label'
import { Input } from '~/components/ui/input'
import { Button } from '~/components/ui/button'


export const AuthSchema = z.discriminatedUnion('intent', [
Expand Down Expand Up @@ -72,4 +75,4 @@ export default function Login () {
<AuthForm />
</div>
)
}
}
11 changes: 11 additions & 0 deletions app/routes/_auth.magic-link.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// app/routes/magic-link.tsx

import { LoaderFunctionArgs } from '@remix-run/node';
import { authenticator } from '~/server/auth/auth.server';

export async function loader ({ request }: LoaderFunctionArgs) {
await authenticator.authenticate('TOTP', request, {
successRedirect: '/account',
failureRedirect: '/login',
})
}
111 changes: 111 additions & 0 deletions app/routes/_auth.verify.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node'

import { Form, useLoaderData } from '@remix-run/react'
import { json, redirect } from '@remix-run/node'
import { authenticator } from '~/server/auth/auth.server'
import { commitSession, getSession } from '~/server/session.server'






export async function loader ({ request }: LoaderFunctionArgs) {
await authenticator.isAuthenticated(request, {
successRedirect: '/',
})

const session = await getSession(request.headers.get('Cookie'))
const authEmail = session.get('auth:email')
const authError = session.get(authenticator.sessionErrorKey)

if (!authEmail) return redirect('/login')

// Commit session to clear any `flash` error message.
return json({ authEmail, authError } as const, {
headers: {
'set-cookie': await commitSession(session),
},
})
}

export async function action ({ request }: ActionFunctionArgs) {
const url = new URL(request.url)
const currentPath = url.pathname

await authenticator.authenticate('totp', request, {
successRedirect: currentPath,
failureRedirect: currentPath,
})
}

export default function Verify () {
const { authEmail, authError } = useLoaderData<typeof loader>()

return (
<div className='grid gap-5 items-center max-w-lg mx-auto'>
{/* Navigation */ }

{/* Content */ }
<div className="mx-auto flex h-full w-full max-w-[350px] flex-col items-center justify-center gap-6">
{/* Code Verification Form */ }
<div className="flex w-full flex-col items-center gap-6">
<div className="flex w-full flex-col items-center justify-center gap-2">
<div className="flex flex-col items-center gap-1">
<h1 className="text-2xl font-semibold tracking-tight">
Please check your inbox
</h1>
<p className="text-center text-base font-normal text-gray-600">
We've sent you a magic link email.
</p>
</div>
</div>

<div className="flex w-full flex-col items-center justify-center gap-2">
<Form method="POST" autoComplete="off" className="flex w-full flex-col gap-2">
<div className="flex flex-col">
<label htmlFor="code" className="sr-only">
Code
</label>
<input
type="text"
name="code"
placeholder="Enter code..."
className="h-11 rounded-md border-2 border-gray-200 bg-transparent px-4 text-base font-semibold placeholder:font-normal placeholder:text-gray-400"
required
/>
</div>
<button
type="submit"
className="clickable flex h-10 items-center justify-center rounded-md bg-gray-800">
<span className="text-sm font-semibold text-white">Continue</span>
</button>
</Form>

{/* Request New Code. */ }
{/* Email is already in session, so no input it's required. */ }
<Form method="POST" autoComplete="off" className="flex w-full flex-col gap-2">
<button
type="submit"
className="clickable flex h-10 items-center justify-center rounded-md bg-gray-200">
<span className="text-sm font-semibold text-black">Request New Code</span>
</button>
</Form>
</div>
</div>

{/* Errors Handling. */ }
{ authEmail && authError && (
<span className="font-semibold text-red-400">{ authError?.message }</span>
) }

<p className="text-center text-xs leading-relaxed text-gray-400">
By continuing, you agree to our{ ' ' }
<span className="clickable underline">Terms of Service</span>
</p>
</div>

{/* Footer */ }
</div>
)
}
8 changes: 4 additions & 4 deletions app/routes/blog.$postId.favorite.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { ActionFunctionArgs, LoaderFunctionArgs } from '@remix-run/node'
import { Response, json } from '@remix-run/node'
import { json } from '@remix-run/node'
import { z } from 'zod'
import { zx } from 'zodix'
import { isAuthenticated } from '~/server/auth/auth.server'
Expand All @@ -13,11 +13,11 @@ import {

// or cloudflare/deno

export async function loader({ request, params }: LoaderFunctionArgs) {
throw new Response("This page doesn't exists.", { status: 404 })
export async function loader ({ request, params }: LoaderFunctionArgs) {
throw new Error("This page doesn't exists.")
}

export async function action({ request, params }: ActionFunctionArgs) {
export async function action ({ request, params }: ActionFunctionArgs) {
const session = await getSession(request.headers.get('Cookie'))

const user = await isAuthenticated(request)
Expand Down
6 changes: 4 additions & 2 deletions app/server/auth/auth.server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,16 @@ import { loginStrategy, registerStrategy } from './strategy/form.server'
import type { Session } from '@remix-run/node'
import { discordStrategy } from './strategy/discord.server'
import { getUser } from '../user.server'

import { totpStrategy } from './strategy/totp-server'
export const authenticator = new Authenticator<User['id']>(sessionStorage, {
throwOnError: true
throwOnError: true,
sessionErrorKey: 'authError'
})

authenticator.use(registerStrategy, 'register')
authenticator.use(loginStrategy, 'login')
authenticator.use(discordStrategy, 'discord')
authenticator.use(totpStrategy, 'totp')
export const isAuthenticated = async (request: Request) => {
const userId = await authenticator.isAuthenticated(request)

Expand Down
53 changes: 53 additions & 0 deletions app/server/auth/strategy/totp-server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { TOTPStrategy } from 'remix-auth-totp'
import { prisma } from '~/server/prisma.server'
import { sendAuthEmail } from '../auth-email.server'

export const totpStrategy = new TOTPStrategy(
{
secret: process.env.ENCRYPTION_SECRET || 'some-secret-key',
magicLinkGeneration: { callbackPath: '/magic-link' },

createTOTP: async (data, expiresAt) => {
await prisma.totp.create({ data: { ...data, expiresAt } })

try {
// Delete expired TOTP records (Optional).
await prisma.totp.deleteMany({
where: { expiresAt: { lt: new Date() } }
})
} catch (error) {
console.warn('Error deleting expired TOTP records', error)
}
},
readTOTP: async (hash) => {
// Get the TOTP data from the database.
return await prisma.totp.findUnique({ where: { hash } })
},
updateTOTP: async (hash, data) => {
// Update the TOTP data in the database.
await prisma.totp.update({ where: { hash }, data })
},
sendTOTP: async ({ email, code, magicLink, request }) => {
await sendAuthEmail({ email, code, magicLink })
}
},
async ({ email, form, magicLink, code }) => {
if (form) console.log('form', form)
if (magicLink) console.log('magicLink', magicLink)
if (code) console.log('code', code)

let user = await prisma.user.findUnique({ where: { email } })

if (!user) {
user = await prisma.user.create({
data: {
email,
username: email.split('@')[0]
}
})
if (!user) throw new Error('Whoops! Unable to create user.')
}

return user.id
}
)
41 changes: 41 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"private": true,
"sideEffects": false,
"type":"module",
"scripts": {
"build": "remix build",
"deploy": "fly deploy --remote-only",
Expand Down Expand Up @@ -82,6 +83,7 @@
"remix-auth": "^3.6.0",
"remix-auth-discord": "1.3.2",
"remix-auth-form": "1.4.0",
"remix-auth-totp": "^2.0.0",
"remix-utils": "^7.5.0",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
Expand Down Expand Up @@ -109,6 +111,6 @@
"typescript": "^5.3.3"
},
"engines": {
"node": ">=14"
"node": ">=18.0.0"
}
}
4 changes: 2 additions & 2 deletions remix.config.js → remix.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
module.exports = {
ignoredRouteFiles: ["**/.*"],
tailwind: true,
serverModuleFormat: "cjs",
serverDependenciesToBundle: [/^remix-utils.*/],
serverModuleFormat: "esm",
serverDependenciesToBundle: [/^remix-utils.*/,/^remix-auth-totp.*/],
// appDirectory: "app",
// assetsBuildDirectory: "public/build",
// serverBuildPath: "build/index.js",
Expand Down

0 comments on commit fa064f5

Please sign in to comment.