@ -6,6 +6,7 @@ Handles login, logout, password reset, and profile authentication.
import os
import secrets
import smtplib
import ssl
from typing import Optional
from datetime import datetime , timedelta , timezone
from email . mime . text import MIMEText
@ -123,38 +124,21 @@ async def password_reset_request(req: PasswordResetRequest, request: Request):
cur . execute ( " INSERT INTO sessions (token, profile_id, expires_at, created_at) VALUES ( %s , %s , %s ,CURRENT_TIMESTAMP) " ,
( f " reset_ { token } " , prof [ ' id ' ] , expires . isoformat ( ) ) )
# Send email
try :
smtp_host = os . getenv ( " SMTP_HOST " )
smtp_port = int ( os . getenv ( " SMTP_PORT " , 587 ) )
smtp_user = os . getenv ( " SMTP_USER " )
smtp_pass = os . getenv ( " SMTP_PASS " )
smtp_from = os . getenv ( " SMTP_FROM " )
app_url = os . getenv ( " APP_URL " , " https://mitai.jinkendo.de " )
if smtp_host and smtp_user and smtp_pass :
msg = MIMEText ( f """ Hallo { prof [ ' name ' ] } ,
app_base = ( os . getenv ( " APP_URL " ) or " https://shinkan.jinkendo.de " ) . rstrip ( " / " )
reset_body = f """ Hallo { prof [ ' name ' ] } ,
Du hast einen Passwort - Reset angefordert .
Reset - Link : { app_ url } / reset - password ? token = { token }
Reset - Link : { app_base } / reset - password ? token = { token }
Der Link ist 1 Stunde gültig .
Falls du diese Anfrage nicht gestellt hast , ignoriere diese E - Mail .
Dein Mitai Jinkendo Team
""" )
msg [ ' Subject ' ] = " Passwort zurücksetzen – Mitai Jinkendo "
msg [ ' From ' ] = smtp_from
msg [ ' To ' ] = email
with smtplib . SMTP ( smtp_host , smtp_port ) as server :
server . starttls ( )
server . login ( smtp_user , smtp_pass )
server . send_message ( msg )
except Exception as e :
print ( f " Email error: { e } " )
Dein Shinkan Jinkendo Team
"""
if not send_email ( email , " Passwort zurücksetzen – Shinkan Jinkendo " , reset_body ) :
print ( " [SMTP] Passwort-Reset konnte nicht gesendet werden (SMTP prüfen). " )
return { " ok " : True , " message " : " Falls die E-Mail existiert, wurde ein Reset-Link gesendet. " }
@ -177,34 +161,89 @@ def password_reset_confirm(req: PasswordResetConfirm):
return { " ok " : True , " message " : " Passwort erfolgreich zurückgesetzt " }
# ── Helpers (Registrierung / E-Mail-Links) ─────────────────────────────────────
def _public_app_base ( ) - > str :
return ( os . getenv ( " APP_URL " ) or " https://shinkan.jinkendo.de " ) . rstrip ( " / " )
def verification_link ( token : str ) - > str :
""" Öffentlicher Link zur Bestätigung (FastAPI unter /api/auth). """
return f " { _public_app_base ( ) } /api/auth/verify/ { token } "
def registration_role ( cur , email_lower : str ) - > str :
"""
bootstrap : erste Registrierung in leerer DB → admin ,
oder E - Mail ∈ ADMIN_BOOTSTRAP_EMAILS ( kommasepariert , Groß / Klein egal ) .
Um alle Self - Regs als Trainer zu haben : AUTO_ADMIN_FIRST_USER = false und keine ADMIN_BOOTSTRAP_EMAILS .
"""
bootstrap = {
x . strip ( ) . lower ( )
for x in os . getenv ( " ADMIN_BOOTSTRAP_EMAILS " , " " ) . split ( " , " )
if x . strip ( )
}
if email_lower in bootstrap :
return " admin "
if os . getenv ( " AUTO_ADMIN_FIRST_USER " , " true " ) . strip ( ) . lower ( ) not in ( " 1 " , " true " , " yes " ) :
return " trainer "
cur . execute ( " SELECT COUNT(*) AS n FROM profiles " )
row = cur . fetchone ( )
try :
n = int ( row [ " n " ] ) if row is not None else 0
except ( KeyError , TypeError , ValueError ) :
n = 0
return " admin " if n == 0 else " trainer "
# ── Helper: Send Email ────────────────────────────────────────────────────────
def send_email ( to_email : str , subject : str , body : str ) :
""" Send email via SMTP (reusable helper). """
""" Send mail via SMTP. Port 465 → SSL; sonst SMTP + optional STARTTLS (587 )."""
try :
smtp_host = os . getenv ( " SMTP_HOST " )
smtp_port = int ( os . getenv ( " SMTP_PORT " , 587 ) )
smtp_user = os . getenv ( " SMTP_USER " )
smtp_host = ( os . getenv ( " SMTP_HOST " ) or " " ) . strip ( )
smtp_port_raw = os . getenv ( " SMTP_PORT " ) or " 587 "
try :
smtp_port = int ( str ( smtp_port_raw ) . strip ( ) )
except ValueError :
smtp_port = 587
smtp_user = ( os . getenv ( " SMTP_USER " ) or " " ) . strip ( )
smtp_pass = os . getenv ( " SMTP_PASS " )
smtp_from = os . getenv ( " SMTP_FROM " , " noreply@jinkendo.de " )
if not smtp_host or not smtp_user or not smtp_pass :
print ( " SMTP not configured, skipping email " )
if not smtp_host or not smtp_user or smtp_pass is None or smtp_pass == " " :
print ( " [SMTP] Nicht konfiguriert — setze SMTP_HOST, SMTP_USER, SMTP_PASS (und ggf. SMTP_PORT) " )
return False
msg = MIMEText ( body )
msg [ ' Subject ' ] = subject
msg [ ' From ' ] = smtp_from
msg [ ' To ' ] = to_email
msg [ " Subject " ] = subject
msg [ " From " ] = smtp_from
msg [ " To " ] = to_email
with smtplib . SMTP ( smtp_host , smtp_port ) as server :
server . starttls ( )
use_tls_env = ( os . getenv ( " SMTP_STARTTLS " , " true " ) . strip ( ) . lower ( ) not in ( " 0 " , " false " , " no " ) )
force_ssl = ( os . getenv ( " SMTP_SSL " , " " ) . strip ( ) . lower ( ) in ( " 1 " , " true " , " yes " ) ) or smtp_port == 465
ctx = ssl . create_default_context ( )
if force_ssl :
print ( f " [SMTP] Versand (SSL) an …@ { to_email . split ( ' @ ' ) [ - 1 ] } über { smtp_host } : { smtp_port } " )
with smtplib . SMTP_SSL ( smtp_host , smtp_port , context = ctx , timeout = 30 ) as server :
server . login ( smtp_user , smtp_pass )
server . send_message ( msg )
else :
print ( f " [SMTP] Versand (STARTTLS= { use_tls_env } ) an …@ { to_email . split ( ' @ ' ) [ - 1 ] } über { smtp_host } : { smtp_port } " )
with smtplib . SMTP ( smtp_host , smtp_port , timeout = 30 ) as server :
server . ehlo ( )
if use_tls_env :
server . starttls ( context = ctx )
server . ehlo ( )
server . login ( smtp_user , smtp_pass )
server . send_message ( msg )
print ( " [SMTP] Nachricht erfolgreich akzeptiert (Server-Queue) " )
return True
except Exception as e :
print ( f " Email error: { e } " )
print ( f " [SMTP] Fehle r: { e } " )
return False
@ -237,6 +276,9 @@ async def register(req: RegisterRequest, request: Request):
verification_token = secrets . token_urlsafe ( 32 )
verification_expires = datetime . now ( timezone . utc ) + timedelta ( hours = 24 )
# Rolle: erster Nutzer oder ADMIN_BOOTSTRAP_EMAILS → admin
role = registration_role ( cur , email )
# Create profile (inactive until verified) — profiles.id ist SERIAL (INT), keine String-IDs einfügen.
pin_hash = hash_pin ( password )
trial_ends = datetime . now ( timezone . utc ) + timedelta ( days = 14 ) # 14-day trial
@ -246,27 +288,28 @@ async def register(req: RegisterRequest, request: Request):
name , email , pin_hash , auth_type , role , tier ,
email_verified , verification_token , verification_expires ,
trial_ends_at , created_at
) VALUES ( % s , % s , % s , ' email ' , ' trainer ' , ' free ' , FALSE , % s , % s , % s , CURRENT_TIMESTAMP )
""" , (name, email, pin_hash, verification_token, verification_expires, trial_ends))
) VALUES ( % s , % s , % s , ' email ' , % s , ' free ' , FALSE , % s , % s , % s , CURRENT_TIMESTAMP )
""" , (name, email, pin_hash, role, verification_token, verification_expires, trial_ends))
# Send verification email
app_url = os . getenv ( " APP_URL " , " https://mitai.jinkendo.de " )
verify_url = f " { app_url } /verify?token= { verification_token } "
verify_url = verification_link ( verification_token )
email_body = f """ Hallo { name } ,
willkommen bei Mitai Jinkendo !
willkommen bei Shinkan Jinkendo !
Bitte bestätige deine E - Mail - Adresse um die Registrierung abzuschließen :
Bitte bestätige deine E - Mail - Adresse , um die Registrierung abzuschließen :
{ verify_url }
Der Link ist 24 Stunden gültig .
Dein Mitai Jinkendo Team
Nach dem Aufruf des Links bist du bestätigt ( Browser zeigt eine kurze JSON - Antwort – das ist in Ordnung ) .
Dein Shinkan Jinkendo Team
"""
send_email ( email , " Willkommen bei Mitai Jinkendo – E-Mail bestätigen " , email_body )
if not send_email ( email , " Shinkan Jinkendo – E-Mail bestätigen " , email_body ) :
print ( " [SMTP] Verifizierungs-Mail konnte nicht gesendet werden — Logs prüfen, SMTP_* in der Laufzeitumgebung. " )
return {
" ok " : True ,
@ -366,34 +409,22 @@ async def resend_verification(req: dict, request: Request):
WHERE id = % s
""" , (verification_token, verification_expires, prof[ ' id ' ]))
# Send verification email
app_url = os . getenv ( " APP_URL " , " https://mitai.jinkendo.de " )
verify_url = f " { app_url } /verify?token= { verification_token } "
verify_url = verification_link ( verification_token )
email_body = f """ Hallo { prof [ ' name ' ] } ,
du hast eine neue Bestätigungs - E - Mail angefordert .
Bitte bestätige deine E - Mail - Adresse , indem du auf folgenden Link klickst :
Bitte bestätige deine E - Mail - Adresse mit diesem Link :
{ verify_url }
Dieser Link ist 24 Stunden gültig .
Falls du diese E - Mail nicht angefordert hast , kannst du sie einfach ignorieren .
Viele Grüße
Dein Mitai Jinkendo Team
Dein Shinkan Jinkendo Team
"""
try :
send_email (
to = email ,
subject = " Neue Bestätigungs-E-Mail - Mitai Jinkendo " ,
body = email_body
)
except Exception as e :
print ( f " Failed to send verification email: { e } " )
raise HTTPException ( 500 , " E-Mail konnte nicht versendet werden " )
if not send_email ( email , " Shinkan Jinkendo – E-Mail bestätigen " , email_body ) :
raise HTTPException ( 502 , " E-Mail konnte nicht versendet werden. SMTP-Einstellungen und Container-Logs prüfen. " )
return { " ok " : True , " message " : " Bestätigungs-E-Mail wurde erneut versendet. " }