Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,13 @@ DISCORD_CLIENT_SECRET=someclientsecret
GITHUB_CLIENT_ID=someclientid
GITHUB_CLIENT_SECRET=someclientsecret
DRIVE_DISK=s3
MAIL_MAILER=smtp
MAIL_FROM_NAME=xContest Automated Mailer
MAIL_FROM_ADDRESS=noreply@local.host
SMTP_HOST=local.host
SMTP_PORT=465
SMTP_USERNAME=noreply@local.host
SMTP_PASSWORD=localpassword
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=thisisaverysecurepassword
13 changes: 12 additions & 1 deletion .github/workflows/api_testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ jobs:
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:8.6-trixie
ports:
- 6379:6379
options: >
--health-cmd "redis-cli ping"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- name: Install pnpm
Expand All @@ -32,13 +41,15 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 20
node-version: 24
cache: 'pnpm'

- name: Copy .env.example to .env
run: |
cp .env.example .env
sed -i 's/DB_HOST=db/DB_HOST=127.0.0.1/' .env
sed -i 's/REDIS_HOST=redis/REDIS_HOST=127.0.0.1/' .env
sed -i 's/REDIS_PASSWORD=.*/REDIS_PASSWORD=/' .env

- name: Install Dependencies
run: pnpm install --frozen-lockfile
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ tmp
.env.local
.env.production.local
.env.development.local
redis.conf

# Frontend assets compiled code
public/assets
Expand Down Expand Up @@ -52,3 +53,7 @@ yarn-error.log

# Useless stuff that doesn't need to be shared
.eslintcache

# Adonis.JS shared types
.adonisjs/
database/schema.ts
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM node:22.16.0-alpine3.22 AS base
FROM node:24.14.0-alpine3.22 AS base

# Install pnpm and husky
RUN corepack enable && corepack prepare pnpm@10.30.1 --activate && npm install -g husky
Expand Down
2 changes: 1 addition & 1 deletion ace.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
/**
* Register hook to process TypeScript files using ts-node
*/
import 'ts-node-maintained/register/esm'
import '@poppinss/ts-exec'

/**
* Import ace console entrypoint
Expand Down
39 changes: 32 additions & 7 deletions adonisrc.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { indexPolicies } from '@adonisjs/bouncer'
import { indexEntities } from '@adonisjs/core'
import { defineConfig } from '@adonisjs/core/app'


export default defineConfig({
/*
|--------------------------------------------------------------------------
Expand All @@ -16,6 +19,15 @@ export default defineConfig({
shutdownInReverseOrder: true,
},

// AdonisJS v7

hooks: {
init: [
indexEntities(),
indexPolicies(),
],
},

/*
|--------------------------------------------------------------------------
| Commands
Expand All @@ -25,7 +37,7 @@ export default defineConfig({
| will be scanned automatically from the "./commands" directory.
|
*/
commands: [() => import('@adonisjs/core/commands'), () => import('@adonisjs/lucid/commands'), () => import('@adonisjs/session/commands'), () => import('@adonisjs/bouncer/commands')],
commands: [() => import('@adonisjs/core/commands'), () => import('@adonisjs/lucid/commands'), () => import('@adonisjs/session/commands'), () => import('@adonisjs/bouncer/commands'), () => import('@adonisjs/mail/commands')],

/*
|--------------------------------------------------------------------------
Expand All @@ -51,6 +63,9 @@ export default defineConfig({
() => import('@adonisjs/ally/ally_provider'),
() => import('@adonisjs/bouncer/bouncer_provider'),
() => import('@adonisjs/drive/drive_provider'),
() => import('@adonisjs/mail/mail_provider'),
() => import('@adonisjs/core/providers/edge_provider'),
() => import('@adonisjs/redis/redis_provider'),
],

/*
Expand All @@ -61,7 +76,13 @@ export default defineConfig({
| List of modules to import before starting the application.
|
*/
preloads: [() => import('#start/routes'), () => import('#start/kernel'), () => import('#start/events')],
preloads: [
() => import('#start/routes'),
() => import('#start/kernel'),
() => import('#start/events'),
() => import('#start/mail'),
{ file: () => import('#workers/mail'), environment: ['web'] },
],

/*
|--------------------------------------------------------------------------
Expand All @@ -75,16 +96,20 @@ export default defineConfig({
tests: {
suites: [
{
files: ['tests/unit/**/*.spec(.ts|.js)'],
files: ['tests/unit/**/*.spec.{ts,js}'],
name: 'unit',
timeout: 2000,
timeout: 500,
},
{
files: ['tests/functional/**/*.spec(.ts|.js)'],
files: ['tests/functional/**/*.spec.{ts,js}'],
name: 'functional',
timeout: 30000,
timeout: 1000,
},
],
forceExit: false,
forceExit: true,
},
metaFiles: [{
pattern: 'resources/views/**/*.edge',
reloadServer: false,
}],
})
34 changes: 32 additions & 2 deletions app/controllers/teams_controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ import { applyQueryFilters } from '#utils/query'
import Task from '#models/task/task'
import { generateMemorableToken, generateSecureToken, invitationValidity } from '#utils/teams'
import TeamInvitation from '#models/team/team_invitation'
import InvitationSent from '#events/invitation_sent'
import mail from '@adonisjs/mail/services/main'
import env from '#start/env'
import { ApiOperation, ApiRequest, ApiResponse } from '#openapi/decorators'
import { merged, paginated } from '#openapi/tools'
Expand Down Expand Up @@ -196,7 +196,37 @@ export default class TeamsController {
token: payload.email ? generateSecureToken() : generateMemorableToken(),
expiresAt: invitationValidity[payload.validFor as keyof typeof invitationValidity](),
})
await InvitationSent.dispatch(invitation)
if (invitation.inviteeEmail) {
await invitation.load('inviter')
await invitation.load('team')
await invitation.team.load('event')

const { inviter, team: invitedTeam } = invitation
const event = invitedTeam.event
const baseUrl = env.get('WEBSITE')

await mail.sendLater((message) => {
message
.to(invitation.inviteeEmail!)
.subject("You've been invited!")
.htmlView('events/invite', {
invitee: { name: invitation.inviteeEmail },
inviter: {
name:
[inviter.name, inviter.surname].filter(Boolean).join(' ') || inviter.nickname,
},
event: {
title: event.title,
date: event.createdAt?.toISODate() ?? '',
description: event.description,
location: 'TBD', //! Event location should be added to the event model.
},
team: { name: invitedTeam.name },
inviteLink: `${baseUrl}/invitations/${invitation.id}?token=${invitation.token}`,
unsubscribeLink: `${baseUrl}/unsubscribe`,
})
})
}
return invitation
}

Expand Down
6 changes: 3 additions & 3 deletions app/utils/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

import { commonQueryValidator } from '#validators/common'
import type { ModelQueryBuilderContract } from '@adonisjs/lucid/types/model'
import type { Request, Response } from '@adonisjs/core/http'
import type { HttpRequest, HttpResponse } from '@adonisjs/core/http'

/**
* Applies common filters (search, sorting, status, filter) to any Lucid query
Expand All @@ -39,8 +39,8 @@ export async function applyQueryFilters<Q extends ModelQueryBuilderContract<any>
allowedColumns = [],
defaultTable,
}: {
request: Request,
response: Response,
request: HttpRequest,
response: HttpResponse,
searchColumn: string
defaultPageSize?: number,
allowedColumns?: string[],
Expand Down
11 changes: 0 additions & 11 deletions config/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,9 @@
*
*/

import env from '#start/env'
import app from '@adonisjs/core/services/app'
import { Secret } from '@adonisjs/core/helpers'
import { defineConfig } from '@adonisjs/core/http'

/**
* The app key is used for encrypting cookies, generating signed URLs,
* and by the "encryption" module.
*
* The encryption module will fail to decrypt data if the key is lost or
* changed. Therefore it is recommended to keep the app key secure.
*/
export const appKey = new Secret(env.get('APP_KEY'))

/**
* The configuration settings used by the HTTP server
*/
Expand Down
25 changes: 14 additions & 11 deletions app/events/invitation_sent.ts → config/encryption.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,27 +5,30 @@
* _> </ /___/ /_/ / / / / /_/ __(__ ) /_
* /_/|_|\____/\____/_/ /_/\__/\___/____/\__/
* Copyright (C) 2026 xContest Team
*
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*
*/

import { BaseEvent } from '@adonisjs/core/events'
import type TeamInvitation from '#models/team/team_invitation'
import env from '#start/env'
import { defineConfig, drivers } from '@adonisjs/core/encryption'

export default class InvitationSent extends BaseEvent {
constructor(public invitation: TeamInvitation) {
super()
}
}
export default defineConfig({
default: 'legacy',
list: {
legacy: drivers.legacy({
keys: [env.get('APP_KEY')],
}),
},
})
77 changes: 77 additions & 0 deletions config/mail.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*
* ______ __ __
* _ __/ ____/___ ____ / /____ _____/ /_
* | |/_/ / / __ \/ __ \/ __/ _ \/ ___/ __/
* _> </ /___/ /_/ / / / / /_/ __(__ ) /_
* /_/|_|\____/\____/_/ /_/\__/\___/____/\__/
* Copyright (C) 2026 xContest Team
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see http://www.gnu.org/licenses/.
*
*/

import env from '#start/env'
import { defineConfig, transports } from '@adonisjs/mail'

const mailConfig = defineConfig({
default: env.get('MAIL_MAILER'),

/**
* The mailers object can be used to configure multiple mailers
* each using a different transport or same transport with different
* options.
*/
from: {
address: env.get('MAIL_FROM_ADDRESS'),
name: env.get('MAIL_FROM_NAME'),
},

/**
* The globals are shared with all the templates rendered using the
* configured template engine.
*
* This could be a nice place to define the logo URL, links base URL
* the brand name to be used within the emails
*/
globals: {
branding: {
name: 'xContest',
},
},

/**
* The mailers object can be used to configure multiple mailers
* each using a different transport or same transport with different
* options.
*/
mailers: {
smtp: transports.smtp({
host: env.get('SMTP_HOST'),
port: env.get('SMTP_PORT'),
secure: true,
auth: {
type: 'login',
user: env.get('SMTP_USERNAME'),
pass: env.get('SMTP_PASSWORD'),
},
}),

},
})

export default mailConfig

declare module '@adonisjs/mail/types' {
export interface MailersList extends InferMailers<typeof mailConfig> {}
}
Loading