Private
Public Access
1
0

Compare commits

...

5 Commits

Author SHA1 Message Date
8795fedda9 chg: usr: add notification on activation too #4
All checks were successful
Deploy to Production / deploy (push) Successful in 11s
2026-04-15 20:23:41 +02:00
588fb57299 new: usr: add notification email when a user is registered #4 2026-04-15 20:19:29 +02:00
eb345e17ca chg: pkg: new version release !skipChangelog
All checks were successful
Deploy to Production / deploy (push) Successful in 19s
2026-04-15 20:13:38 +02:00
c2693c4648 fix: usr: another attempt to fix the email assets #4
All checks were successful
Deploy to Production / deploy (push) Successful in 11s
2026-04-15 20:03:48 +02:00
43efc16562 fix: usr: the images does not shows in emails #4
All checks were successful
Deploy to Production / deploy (push) Successful in 15s
2026-04-15 19:50:14 +02:00
9 changed files with 250 additions and 4 deletions

View File

@@ -6,6 +6,9 @@
APP_ENV=dev APP_ENV=dev
APP_SECRET=changethis APP_SECRET=changethis
APP_NAME=mineseeker APP_NAME=mineseeker
# APP_PUBLIC_HOSTNAME: The public hostname for your application (used for generating absolute URLs in emails)
# For production, set this to your domain (e.g., mineseeker.com)
APP_PUBLIC_HOSTNAME=localhost
# TRUSTED_PROXIES: Only needed for bare-metal dev behind a reverse proxy # TRUSTED_PROXIES: Only needed for bare-metal dev behind a reverse proxy
# For Docker development, this is set in compose.override.yaml # For Docker development, this is set in compose.override.yaml
# For production, set in PROD_ENV_FILE Gitea secret (use 172.18.0.0/16 initially) # For production, set in PROD_ENV_FILE Gitea secret (use 172.18.0.0/16 initially)

View File

@@ -11,6 +11,7 @@ services:
SERVER_NAME: ${SERVER_NAME:-:80} SERVER_NAME: ${SERVER_NAME:-:80}
APP_ENV: prod APP_ENV: prod
APP_SECRET: ${APP_SECRET} APP_SECRET: ${APP_SECRET}
APP_PUBLIC_HOSTNAME: ${APP_PUBLIC_HOSTNAME:-localhost}
APP_CONTACT_MAIL_ADDRESS: ${APP_CONTACT_MAIL_ADDRESS:-7system7@gmail.com} APP_CONTACT_MAIL_ADDRESS: ${APP_CONTACT_MAIL_ADDRESS:-7system7@gmail.com}
DATABASE_URL: >- DATABASE_URL: >-
postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?serverVersion=${POSTGRES_VERSION}&charset=utf8 postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}?serverVersion=${POSTGRES_VERSION}&charset=utf8

View File

@@ -8,6 +8,13 @@ framework:
session: session:
handler_id: ~ handler_id: ~
# Trust headers from reverse proxy (Caddy)
# This ensures absolute_url() uses HTTPS scheme when behind a reverse proxy
# Production: TRUSTED_PROXIES from .env (Gitea secret)
# Development: TRUSTED_PROXIES from compose.override.yaml
trusted_proxies: '%env(TRUSTED_PROXIES)%'
trusted_headers: ['x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host', 'x-forwarded-port']
#esi: true #esi: true
#fragments: true #fragments: true
php_errors: php_errors:

View File

@@ -0,0 +1,8 @@
framework:
# In production with FrankenPHP, the reverse proxy (Caddy) is in the same container
# Requests come from 127.0.0.1, so we must trust that IP to process X-Forwarded-Proto headers
# TRUSTED_PROXIES is set in the .env file (stored in Gitea secrets)
# Typical value for Docker: 172.18.0.0/16 (or the specific Docker network CIDR)
# This must be provided by the PROD_ENV_FILE secret in Gitea
trusted_proxies: '%env(TRUSTED_PROXIES)%'
trusted_headers: ['x-forwarded-for', 'x-forwarded-proto', 'x-forwarded-host', 'x-forwarded-port']

View File

@@ -19,6 +19,7 @@ use DateTime;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bridge\Twig\Mime\TemplatedEmail; use Symfony\Bridge\Twig\Mime\TemplatedEmail;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Attribute\AsController; use Symfony\Component\HttpKernel\Attribute\AsController;
@@ -41,6 +42,12 @@ use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;
#[AsController] #[AsController]
class SecurityController extends AbstractController class SecurityController extends AbstractController
{ {
public function __construct(
#[Autowire(env: 'APP_CONTACT_MAIL_ADDRESS')]
private readonly string $appContactMailAddress,
) {
}
#[Route('/login', name: 'MineSeekerBundle_login')] #[Route('/login', name: 'MineSeekerBundle_login')]
public function login(AuthenticationUtils $authenticationUtils): Response public function login(AuthenticationUtils $authenticationUtils): Response
{ {
@@ -92,6 +99,11 @@ class SecurityController extends AbstractController
UrlGeneratorInterface::ABSOLUTE_URL, UrlGeneratorInterface::ABSOLUTE_URL,
); );
/** Ensure HTTPS scheme in production */
if ($this->getParameter('kernel.environment') === 'prod') {
$activationUrl = str_replace('http://', 'https://', $activationUrl);
}
$mailer->send( $mailer->send(
new TemplatedEmail() new TemplatedEmail()
->from('noreply@mineseeker.hu') ->from('noreply@mineseeker.hu')
@@ -104,6 +116,19 @@ class SecurityController extends AbstractController
]) ])
); );
/** Send admin notification about new user registration */
$mailer->send(
new TemplatedEmail()
->from('noreply@mineseeker.hu')
->to($this->appContactMailAddress)
->subject('🎉 New User Registration: ' . $user->getUsername())
->htmlTemplate('emails/user_registration_notification.html.twig')
->context([
'user' => $user,
'registeredAt' => new DateTime(),
])
);
$this->addFlash('verify_email', $user->getEmail()); $this->addFlash('verify_email', $user->getEmail());
return $this->redirectToRoute('MineSeekerBundle_register'); return $this->redirectToRoute('MineSeekerBundle_register');
@@ -143,6 +168,11 @@ class SecurityController extends AbstractController
UrlGeneratorInterface::ABSOLUTE_URL, UrlGeneratorInterface::ABSOLUTE_URL,
); );
/** Ensure HTTPS scheme in production */
if ($this->getParameter('kernel.environment') === 'prod') {
$resetUrl = str_replace('http://', 'https://', $resetUrl);
}
$mailer->send( $mailer->send(
new TemplatedEmail() new TemplatedEmail()
->from('noreply@mineseeker.hu') ->from('noreply@mineseeker.hu')
@@ -199,7 +229,7 @@ class SecurityController extends AbstractController
} }
#[Route('/activate/{token}', name: 'MineSeekerBundle_activate')] #[Route('/activate/{token}', name: 'MineSeekerBundle_activate')]
public function activate(string $token, EntityManagerInterface $em): Response public function activate(string $token, EntityManagerInterface $em, MailerInterface $mailer): Response
{ {
$user = $em->getRepository(User::class)->findOneBy(['verificationToken' => $token]); $user = $em->getRepository(User::class)->findOneBy(['verificationToken' => $token]);
@@ -211,6 +241,19 @@ class SecurityController extends AbstractController
$user->setIsVerified(true)->setVerificationToken(null); $user->setIsVerified(true)->setVerificationToken(null);
$em->flush(); $em->flush();
/** Send admin notification about account activation */
$mailer->send(
new TemplatedEmail()
->from('noreply@mineseeker.hu')
->to($this->appContactMailAddress)
->subject('✅ User Account Activated: ' . $user->getUsername())
->htmlTemplate('emails/user_activation_notification.html.twig')
->context([
'user' => $user,
'activatedAt' => new DateTime(),
])
);
$this->addFlash('success', 'Your account is now active. Welcome, ' . $user->getUsername() . '!'); $this->addFlash('success', 'Your account is now active. Welcome, ' . $user->getUsername() . '!');
return $this->redirectToRoute('MineSeekerBundle_login'); return $this->redirectToRoute('MineSeekerBundle_login');

View File

@@ -77,7 +77,7 @@
<body> <body>
<div class="wrapper"> <div class="wrapper">
<div class="logo"> <div class="logo">
<img src="{{ absolute_url(asset('images/mine-logo-txt.png')) }}" alt="MineSeeker"/> <img src="{{ absolute_url(asset('images/mine-logo-txt.png')) | replace({'http://': 'https://'}) }}" alt="MineSeeker"/>
</div> </div>
<div class="card"> <div class="card">
<h1>One step to go</h1> <h1>One step to go</h1>
@@ -100,4 +100,4 @@
</div> </div>
</div> </div>
</body> </body>
</html> </html>

View File

@@ -91,7 +91,7 @@
<body> <body>
<div class="wrapper"> <div class="wrapper">
<div class="logo"> <div class="logo">
<img src="{{ absolute_url(asset('images/mine-logo-txt.png')) }}" alt="MineSeeker"/> <img src="{{ absolute_url(asset('images/mine-logo-txt.png')) | replace({'http://': 'https://'}) }}" alt="MineSeeker"/>
</div> </div>
<div class="card"> <div class="card">
<h1>Reset your password</h1> <h1>Reset your password</h1>

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>User Account Activated</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 8px 8px 0 0;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 24px;
}
.content {
background: #f9f9f9;
padding: 30px;
border-radius: 0 0 8px 8px;
}
.field {
margin-bottom: 20px;
}
.field-label {
font-weight: 600;
color: #666;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 5px;
}
.field-value {
background: white;
padding: 12px;
border-radius: 4px;
border-left: 3px solid #667eea;
}
.footer {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ddd;
font-size: 12px;
color: #999;
text-align: center;
}
</style>
</head>
<body>
<div class="header">
<h1>✅ User Account Activated</h1>
</div>
<div class="content">
<div class="field">
<div class="field-label">Username</div>
<div class="field-value">
<strong>{{ user.username }}</strong>
</div>
</div>
<div class="field">
<div class="field-label">Email</div>
<div class="field-value">
<a href="mailto:{{ user.email }}">{{ user.email }}</a>
</div>
</div>
<div class="field">
<div class="field-label">Details</div>
<div class="field-value">
<strong>Activated:</strong> {{ activatedAt|date('Y-m-d H:i:s') }}<br>
<strong>Status:</strong> ✓ Email Verified - Account Active<br>
<strong>Email Verified:</strong> Yes
</div>
</div>
<div class="footer">
A user has successfully verified their email and activated their account on MineSeeker.<br>
They can now play games immediately.
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,92 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>New User Registration</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
line-height: 1.6;
color: #333;
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 30px;
border-radius: 8px 8px 0 0;
text-align: center;
}
.header h1 {
margin: 0;
font-size: 24px;
}
.content {
background: #f9f9f9;
padding: 30px;
border-radius: 0 0 8px 8px;
}
.field {
margin-bottom: 20px;
}
.field-label {
font-weight: 600;
color: #666;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 5px;
}
.field-value {
background: white;
padding: 12px;
border-radius: 4px;
border-left: 3px solid #667eea;
}
.footer {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid #ddd;
font-size: 12px;
color: #999;
text-align: center;
}
</style>
</head>
<body>
<div class="header">
<h1>👤 New User Registration</h1>
</div>
<div class="content">
<div class="field">
<div class="field-label">Username</div>
<div class="field-value">
<strong>{{ user.username }}</strong>
</div>
</div>
<div class="field">
<div class="field-label">Email</div>
<div class="field-value">
<a href="mailto:{{ user.email }}">{{ user.email }}</a>
</div>
</div>
<div class="field">
<div class="field-label">Details</div>
<div class="field-value">
<strong>Registered:</strong> {{ registeredAt|date('Y-m-d H:i:s') }}<br>
<strong>Status:</strong> {% if user.isVerified %}✓ Verified{% else %}⏳ Awaiting Email Verification{% endif %}<br>
<strong>Email Verified:</strong> {% if user.isVerified %}Yes{% else %}No - activation link sent{% endif %}
</div>
</div>
<div class="footer">
A new user has registered on MineSeeker.<br>
User must verify their email before account is fully activated.
</div>
</div>
</body>
</html>