@ -6,6 +6,7 @@ Handles login, logout, password reset, and profile authentication.
import os
import os
import secrets
import secrets
import smtplib
import smtplib
import ssl
from typing import Optional
from typing import Optional
from datetime import datetime , timedelta , timezone
from datetime import datetime , timedelta , timezone
from email . mime . text import MIMEText
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) " ,
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 ( ) ) )
( f " reset_ { token } " , prof [ ' id ' ] , expires . isoformat ( ) ) )
# Send email
app_base = ( os . getenv ( " APP_URL " ) or " https://shinkan.jinkendo.de " ) . rstrip ( " / " )
try :
reset_body = f """ Hallo { prof [ ' name ' ] } ,
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 ' ] } ,
Du hast einen Passwort - Reset angefordert .
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 .
Der Link ist 1 Stunde gültig .
Falls du diese Anfrage nicht gestellt hast , ignoriere diese E - Mail .
Falls du diese Anfrage nicht gestellt hast , ignoriere diese E - Mail .
Dein Mitai Jinkendo Team
Dein Shinkan Jinkendo Team
""" )
"""
msg [ ' Subject ' ] = " Passwort zurücksetzen – Mitai Jinkendo "
if not send_email ( email , " Passwort zurücksetzen – Shinkan Jinkendo " , reset_body ) :
msg [ ' From ' ] = smtp_from
print ( " [SMTP] Passwort-Reset konnte nicht gesendet werden (SMTP prüfen). " )
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 } " )
return { " ok " : True , " message " : " Falls die E-Mail existiert, wurde ein Reset-Link gesendet. " }
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 " }
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 ────────────────────────────────────────────────────────
# ── Helper: Send Email ────────────────────────────────────────────────────────
def send_email ( to_email : str , subject : str , body : str ) :
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 :
try :
smtp_host = os . getenv ( " SMTP_HOST " )
smtp_host = ( os . getenv ( " SMTP_HOST " ) or " " ) . strip ( )
smtp_port = int ( os . getenv ( " SMTP_PORT " , 587 ) )
smtp_port_raw = os . getenv ( " SMTP_PORT " ) or " 587 "
smtp_user = os . getenv ( " SMTP_USER " )
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_pass = os . getenv ( " SMTP_PASS " )
smtp_from = os . getenv ( " SMTP_FROM " , " noreply@jinkendo.de " )
smtp_from = os . getenv ( " SMTP_FROM " , " noreply@jinkendo.de " )
if not smtp_host or not smtp_user or not smtp_pass :
if not smtp_host or not smtp_user or smtp_pass is None or smtp_pass == " " :
print ( " SMTP not configured, skipping email " )
print ( " [SMTP] Nicht konfiguriert — setze SMTP_HOST, SMTP_USER, SMTP_PASS (und ggf. SMTP_PORT) " )
return False
return False
msg = MIMEText ( body )
msg = MIMEText ( body )
msg [ ' Subject ' ] = subject
msg [ " Subject " ] = subject
msg [ ' From ' ] = smtp_from
msg [ " From " ] = smtp_from
msg [ ' To ' ] = to_email
msg [ " To " ] = to_email
with smtplib . SMTP ( smtp_host , smtp_port ) as server :
use_tls_env = ( os . getenv ( " SMTP_STARTTLS " , " true " ) . strip ( ) . lower ( ) not in ( " 0 " , " false " , " no " ) )
server . starttls ( )
force_ssl = ( os . getenv ( " SMTP_SSL " , " " ) . strip ( ) . lower ( ) in ( " 1 " , " true " , " yes " ) ) or smtp_port == 465
server . login ( smtp_user , smtp_pass )
server . send_message ( msg )
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
return True
except Exception as e :
except Exception as e :
print ( f " Email error: { e } " )
print ( f " [SMTP] Fehle r: { e } " )
return False
return False
@ -237,6 +276,9 @@ async def register(req: RegisterRequest, request: Request):
verification_token = secrets . token_urlsafe ( 32 )
verification_token = secrets . token_urlsafe ( 32 )
verification_expires = datetime . now ( timezone . utc ) + timedelta ( hours = 24 )
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.
# Create profile (inactive until verified) — profiles.id ist SERIAL (INT), keine String-IDs einfügen.
pin_hash = hash_pin ( password )
pin_hash = hash_pin ( password )
trial_ends = datetime . now ( timezone . utc ) + timedelta ( days = 14 ) # 14-day trial
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 ,
name , email , pin_hash , auth_type , role , tier ,
email_verified , verification_token , verification_expires ,
email_verified , verification_token , verification_expires ,
trial_ends_at , created_at
trial_ends_at , created_at
) VALUES ( % s , % s , % s , ' email ' , ' trainer ' , ' free ' , FALSE , % s , % s , % s , CURRENT_TIMESTAMP )
) VALUES ( % s , % s , % s , ' email ' , % s , ' free ' , FALSE , % s , % s , % s , CURRENT_TIMESTAMP )
""" , (name, email, pin_hash, verification_token, verification_expires, trial_ends))
""" , (name, email, pin_hash, role, verification_token, verification_expires, trial_ends))
# Send verification email
verify_url = verification_link ( verification_token )
app_url = os . getenv ( " APP_URL " , " https://mitai.jinkendo.de " )
verify_url = f " { app_url } /verify?token= { verification_token } "
email_body = f """ Hallo { name } ,
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 }
{ verify_url }
Der Link ist 24 Stunden gültig .
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 {
return {
" ok " : True ,
" ok " : True ,
@ -366,34 +409,22 @@ async def resend_verification(req: dict, request: Request):
WHERE id = % s
WHERE id = % s
""" , (verification_token, verification_expires, prof[ ' id ' ]))
""" , (verification_token, verification_expires, prof[ ' id ' ]))
# Send verification email
verify_url = verification_link ( verification_token )
app_url = os . getenv ( " APP_URL " , " https://mitai.jinkendo.de " )
verify_url = f " { app_url } /verify?token= { verification_token } "
email_body = f """ Hallo { prof [ ' name ' ] } ,
email_body = f """ Hallo { prof [ ' name ' ] } ,
du hast eine neue Bestätigungs - E - Mail angefordert .
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 }
{ verify_url }
Dieser Link ist 24 Stunden gültig .
Dieser Link ist 24 Stunden gültig .
Falls du diese E - Mail nicht angefordert hast , kannst du sie einfach ignorieren .
Dein Shinkan Jinkendo Team
Viele Grüße
Dein Mitai Jinkendo Team
"""
"""
try :
if not send_email ( email , " Shinkan Jinkendo – E-Mail bestätigen " , email_body ) :
send_email (
raise HTTPException ( 502 , " E-Mail konnte nicht versendet werden. SMTP-Einstellungen und Container-Logs prüfen. " )
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 " )
return { " ok " : True , " message " : " Bestätigungs-E-Mail wurde erneut versendet. " }
return { " ok " : True , " message " : " Bestätigungs-E-Mail wurde erneut versendet. " }