Skip to content

Commit

Permalink
Merge pull request #32 from NIAEFEUP/feature/login
Browse files Browse the repository at this point in the history
feature: login
  • Loading branch information
limwa authored Jan 24, 2025
2 parents 4ffb62b + 2e8352a commit f55f908
Show file tree
Hide file tree
Showing 82 changed files with 5,362 additions and 3,492 deletions.
13 changes: 10 additions & 3 deletions deploy/website/entrypoint.sh
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#!/bin/sh
#!/bin/bash

set -e
set -o pipefail

# Script configuration

Expand Down Expand Up @@ -28,9 +29,13 @@ run_command() {
echo
print "# $command"

trap "print_file $_LOG_FILE" EXIT

# Strip any ANSI escape sequences from the output
eval "$command" 2>&1 | sed "s/\x1B\[[0-9;]*[a-zA-Z]//g" > "$_LOG_FILE"

trap - EXIT

print_file "$_LOG_FILE"
echo
}
Expand Down Expand Up @@ -120,11 +125,13 @@ print ">> Application preparation"
push_indent " "

if [ "$ON_STARTUP_MIGRATE" = "true" ]; then
print ">> MIGRATE_ON_STARTUP is enabled"
mode="${ON_STARTUP_MIGRATE_MODE:-run}"

print ">> ON_STARTUP_MIGRATE is enabled [mode: $mode]"
push_indent " "

print "Migrating database..."
run_command node ace migration:run --force
run_command node ace migration:$mode --force

pop_indent
fi
Expand Down
22 changes: 22 additions & 0 deletions website/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,32 @@ SESSION_DRIVER=cookie

# E-mail
FROM_EMAIL=[email protected]
REPLY_TO_EMAIL=[email protected]
SMTP_HOST=localhost
SMTP_PORT=1025

# Rate limiting
LIMITER_STORE=memory

# Redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379
REDIS_PASSWORD=

# Ally
GITHUB_CLIENT_ID=********
GITHUB_CLIENT_SECRET=********
GOOGLE_CLIENT_ID=********
GOOGLE_CLIENT_SECRET=********
LINKEDIN_CLIENT_ID=********
LINKEDIN_CLIENT_SECRET=********

# Feature flags
FEATURES_DISABLE_AUTH=false

# Frontend
INERTIA_PUBLIC_TZ=Europe/Lisbon
INERTIA_PUBLIC_EVENT_COUNTDOWN_DATE=2025-04-11

# Tuyau
INERTIA_PUBLIC_APP_URL=http://127.0.0.1:3333
5 changes: 4 additions & 1 deletion website/adonisrc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ export default defineConfig({
() => import('@adonisjs/inertia/inertia_provider'),
() => import('@adonisjs/mail/mail_provider'),
() => import('@tuyau/core/tuyau_provider'),
() => import('@adonisjs/ally/ally_provider'),
() => import('@adonisjs/limiter/limiter_provider'),
() => import('@adonisjs/redis/redis_provider')
],

/*
Expand All @@ -55,7 +58,7 @@ export default defineConfig({
| List of modules to import before starting the application.
|
*/
preloads: [() => import('#start/routes'), () => import('#start/kernel')],
preloads: [() => import('#start/routes'), () => import('#start/kernel'), () => import('#start/events'), () => import('#start/validator')],

/*
|--------------------------------------------------------------------------
Expand Down
117 changes: 117 additions & 0 deletions website/app/controllers/authentication_controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import Account from '#models/account'
import type { HttpContext } from '@adonisjs/core/http'
import {
registerWithCredentialsValidator,
emailVerificationCallbackValidator,
loginWithCredentialsValidator,
} from '#validators/authentication'
import { UserService } from '#services/user_service'
import { inject } from '@adonisjs/core'
import UserCreated from '#events/user_created'
import SendVerificationEmail from '#listeners/send_verification_email'
import { errors } from '@adonisjs/auth'

@inject()
export default class AuthenticationController {
constructor(private userService: UserService) {}

async login({ request, auth, session, response }: HttpContext) {
const { email, password } = await request.validateUsing(loginWithCredentialsValidator)

try {
const account = await Account.verifyCredentials(`credentials:${email}`, password)

await account.load('user')
await auth.use('web').login(account.user)

if (!account.user.isEmailVerified())
return response.redirect().toRoute('pages:auth.verify')

return response.redirect().toRoute('pages:home')

} catch (error) {
if (error instanceof errors.E_INVALID_CREDENTIALS) {
session.flashErrors({ password: 'As credenciais que introduziste não são válidas' })
return response.redirect().back()
}

throw error
}
}

async logout({ auth, response }: HttpContext) {
await auth.use('web').logout()
return response.redirect().toRoute('pages:home')
}

async register({ request, auth, response }: HttpContext) {
const { email, password } = await request.validateUsing(registerWithCredentialsValidator)

const user = await this.userService.createUserWithCredentials(email, password)
await auth.use('web').login(user)

return response.redirect().toRoute('pages:auth.verify')
}

async retryEmailVerification({ auth, response }: HttpContext) {
const user = auth.getUserOrFail()

const listener = new SendVerificationEmail()
listener.handle(new UserCreated(user))

return response.redirect().toRoute('pages:auth.verify')
}

async callbackForEmailVerification({ request, response }: HttpContext) {
const { email } = await request.validateUsing(emailVerificationCallbackValidator)
await this.userService.verifyEmail(email)

return response.redirect().toRoute('actions:auth.verify.success')
}

// SOCIAL AUTHENTICATION

// async initiateGithubLogin({ ally, inertia }: HttpContext) {
// const url = await ally.use('github').redirectUrl()
// return inertia.location(url)
// }

// async callbackForGithubLogin({ ally }: HttpContext) {
// const github = ally.use('github')
// const user = await github.user()

// const data = await socialAccountLoginValidator.validate(user)
// console.log(data)

// const account = await getOrCreate({
// provider: 'github',
// providerId: data.id,
// })

// return response.json({ user, account: account.serialize() })
// }

// 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 })
// }
}
4 changes: 2 additions & 2 deletions website/app/env.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import vine from '@vinejs/vine'
import { ConstructableSchema, SchemaTypes } from '@vinejs/vine/types'
import type { ConstructableSchema, SchemaTypes } from '@vinejs/vine/types'
import { EnvProcessor, Env as AdonisEnv } from '@adonisjs/core/env'

type Primitives = string | number | boolean
type Primitives = string | number | boolean | null | undefined

function createObjectInterceptor() {
const keys = new Set<string>()
Expand Down
8 changes: 8 additions & 0 deletions website/app/events/user_created.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import User from '#models/user'
import { BaseEvent } from '@adonisjs/core/events'

export default class UserCreated extends BaseEvent {
constructor(public readonly user: User) {
super()
}
}
8 changes: 8 additions & 0 deletions website/app/events/user_email_verified.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import type User from '#models/user'
import { BaseEvent } from '@adonisjs/core/events'

export default class UserEmailVerified extends BaseEvent {
constructor(public readonly user: User) {
super()
}
}
8 changes: 8 additions & 0 deletions website/app/exceptions/authentication_disabled_exception.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Exception } from '@adonisjs/core/exceptions'

export default class AuthenticationDisabledException extends Exception {
static status = 403
static code = "E_AUTH_DISABLED"
static message = "Authentication is disabled"
static help = "Did you forget to enable authentication in your .env file?"
}
10 changes: 8 additions & 2 deletions website/app/exceptions/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,19 @@ export default class HttpExceptionHandler extends ExceptionHandler {
*/
protected renderStatusPages = app.inProduction

protected ignoreCodes = ["E_AUTH_DISABLED"]

/**
* Status pages is a collection of error code range and a callback
* to return the HTML contents to send as a response.
*/
protected statusPages: Record<StatusPageRange, StatusPageRenderer> = {
'404': (error, { inertia }) => inertia.render('errors/not_found', { error }),
'500..599': (error, { inertia }) => inertia.render('errors/server_error', { error }),
// '403': (error, { inertia }) => inertia.render('errors/forbidden', { error }),
'403': (_error, { response }) => response.status(403).finish(),
// '404': (error, { inertia }) => inertia.render('errors/not_found', { error }),
'404': (_error, { response }) => response.status(404).finish(),
// '500..599': (error, { inertia }) => inertia.render('errors/server_error', { error }),
'500..599': (_error, { response }) => response.status(500).finish(),
}

/**
Expand Down
23 changes: 23 additions & 0 deletions website/app/listeners/send_verification_email.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import UserCreated from "#events/user_created";
import EmailVerificationNotification from "#mails/email_verification_notification";
import mail from "@adonisjs/mail/services/main";
import { buildUrl, staticUrl } from "../url.js";

export default class SendVerificationEmail {
async handle(event: UserCreated) {
// Don't send the verification e-mail if the user has already verified it
if (event.user.emailVerifiedAt) return;

const email = event.user.email;
const notification = new EmailVerificationNotification({
email,
logoUrl: staticUrl("/images/logo-white.png"),

verificationLink: buildUrl()
.qs({ email })
.makeSigned("actions:auth.verify.callback", { expiresIn: "1h" }),
});

await mail.send(notification);
}
}
18 changes: 18 additions & 0 deletions website/app/mails/base/react_notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { BaseMail } from '@adonisjs/mail'
import { render } from '@react-email/components'
import type { JSX } from 'react'

type JSXImport<T = {}> = () => Promise<{ default: (props: T) => JSX.Element }>

export abstract class ReactNotification extends BaseMail {
async jsx<T = {}>(importer: JSXImport<T>, props: NoInfer<T>) {
const component = await importer().then((mod) => mod.default)
const element = component(props)

this.message
.html(await render(element))
.text(await render(element, { plainText: true }))
}

abstract prepare(): void | Promise<void>;
}
14 changes: 14 additions & 0 deletions website/app/mails/email_verification_notification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ReactNotification } from './base/react_notification.js'
import type { EmailVerificationProps } from '#resources/emails/auth/email_verification'

export default class EmailVerificationNotification extends ReactNotification {
constructor(private props: EmailVerificationProps) {
super()
}

async prepare() {
this.message.to(this.props.email).subject('Confirma o teu e-mail!')

await this.jsx(() => import('#resources/emails/auth/email_verification'), this.props)
}
}
31 changes: 0 additions & 31 deletions website/app/mails/example_e_notification.ts

This file was deleted.

8 changes: 8 additions & 0 deletions website/app/messages.ts
Original file line number Diff line number Diff line change
@@ -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
2 changes: 1 addition & 1 deletion website/app/middleware/auth_middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export default class AuthMiddleware {
/**
* The URL to redirect to, when authentication fails
*/
redirectTo = '/login'
redirectTo = '/auth/login'

async handle(
ctx: HttpContext,
Expand Down
12 changes: 12 additions & 0 deletions website/app/middleware/automatic_submit_middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { HttpContext } from '@adonisjs/core/http'
import type { NextFn } from '@adonisjs/core/types/http'

export default class AutomaticSubmitMiddleware {
async handle({ request, response, view }: HttpContext, next: NextFn) {
const method = request.method()
if (method === "POST") return next()

// Clever hack by virk to render Edge.js templates in middlewares
return response.status(200).send(await view.render('automatic_submit'))
}
}
Loading

0 comments on commit f55f908

Please sign in to comment.