From 0112aed3f2d8c25e3ab1f784cb534afa735cb374 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Lima?= Date: Mon, 6 Jan 2025 00:30:24 +0000 Subject: [PATCH] feat: add login form and oauth verification --- website/.env.example | 12 +++--- .../controllers/authentication_controller.ts | 41 +++++++++++++++++++ website/app/messages.ts | 8 ++++ .../verify_social_callback_middleware.ts | 34 +++++++++++++++ website/config/ally.ts | 2 + website/config/inertia.ts | 2 +- website/inertia/app/app.tsx | 3 +- website/inertia/app/tuyau.ts | 3 +- website/inertia/hooks/use_error.ts | 29 +++++++++++++ website/inertia/hooks/use_toast.ts | 2 +- website/inertia/layouts/applayout.tsx | 13 +++--- website/inertia/pages/login.tsx | 38 ++++++++++++++--- website/start/kernel.ts | 1 + website/start/routes.ts | 36 ++++++++++++++++ 14 files changed, 202 insertions(+), 22 deletions(-) create mode 100644 website/app/controllers/authentication_controller.ts create mode 100644 website/app/messages.ts create mode 100644 website/app/middleware/verify_social_callback_middleware.ts create mode 100644 website/inertia/hooks/use_error.ts diff --git a/website/.env.example b/website/.env.example index c139712..211d477 100644 --- a/website/.env.example +++ b/website/.env.example @@ -13,9 +13,9 @@ SMTP_PORT=1025 VITE_TZ=Europe/Lisbon VITE_EVENT_COUNTDOWN_DATE=2025-04-11 -GITHUB_CLIENT_ID= -GITHUB_CLIENT_SECRET= -GOOGLE_CLIENT_ID= -GOOGLE_CLIENT_SECRET= -LINKEDIN_CLIENT_ID= -LINKEDIN_CLIENT_SECRET= +GITHUB_CLIENT_ID=******** +GITHUB_CLIENT_SECRET=******** +GOOGLE_CLIENT_ID=******** +GOOGLE_CLIENT_SECRET=******** +LINKEDIN_CLIENT_ID=******** +LINKEDIN_CLIENT_SECRET=******** diff --git a/website/app/controllers/authentication_controller.ts b/website/app/controllers/authentication_controller.ts new file mode 100644 index 0000000..5d67e9c --- /dev/null +++ b/website/app/controllers/authentication_controller.ts @@ -0,0 +1,41 @@ +import type { HttpContext } from '@adonisjs/core/http' + +export default class AuthenticationController { + async login() {} + + async initiateGithubLogin({ ally, inertia }: HttpContext) { + const url = await ally.use('github').redirectUrl() + return inertia.location(url) + } + + async callbackForGithubLogin({ response, ally }: HttpContext) { + const github = ally.use('github') + const user = await github.user() + + return response.json({ user }) + } + + async initiateGoogleLogin({ ally, inertia }: HttpContext) { + const url = await ally.use('google').redirectUrl() + return inertia.location(url) + } + + async callbackForGoogleLogin({ response, ally }: HttpContext) { + const google = ally.use('google') + const user = await google.user() + + return response.json({ user }) + } + + async initiateLinkedinLogin({ ally, inertia }: HttpContext) { + const url = await ally.use('linkedin').redirectUrl() + return inertia.location(url) + } + + async callbackForLinkedinLogin({ response, ally }: HttpContext) { + const linkedin = ally.use('linkedin') + const user = await linkedin.user() + + return response.json({ user }) + } +} diff --git a/website/app/messages.ts b/website/app/messages.ts new file mode 100644 index 0000000..620c63c --- /dev/null +++ b/website/app/messages.ts @@ -0,0 +1,8 @@ +export const messages = { + auth: { + oauth: { + accessDenied: 'O pedido de início de sessão foi rejeitado.', + stateMismatch: 'Ocorreu um erro ao iniciar sessão. Por favor, tenta novamente.', + }, + }, +} as const diff --git a/website/app/middleware/verify_social_callback_middleware.ts b/website/app/middleware/verify_social_callback_middleware.ts new file mode 100644 index 0000000..713b433 --- /dev/null +++ b/website/app/middleware/verify_social_callback_middleware.ts @@ -0,0 +1,34 @@ +import { SocialProviders } from '@adonisjs/ally/types' +import type { HttpContext } from '@adonisjs/core/http' +import type { NextFn } from '@adonisjs/core/types/http' +import { messages } from '../messages.js' + +export default class VerifySocialCallbackMiddleware { + async handle( + { response, session, ally }: HttpContext, + next: NextFn, + options: { provider: keyof SocialProviders } + ) { + const oauth = ally.use(options.provider) + + if (oauth.accessDenied()) { + session.flashErrors({ oauth: messages.auth.oauth.accessDenied }) + return response.redirect('/login') + } + + if (oauth.stateMisMatch()) { + session.flashErrors({ oauth: messages.auth.oauth.stateMismatch }) + return response.redirect('/login') + } + + const postRedirectError = oauth.getError() + if (postRedirectError) { + session.flashErrors({ oauth: postRedirectError }) + return response.redirect('/login') + } + + const output = await next() + + return output + } +} diff --git a/website/config/ally.ts b/website/config/ally.ts index d5cde6d..d49d03c 100644 --- a/website/config/ally.ts +++ b/website/config/ally.ts @@ -6,6 +6,8 @@ const allyConfig = defineConfig({ clientId: env.get('GITHUB_CLIENT_ID'), clientSecret: env.get('GITHUB_CLIENT_SECRET'), callbackUrl: '', + scopes: ['user:email'], + allowSignup: false, }), google: services.google({ clientId: env.get('GOOGLE_CLIENT_ID'), diff --git a/website/config/inertia.ts b/website/config/inertia.ts index 7b00083..d3ef649 100644 --- a/website/config/inertia.ts +++ b/website/config/inertia.ts @@ -11,7 +11,7 @@ const inertiaConfig = defineConfig({ * Data that should be shared with all rendered pages */ sharedData: { - errors: (ctx) => ctx.session?.flashMessages.get('errors'), + errors: (ctx) => ctx.session?.flashMessages.get('errorsBag'), }, /** diff --git a/website/inertia/app/app.tsx b/website/inertia/app/app.tsx index e58b226..734bac3 100644 --- a/website/inertia/app/app.tsx +++ b/website/inertia/app/app.tsx @@ -1,6 +1,7 @@ /// -/// +/// /// +/// import '../css/app.css' diff --git a/website/inertia/app/tuyau.ts b/website/inertia/app/tuyau.ts index a276e92..61be578 100644 --- a/website/inertia/app/tuyau.ts +++ b/website/inertia/app/tuyau.ts @@ -1,7 +1,8 @@ import { createTuyau } from '@tuyau/client' import { api } from '#.adonisjs/api' +console.log(import.meta.env.BASE_URL) export const tuyau = createTuyau({ api, - baseUrl: import.meta.env.BASE_URL || 'http://localhost:3333', + baseUrl: 'http://localhost:3333', }) diff --git a/website/inertia/hooks/use_error.ts b/website/inertia/hooks/use_error.ts new file mode 100644 index 0000000..ca031bc --- /dev/null +++ b/website/inertia/hooks/use_error.ts @@ -0,0 +1,29 @@ +import { usePage } from '@inertiajs/react' +import { useEffect, useMemo } from 'react' +import { Toast, toast } from './use_toast' + +export function useError(key: string) { + const page = usePage() + const error = useMemo(() => { + const props = page.props + if (props.errors && key in props.errors) { + return props.errors[key] + } + + return null + }, [page.props]) + + return error +} + +export function useErrorToast(key: string, toastCreator: (msg: string) => Toast) { + const error = useError(key) + const toastContent = useMemo(() => error && toastCreator(error), [error]) + + useEffect(() => { + if (toastContent) { + const t = toast(toastContent) + return () => t.dismiss() + } + }, [toastContent]) +} diff --git a/website/inertia/hooks/use_toast.ts b/website/inertia/hooks/use_toast.ts index 27f0b62..e504ca3 100644 --- a/website/inertia/hooks/use_toast.ts +++ b/website/inertia/hooks/use_toast.ts @@ -135,7 +135,7 @@ function dispatch(action: Action) { }) } -type Toast = Omit +export type Toast = Omit function toast({ ...props }: Toast) { const id = genId() diff --git a/website/inertia/layouts/applayout.tsx b/website/inertia/layouts/applayout.tsx index 62a82a2..4f65b89 100644 --- a/website/inertia/layouts/applayout.tsx +++ b/website/inertia/layouts/applayout.tsx @@ -1,11 +1,12 @@ -import { ReactElement } from 'react' +import React from 'react' import { Head } from '@inertiajs/react' import NavBar from '../components/navbar' +import { Toaster } from '~/components/ui/toaster' type Props = { title: string - children: ReactElement[] - className: string + children: React.ReactNode + className?: string } export default function AppLayout({ title, children, className }: Props) { @@ -22,10 +23,8 @@ export default function AppLayout({ title, children, className }: Props) {
-
- {children} - -
+
{children}
+ ) diff --git a/website/inertia/pages/login.tsx b/website/inertia/pages/login.tsx index 19e916a..da065ee 100644 --- a/website/inertia/pages/login.tsx +++ b/website/inertia/pages/login.tsx @@ -1,14 +1,20 @@ -import { Button } from '~/components/ui/button' +import { Link } from '@tuyau/inertia/react' +import { Button, buttonVariants } from '~/components/ui/button' import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '~/components/ui/card' import { Input } from '~/components/ui/input' import { Label } from '~/components/ui/label' import { Separator } from '~/components/ui/separator' +import { useError } from '~/hooks/use_error' +import { cn } from '~/lib/utils' export default function Login() { + const oauthError = useError('oauth') + return ( + //
- + Iniciar Sessão @@ -43,9 +49,29 @@ export default function Login() {

Ou

- +
+ + Iniciar Sessão com o Google + {/* */} + + + Iniciar Sessão com o Github + {/* */} + + + Iniciar Sessão com o LinkedIn + {/* */} + +
@@ -57,7 +83,9 @@ export default function Login() { + {oauthError &&

{oauthError}

}
+ //
) } diff --git a/website/start/kernel.ts b/website/start/kernel.ts index 17fb18e..df3d3e1 100644 --- a/website/start/kernel.ts +++ b/website/start/kernel.ts @@ -46,6 +46,7 @@ router.use([ * the routes or the routes group. */ export const middleware = router.named({ + verifySocialCallback: () => import('#middleware/verify_social_callback_middleware'), guest: () => import('#middleware/guest_middleware'), auth: () => import('#middleware/auth_middleware'), }) diff --git a/website/start/routes.ts b/website/start/routes.ts index de3a200..fcb7780 100644 --- a/website/start/routes.ts +++ b/website/start/routes.ts @@ -7,10 +7,46 @@ | */ import router from '@adonisjs/core/services/router' +import { middleware } from '#start/kernel' +const AuthenticationController = () => import('#controllers/authentication_controller') const TicketsController = () => import('#controllers/tickets_controller') router.on('/').renderInertia('home') router.on('/login').renderInertia('login') router.get('/tickets', [TicketsController, 'index']) router.on('/tickets/:id/checkout').renderInertia('payments').as('checkout') + +router + .group(() => { + // Github + router + .get('/github/initiate', [AuthenticationController, 'initiateGithubLogin']) + .as('auth.github.initiate') + + router + .get('/github/callback', [AuthenticationController, 'callbackForGithubLogin']) + .middleware(middleware.verifySocialCallback({ provider: 'github' })) + .as('auth.github.callback') + + // Google + router + .get('/google/initiate', [AuthenticationController, 'initiateGoogleLogin']) + .as('auth.google.initiate') + + router + .get('/google/callback', [AuthenticationController, 'callbackForGoogleLogin']) + .middleware(middleware.verifySocialCallback({ provider: 'google' })) + .as('auth.google.callback') + + // LinkedIn + router + .get('/linkedin/initiate', [AuthenticationController, 'initiateLinkedinLogin']) + .as('auth.linkedin.initiate') + + router + .get('/linkedin/callback', [AuthenticationController, 'callbackForLinkedinLogin']) + .middleware(middleware.verifySocialCallback({ provider: 'linkedin' })) + .as('auth.linkedin.callback') + }) + .prefix('/auth')