Skip to content

Commit

Permalink
feat: add mail (#29)
Browse files Browse the repository at this point in the history
  • Loading branch information
Barbapapazes authored Jul 29, 2022
1 parent 689e3e9 commit 7fa70e3
Show file tree
Hide file tree
Showing 21 changed files with 2,049 additions and 51 deletions.
25 changes: 19 additions & 6 deletions .adonisrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"@adonisjs/core/build/commands/index.js",
"@adonisjs/repl/build/commands",
"@adonisjs/lucid/build/commands",
"adonis-lucid-filter/build/commands"
"adonis-lucid-filter/build/commands",
"@adonisjs/mail/build/commands"
],
"exceptionHandlerNamespace": "App/Exceptions/Handler",
"aliases": {
Expand All @@ -14,7 +15,12 @@
"Database": "database",
"Contracts": "contracts"
},
"preloads": ["./start/routes", "./start/kernel", "./start/view"],
"preloads": [
"./start/routes",
"./start/kernel",
"./start/view",
"./start/mail"
],
"providers": [
"./providers/AppProvider",
"@adonisjs/core",
Expand All @@ -25,7 +31,8 @@
"@adonisjs/lucid-slugify",
"@adonisjs/route-model-binding/build/providers/RmbProvider",
"@adonisjs/attachment-lite",
"adonis-lucid-filter"
"adonis-lucid-filter",
"@adonisjs/mail"
],
"metaFiles": [
{
Expand All @@ -37,15 +44,21 @@
"reloadServer": false
}
],
"aceProviders": ["@adonisjs/repl"],
"aceProviders": [
"@adonisjs/repl"
],
"tests": {
"suites": [
{
"name": "functional",
"files": ["tests/functional/**/*.spec(.ts|.js)"],
"files": [
"tests/functional/**/*.spec(.ts|.js)"
],
"timeout": 60000
}
]
},
"testProviders": ["@japa/preset-adonis/TestsProvider"]
"testProviders": [
"@japa/preset-adonis/TestsProvider"
]
}
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,10 @@ PG_PORT=5432
PG_USER=lucid
PG_PASSWORD=
PG_DB_NAME=lucid
SMTP_HOST=localhost
SMTP_PORT=587
SMTP_USERNAME=<username>
SMTP_PASSWORD=<password>
SES_ACCESS_KEY=<aws-access-key>
SES_ACCESS_SECRET=<aws-secret>
SES_REGION=us-east-1
17 changes: 17 additions & 0 deletions ace-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -531,6 +531,23 @@
],
"aliases": [],
"flags": []
},
"make:mailer": {
"settings": {},
"commandPath": "@adonisjs/mail/build/commands/MakeMailer",
"commandName": "make:mailer",
"description": "Make a new mailer class",
"args": [
{
"type": "string",
"propertyName": "name",
"name": "name",
"required": true,
"description": "Name of the mailer class"
}
],
"aliases": [],
"flags": []
}
},
"aliases": {}
Expand Down
64 changes: 64 additions & 0 deletions app/Controllers/Http/AssociationsController.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
import { bind } from '@adonisjs/route-model-binding'
import { Attachment } from '@ioc:Adonis/Addons/AttachmentLite'
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'
import Route from '@ioc:Adonis/Core/Route'
import VerifyEmail from 'App/Mailers/VerifyEmail'
import Association from 'App/Models/Association'
import Category from 'App/Models/Category'
import School from 'App/Models/School'
import AssociationDocumentUpdateValidator from 'App/Validators/AssociationDocumentUpdateValidator'
import AssociationImageUpdateValidator from 'App/Validators/AssociationImageUpdateValidator'
import AssociationStoreValidator from 'App/Validators/AssociationStoreValidator'
import AssociationUpdateValidator from 'App/Validators/AssociationUpdateValidator'
import VoteStoreValidator from 'App/Validators/VoteStoreValidator'

export default class AssociationsController {
public async index({ request, view }: HttpContextContract) {
const associations = await Association.filter(request.qs())
.withCount('votes')
.preload('category')
.preload('school')

Expand Down Expand Up @@ -58,6 +62,8 @@ export default class AssociationsController {
loader.preload('school')
})

await association.loadCount('votes')

const relatedAssociations = await Association.query()
.where('category_id', association.categoryId ?? 0)
.where('id', '!=', association.id)
Expand Down Expand Up @@ -123,4 +129,62 @@ export default class AssociationsController {

return response.redirect().toRoute('AssociationsController.index')
}

@bind()
public async sendEmailVote({ request, view }: HttpContextContract, association: Association) {
const { email, acceptClassement, acceptActivities } = await request.validate(VoteStoreValidator)

const signedUrl = Route.makeSignedUrl(
'AssociationsController.vote',
{ id: association.slug, email },
{
qs: { acceptClassement, acceptActivities },
}
)

await new VerifyEmail(email, signedUrl).sendLater()

return view.render('vote/index', {
title: 'Pense à valider ton vote',
subtitle:
"Merci d'avoir voté ! Tu vas recevoir d'ici quelques instant un mail pour valider ton vote.",
})
}

@bind()
public async vote(
{ request, params, view, logger }: HttpContextContract,
association: Association
) {
if (request.hasValidSignature()) {
const { email } = params
const { acceptClassement, acceptActivities } = request.qs()

try {
await association.related('votes').create({
email,
acceptClassement,
acceptActivities,
})
} catch (error) {
logger.error(error)
logger.error(email)
return view.render('vote/index', {
title: 'Vous avez déjà voté pour cette association',
subtitle: 'Mais tu peux continuer à suivre le Classement sur ses réseaux !',
})
}

return view.render('vote/index', {
title: 'Votre voix a été prise en compte',
subtitle:
"Merci d'avoir voté. Tu peux continuer à suivre le Classement via ses réseaux si tu le souhaites !",
})
}

return view.render('vote/index', {
title: "Ce lien n'est pas valide",
subtitle: 'Tu peux réessayer en retournant sur la page de ton association !',
})
}
}
18 changes: 18 additions & 0 deletions app/Mailers/VerifyEmail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { BaseMailer, MessageContract } from '@ioc:Adonis/Addons/Mail'
import View from '@ioc:Adonis/Core/View'

export default class VerifyEmail extends BaseMailer {
constructor(private email: string, private signedUrl: string) {
super()
}

public async prepare(message: MessageContract) {
const html = await View.render('emails/verify-email', { signedUrl: this.signedUrl })

message
.subject('Valide ton vote - Le Classement des Associations ✨')
.from('no-reply@le-classement.fr')
.to(this.email)
.html(html)
}
}
6 changes: 5 additions & 1 deletion app/Models/Association.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,12 @@ import { attachment, AttachmentContract } from '@ioc:Adonis/Addons/AttachmentLit
import { Filterable } from '@ioc:Adonis/Addons/LucidFilter'
import { slugify } from '@ioc:Adonis/Addons/LucidSlugify'
import { compose } from '@ioc:Adonis/Core/Helpers'
import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm'
import { BaseModel, BelongsTo, belongsTo, column, HasMany, hasMany } from '@ioc:Adonis/Lucid/Orm'
import { DateTime } from 'luxon'
import Category from './Category'
import AssociationFilter from './Filters/AssociationFilter'
import School from './School'
import Vote from './Vote'

export default class Association extends compose(BaseModel, Filterable) {
public static $filter = () => AssociationFilter
Expand Down Expand Up @@ -67,4 +68,7 @@ export default class Association extends compose(BaseModel, Filterable) {

@belongsTo(() => School)
public school: BelongsTo<typeof School>

@hasMany(() => Vote)
public votes: HasMany<typeof Vote>
}
2 changes: 1 addition & 1 deletion app/Models/Filters/AssociationFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default class AssociationFilter extends BaseModelFilter {

public setup() {
if (!this.$input['order_by']) {
// Order by number of voices by default
this.$query.orderBy('votes_count', 'desc')
}
}

Expand Down
29 changes: 29 additions & 0 deletions app/Models/Vote.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { BaseModel, BelongsTo, belongsTo, column } from '@ioc:Adonis/Lucid/Orm'
import { DateTime } from 'luxon'
import Association from './Association'

export default class Vote extends BaseModel {
@column({ isPrimary: true })
public id: number

@column()
public associationId: number

@column()
public email: string

@column()
public acceptClassement: boolean

@column()
public acceptActivities: boolean

@column.dateTime({ autoCreate: true })
public createdAt: DateTime

@column.dateTime({ autoCreate: true, autoUpdate: true })
public updatedAt: DateTime

@belongsTo(() => Association)
public association: BelongsTo<typeof Association>
}
38 changes: 38 additions & 0 deletions app/Validators/VoteStoreValidator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { schema, rules, CustomMessages } from '@ioc:Adonis/Core/Validator'
import type { HttpContextContract } from '@ioc:Adonis/Core/HttpContext'

export default class VoteStoreValidator {
constructor(protected ctx: HttpContextContract) {}

public schema = schema.create({
email: schema.string([
rules.trim(),
rules.maxLength(255),
rules.email(),
rules.normalizeEmail({
allLowercase: true,
gmailRemoveDots: true,
gmailRemoveSubaddress: true,
icloudRemoveSubaddress: true,
outlookdotcomRemoveSubaddress: true,
yahooRemoveSubaddress: true,
}),
rules.unique({
table: 'votes',
column: 'email',
}),
]),
acceptClassement: schema.boolean.optional(),
acceptActivities: schema.boolean.optional(),
})

public messages: CustomMessages = {
'association_id.exists': "Cette association n'existe pas",
'email.required': 'Un email est requis',
'email.maxLength': "L'email ne doit pas dépasser 255 caractères",
'email.email': "Cet email n'est pas valide",
'email.unique': 'Cet email est déjà utilisé',
'accept_classement.boolean': 'Cette option doit être un booléen',
'accept_activities.boolean': 'Cette option doit être un booléen',
}
}
80 changes: 80 additions & 0 deletions config/mail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* Config source: https://git.io/JvgAf
*
* Feel free to let us know via PR, if you find something broken in this contract
* file.
*/

import Env from '@ioc:Adonis/Core/Env'
import { mailConfig } from '@adonisjs/mail/build/config'

export default mailConfig({
/*
|--------------------------------------------------------------------------
| Default mailer
|--------------------------------------------------------------------------
|
| The following mailer will be used to send emails, when you don't specify
| a mailer
|
*/
mailer: 'smtp',

/*
|--------------------------------------------------------------------------
| Mailers
|--------------------------------------------------------------------------
|
| You can define or more mailers to send emails from your application. A
| single `driver` can be used to define multiple mailers with different
| config.
|
| For example: Postmark driver can be used to have different mailers for
| sending transactional and promotional emails
|
*/
mailers: {
/*
|--------------------------------------------------------------------------
| Smtp
|--------------------------------------------------------------------------
|
| Uses SMTP protocol for sending email
|
*/
smtp: {
driver: 'smtp',
host: Env.get('SMTP_HOST'),
port: Env.get('SMTP_PORT'),
auth: {
user: Env.get('SMTP_USERNAME'),
pass: Env.get('SMTP_PASSWORD'),
type: 'login',
},
},

/*
|--------------------------------------------------------------------------
| SES
|--------------------------------------------------------------------------
|
| Uses Amazon SES for sending emails. You will have to install the aws-sdk
| when using this driver.
|
| ```
| npm i aws-sdk
| ```
|
*/
ses: {
driver: 'ses',
apiVersion: '2010-12-01',
key: Env.get('SES_ACCESS_KEY'),
secret: Env.get('SES_ACCESS_SECRET'),
region: Env.get('SES_REGION'),
sslEnabled: true,
sendingRate: 10,
maxConnections: 5,
},
},
})
Loading

0 comments on commit 7fa70e3

Please sign in to comment.