Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: login #32

Open
wants to merge 37 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
f59aeb4
feat: add login page
limwa Jan 5, 2025
864ed4a
style: add lint for frontend
limwa Jan 5, 2025
4537c4e
feat: initialize social authentication
limwa Jan 5, 2025
0112aed
feat: add login form and oauth verification
limwa Jan 6, 2025
55c1c9e
refactor: public url for tuyau
limwa Jan 8, 2025
9874e14
feat: add account model
limwa Jan 8, 2025
f9ddcb2
feat: login and showing invalid credential error
tomaspalma Jan 8, 2025
c0d0f51
chore: remove unsued import
tomaspalma Jan 8, 2025
91e26b0
feat: login and registering working with account and model architecture
tomaspalma Jan 16, 2025
afcdb8e
feat: middleware to redirect from login and register if user is logge…
tomaspalma Jan 17, 2025
917705f
feat: error reporting for register if email is taken or password != c…
tomaspalma Jan 17, 2025
ebf0f8a
feat: added login button to navbar and changed login and register bac…
tomaspalma Jan 17, 2025
3db2d68
feat: email and password length validation on backend
tomaspalma Jan 19, 2025
4fd21c0
Merge branch 'main' into feature/login
tomaspalma Jan 19, 2025
e10b94a
fix: confirm password does not give false positives anymore
tomaspalma Jan 19, 2025
b6e14ee
Merge branch 'main' into feature/login
limwa Jan 19, 2025
f8a99b7
fix: shadowed variable on ssr.tsx
tomaspalma Jan 20, 2025
293c3f8
feat: implement react email and part of email verification
limwa Jan 21, 2025
80bcdc8
feat: add limiter and change route names
limwa Jan 21, 2025
ed20c1b
refactor: change auth service to user service
limwa Jan 21, 2025
b456157
build: upgrade dependencies
limwa Jan 21, 2025
21f90d0
refactor: move validation messages to #start/validator
limwa Jan 21, 2025
3db18b5
chore: add migrations
limwa Jan 21, 2025
cfefe4b
feat: finish email confirmation flow, login and register
limwa Jan 22, 2025
3a17ff7
chore: make deployment script fail with invalid env variables
limwa Jan 22, 2025
291df10
chore: change entrypoint shell
limwa Jan 22, 2025
c1d5450
fix: handle caught exceptions
limwa Jan 22, 2025
e2af983
chore: allow fresh migrations on entrypoint
limwa Jan 22, 2025
3f98a65
fix: no need for inertia.always
limwa Jan 22, 2025
79ff027
feat: add cooldown to email verification page
limwa Jan 22, 2025
8be45c8
feat: make verification pages ui/ux better
limwa Jan 22, 2025
c3926e5
fix: make authentication emails use sans serif font
limwa Jan 22, 2025
3d14167
feat: small ux improvements
limwa Jan 22, 2025
e02f304
feat: use beige background in verification email
limwa Jan 22, 2025
234dd45
Update card.tsx
limwa Jan 22, 2025
6cd0f10
refactor: some changes here and there
limwa Jan 22, 2025
efce55e
chore: some more stuff
limwa Jan 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
[email protected]
[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=true

# 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?"
}
5 changes: 4 additions & 1 deletion website/app/exceptions/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,16 @@ export default class HttpExceptionHandler extends ExceptionHandler {
* codes. You might want to enable them in production only, but feel
* free to enable them in development as well.
*/
protected renderStatusPages = app.inProduction
protected renderStatusPages = true

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> = {
'403': (error, { inertia }) => inertia.render('errors/forbidden', { error }),
'404': (error, { inertia }) => inertia.render('errors/not_found', { error }),
'500..599': (error, { inertia }) => inertia.render('errors/server_error', { error }),
}
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'))
}
}
2 changes: 1 addition & 1 deletion website/app/middleware/container_bindings_middleware.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Logger } from '@adonisjs/core/logger'
import { HttpContext } from '@adonisjs/core/http'
import { NextFn } from '@adonisjs/core/types/http'
import type { NextFn } from '@adonisjs/core/types/http'

/**
* The container bindings middleware binds classes to their request
Expand Down
Loading
Loading