Ë
    |v…jÂ   ã                   ó®  — d Z ddlZddlZddlZddlZddlZddlZddlmZmZm	Z	 ddl
mZ  ee«      j                  dz  Zej                  d¬«       edz  Zd	Zd
Z ej&                  d«      ZdZ ej,                  «       Zdej0                  fd„Zd„ Z e«        dedefd„Zdededefd„Zdedefd„Zdededz  fd„Z dejB                  de"fd„Z#defd„Z$de%fd„Z&dede"dz  fd„Z'de%de"dz  fd„Z(de)e"   fd„Z*dededede"fd „Z+dedede"dz  fd!„Z,de%fd"„Z-de%defd#„Z.de%d$efd%„Z/ded&ed$edefd'„Z0dedefd(„Z1d)edz  de"dz  fd*„Z2d)edz  fd+„Z3y),u  
Multi-user authentication, SQLite-backed.

- `users` table: email/password_hash/role. role is 'admin' or 'member'.
  Admins can manage other users; members can use the cleaning dashboard but
  not the user-management screen.
- `sessions` table: opaque token -> email + expiry. Sessions are looked up
  fresh against `users` on every request, so a role change (or account
  deletion) takes effect immediately without waiting for re-login.
- Passwords: PBKDF2-HMAC-SHA256, stdlib only.
- A short-lived sqlite3 connection is opened per call rather than shared
  across threads â€” simplest safe pattern for a small self-hosted app; if
  this app grows into something with real concurrent write load, swap for
  a connection pool (or Postgres) using the same function signatures here.
é    N)ÚdatetimeÚ	timedeltaÚtimezone)ÚPathÚdataT)Úexist_okzapp.dbé¨   i ÷ z^[^@\s]+@[^@\s]+\.[^@\s]+$)ÚadminÚmemberÚreturnc                  ó„   — t        j                  t        «      } t         j                  | _        | j                  d«       | S )NzPRAGMA foreign_keys = ON)Úsqlite3ÚconnectÚDB_PATHÚRowÚrow_factoryÚexecute©Úconns    ú*/var/www/html/lead-cleaner/backend/auth.pyÚ_connectr   &   s.   € Ü�?‰?œ7Ó#€DÜ—{‘{€DÔØ‡L�LÐ+Ô,Ø€Kó    c                  óÜ   — t         5  t        «       5 } | j                  d«       | j                  d«       | j                  «        d d d «       d d d «       y # 1 sw Y   ŒxY w# 1 sw Y   y xY w)NaD  
            CREATE TABLE IF NOT EXISTS users (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                email TEXT UNIQUE NOT NULL,
                password_hash TEXT NOT NULL,
                role TEXT NOT NULL CHECK(role IN ('admin','member')),
                created_at TEXT NOT NULL
            )
        z¿
            CREATE TABLE IF NOT EXISTS sessions (
                token TEXT PRIMARY KEY,
                email TEXT NOT NULL,
                expires_at TEXT NOT NULL
            )
        ©Ú_write_lockr   r   Úcommitr   s    r   Úinit_dbr   -   sM   € Þ	”h”j DØ�‰ð ô 	ð 	�‰ð ô 	ð 	�‰Œ÷# !�ˆ—j�jú�ˆús"   ‡A"’3AÁA"ÁA	ÁA"Á"A+Úpasswordc                 óÒ   — t        j                  d«      }t        j                  d| j	                  d«      t
        j                  |«      t        «      }|› d|j                  «       › �S )Né   Úsha256úutf-8Ú$)	ÚsecretsÚ	token_hexÚhashlibÚpbkdf2_hmacÚencodeÚbytesÚfromhexÚPBKDF2_ITERATIONSÚhex)r   ÚsaltÚdks      r   Úhash_passwordr/   I   sR   € Ü×Ñ˜RÓ €DÜ	×	Ñ	˜X x§¡°wÓ'?ÄÇÁÈtÓATÔVgÓ	h€BØˆV�1�R—V‘V“X�JÐÐr   Ústoredc                 ó  — 	 |j                  dd«      \  }}t        j                  d| j	                  d«      t
        j                  |«      t        «      }t        j                  |j                  «       |«      S # t        $ r Y yw xY w)Nr#   é   Fr!   r"   )ÚsplitÚ
ValueErrorr&   r'   r(   r)   r*   r+   ÚhmacÚcompare_digestr,   )r   r0   r-   Úhash_hexr.   s        r   Úverify_passwordr8   O   ss   € ðØŸ™ c¨1Ó-‰ˆˆhô 
×	Ñ	˜X x§¡°wÓ'?ÄÇÁÈtÓATÔVgÓ	h€BÜ×Ñ˜rŸv™v›x¨Ó2Ð2øô ò Ùðús   ‚A9 Á9	BÂBÚemailc                 óF   — t        t        j                  | xs d«      «      S )NÚ )ÚboolÚEMAIL_REÚmatch)r9   s    r   Úvalidate_emailr?   X   s   € Ü”—‘˜uš{¨Ó+Ó,Ð,r   c                 ó&   — | rt        | «      dk  ryy )Né   z&Password must be at least 8 characters)Úlen)r   s    r   Úvalidate_password_strengthrC   \   s   € Ù”s˜8“} qÒ(Ø7Ør   Úrowc                 ó(   — | d   | d   | d   | d   dœS )NÚidr9   ÚroleÚ
created_at©rF   r9   rG   rH   © )rD   s    r   Ú_row_to_userrK   f   s$   € Ø�d‘) c¨'¡l¸CÀ¹KÐWZÐ[gÑWhÑiÐir   c                  ó„   — t        «       5 } | j                  d«      j                  «       d ucd d d «       S # 1 sw Y   y xY w)NzSELECT 1 FROM users LIMIT 1©r   r   Úfetchoner   s    r   Úis_setup_completerO   j   s.   € Ü	Œ�tØ�|‰|Ð9Ó:×CÑCÓEÈTÐQ÷ 
�Šús   ‹!6¶?c                  ó†   — t        «       5 } | j                  d«      j                  «       d   cd d d «       S # 1 sw Y   y xY w)Nz2SELECT COUNT(*) AS n FROM users WHERE role='admin'ÚnrM   r   s    r   Úcount_adminsrR   o   s/   € Ü	Œ�tØ�|‰|ÐPÓQ×ZÑZÓ\Ð]`Ña÷ 
�Šús	   ‹"7·A c                 óÚ   — t        «       5 }|j                  d| j                  «       j                  «       f«      j	                  «       }|rt        |«      nd cd d d «       S # 1 sw Y   y xY w)Nú!SELECT * FROM users WHERE email=?)r   r   ÚstripÚlowerrN   rK   )r9   r   rD   s      r   Úget_user_by_emailrW   t   sK   € Ü	Œ�tØ�l‰lÐ>ÀÇÁÃ×ATÑATÓAVÐ@XÓY×bÑbÓdˆÙ$'Œ|˜CÔ ¨T÷ 
�Šús   ‹AA!Á!A*Úuser_idc                 ó¢   — t        «       5 }|j                  d| f«      j                  «       }|rt        |«      nd cd d d «       S # 1 sw Y   y xY w)NzSELECT * FROM users WHERE id=?)r   r   rN   rK   )rX   r   rD   s      r   Úget_user_by_idrZ   z   s;   € Ü	Œ�tØ�l‰lÐ;¸g¸ZÓH×QÑQÓSˆÙ$'Œ|˜CÔ ¨T÷ 
�Šús   ‹0AÁAc                  ó¼   — t        «       5 } | j                  d«      j                  «       }|D �cg c]  }t        |«      ‘Œ c}cd d d «       S c c}w # 1 sw Y   y xY w)Nz+SELECT * FROM users ORDER BY created_at ASC)r   r   ÚfetchallrK   )r   ÚrowsÚrs      r   Ú
list_usersr_   €   sI   € Ü	Œ�tØ�|‰|ÐIÓJ×SÑSÓUˆÙ)-Ó.© A”˜Q•¨Ñ.÷ 
‰ùâ.÷ 
ˆús   ‹$A¯AÁAÁAÁArG   c           	      ó:  — | j                  «       j                  «       } |t        vrt        dt        › �«      ‚t        5  t        «       5 }|j                  d| f«      j                  «       }|rt        d«      ‚t        j                  t        j                  «      j                  «       }|j                  d| t        |«      ||f«      }|j                  «        |j                  | ||dœcd d d «       cd d d «       S # 1 sw Y   nxY wd d d «       y # 1 sw Y   y xY w)Núrole must be one of z!SELECT 1 FROM users WHERE email=?z%A user with that email already existszKINSERT INTO users (email, password_hash, role, created_at) VALUES (?,?,?,?)rI   )rU   rV   ÚVALID_ROLESr4   r   r   r   rN   r   Únowr   ÚutcÚ	isoformatr/   r   Ú	lastrowid)r9   r   rG   r   Úexistingrc   Úcurs          r   Úcreate_userri   †   sÕ   € Ø�K‰K‹M×ÑÓ!€EØ”;ÑÜÐ/´¨}Ð=Ó>Ð>Þ	”h”j DØ—<‘<Ð CÀeÀXÓN×WÑWÓYˆÙÜÐDÓEÐEÜ�l‰lœ8Ÿ<™<Ó(×2Ñ2Ó4ˆØ�l‰lØYØ”M (Ó+¨T°3Ð7ó
ˆð 	�‰ŒØ—m‘m¨e¸TÐQTÑU÷ !�j�‰—j�jú��‰ús$   ¿DÁ
BC<Ã)	DÃ<D	ÄDÄDc                 ó  — t        «       5 }|j                  d| xs dj                  «       j                  «       f«      j	                  «       }|rt        ||d   «      s
	 d d d «       y t        |«      cd d d «       S # 1 sw Y   y xY w)NrT   r;   Úpassword_hash)r   r   rU   rV   rN   r8   rK   )r9   r   r   rD   s       r   Úauthenticaterl   —   sk   € Ü	Œ�tØ�l‰lÐ>À%Â+È2×ATÑATÓAV×A\ÑA\ÓA^Ð@`Óa×jÑjÓlˆÙœ/¨(°C¸Ñ4HÔIØ÷ 
ˆô ˜CÓ ÷	 
�Šús   ‹AA<Á(
A<Á<Bc                 ó¾   — t         5  t        «       5 }|j                  d| f«       |j                  «        d d d «       d d d «       y # 1 sw Y   ŒxY w# 1 sw Y   y xY w)NzDELETE FROM users WHERE id=?r   )rX   r   s     r   Údelete_userrn   Ÿ   s8   € Þ	”h”j DØ�‰Ð3°g°ZÔ@Ø�‰Œ÷ !�ˆ—j�jú�ˆús!   ‡A’$A¶AÁA	ÁAÁAc                 óô   — |t         vrt        dt         › �«      ‚t        5  t        «       5 }|j	                  d|| f«       |j                  «        d d d «       d d d «       y # 1 sw Y   ŒxY w# 1 sw Y   y xY w)Nra   z"UPDATE users SET role=? WHERE id=?)rb   r4   r   r   r   r   )rX   rG   r   s      r   Úupdate_user_rolerp   ¥   sU   € Ø”;ÑÜÐ/´¨}Ð=Ó>Ð>Þ	”h”j DØ�‰Ð9¸DÀ'¸?ÔKØ�‰Œ÷ !�ˆ—j�jú�ˆús"   ¡A.¬%A"ÁA.Á"A+	Á'A.Á.A7Únew_passwordc                 óÒ   — t         5  t        «       5 }|j                  dt        |«      | f«       |j	                  «        d d d «       d d d «       y # 1 sw Y   ŒxY w# 1 sw Y   y xY w)Nú+UPDATE users SET password_hash=? WHERE id=?)r   r   r   r/   r   )rX   rq   r   s      r   Úadmin_reset_passwordrt   ­   sE   € Þ	”h”j DØ�‰Ø9Ü˜<Ó(¨'Ð2ô	
ð 	�‰Œ÷ !�ˆ—j�jú�ˆús"   ‡A’.AÁ AÁA	ÁAÁA&Úcurrent_passwordc                 ó¬  — t         5  t        «       5 }|j                  d| j                  «       j	                  «       f«      j                  «       }|rt        ||d   «      s	 d d d «       d d d «       y|j                  dt        |«      |d   f«       |j                  «        	 d d d «       d d d «       y# 1 sw Y   nxY wd d d «       y # 1 sw Y   y xY w)NrT   rk   Frs   rF   T)	r   r   r   rU   rV   rN   r8   r/   r   )r9   ru   rq   r   rD   s        r   Úchange_own_passwordrw   ¶   s™   € Þ	”h”j DØ�l‰lÐ>ÀÇÁÃ×ATÑATÓAVÐ@XÓY×bÑbÓdˆÙœ/Ð*:¸CÀÑ<PÔQØ÷ !�ˆð 	�‰ÐBÄ]ÐS_ÓE`ÐbeÐfjÑbkÐDlÔmØ�‰ŒØ÷ !�ˆ—j�jú��‰ús/   ‡C
’AB5Á"C
Á31B5Â$C
Â5B>	Â:C
Ã
Cc                 ót  — t        j                  d«      }t        j                  t        j
                  «      t        t        ¬«      z   j                  «       }t        5  t        «       5 }|j                  d|| |f«       |j                  «        d d d «       d d d «       |S # 1 sw Y   ŒxY w# 1 sw Y   |S xY w)Né    )Úhoursz>INSERT INTO sessions (token, email, expires_at) VALUES (?,?,?))r$   Útoken_urlsafer   rc   r   rd   r   ÚSESSION_TTL_HOURSre   r   r   r   r   )r9   ÚtokenÚ
expires_atr   s       r   Úcreate_sessionr   Ä   sƒ   € Ü×!Ñ! "Ó%€EÜ—,‘,œxŸ|™|Ó,¬yÔ?PÔ/QÑQ×\Ñ\Ó^€JÞ	”h”j DØ�‰ÐUÐX]Ð_dÐfpÐWqÔrØ�‰Œ÷ !�ð €L÷ !�jú�ð €Lús$   ÁB-Á)&B!ÂB-Â!B*	Â&B-Â-B7r}   c                 óª  — | syt         5  t        «       5 }|j                  d| f«      j                  «       }|s	 ddd«       ddd«       yt	        j
                  |d   «      t	        j                  t        j                  «      k  r5|j                  d| f«       |j                  «        	 ddd«       ddd«       y|j                  d|d   f«      j                  «       }|s5|j                  d| f«       |j                  «        	 ddd«       ddd«       yt        |«      cddd«       cddd«       S # 1 sw Y   nxY wddd«       y# 1 sw Y   yxY w)u¶   Returns the current {id, email, role} for a valid, unexpired session
    â€” looked up fresh each call so role changes/account deletion apply
    immediately, not just on next login.Nz$SELECT * FROM sessions WHERE token=?r~   ú"DELETE FROM sessions WHERE token=?rT   r9   )r   r   r   rN   r   Úfromisoformatrc   r   rd   r   rK   )r}   r   rD   Úuser_rows       r   Úget_session_userr„   Í   s÷   € ñ ØÞ	”h”j DØ�l‰lÐAÀEÀ8ÓL×UÑUÓWˆÙØ÷ !�ˆô ×!Ñ! # lÑ"3Ó4´x·|±|ÄHÇLÁLÓ7QÒQØ�L‰LÐ=À¸xÔHØ�K‰KŒMØ÷ !�ˆð —<‘<Ð CÀcÈ'ÁlÀ_ÓU×^Ñ^Ó`ˆÙà�L‰LÐ=À¸xÔHØ�K‰KŒMØ÷ !�ˆô ˜HÓ%÷ !�j�‰—j�jú��‰úsG   ŠE	•%D4ºE	ÁA D4Â+E	Â<A
D4ÄE	Ä
D4Ä!	E	Ä4D=	Ä9E	Å	Ec                 óÄ   — | sy t         5  t        «       5 }|j                  d| f«       |j                  «        d d d «       d d d «       y # 1 sw Y   ŒxY w# 1 sw Y   y xY w)Nr�   r   )r}   r   s     r   Údelete_sessionr†   ä   s>   € ÙØÞ	”h”j DØ�‰Ð9¸E¸8ÔDØ�‰Œ÷ !�ˆ—j�jú�ˆús!   ŠA•$A
¹AÁ
A	ÁAÁA)4Ú__doc__r&   r5   Úrer$   r   Ú	threadingr   r   r   Úpathlibr   Ú__file__ÚparentÚDATA_DIRÚmkdirr   r|   r+   Úcompiler=   rb   ÚLockr   Ú
Connectionr   r   Ústrr/   r<   r8   r?   rC   r   ÚdictrK   rO   ÚintrR   rW   rZ   Úlistr_   ri   rl   rn   rp   rt   rw   r   r„   r†   rJ   r   r   Ú<module>r–      sB  ðñó  Û Û 	Û Û Û ß 2Ñ 2Ý á�‹>× Ñ  6Ñ)€Ø ‡�˜€Ô Ø
�XÑ
€àÐ ØÐ Øˆ2�:‰:Ð3Ó4€Ø!€àˆi�n‰nÓ€ð�'×$Ñ$ó òñ* „	ð ˜Cð   Có  ð3˜cð 3¨3ð 3°4ó 3ð-˜#ð - $ó -ð¨ð °°t±ó ðj�g—k‘kð j dó jðR˜4ó Rð
b�có bð
2˜Sð 2 T¨D¡[ó 2ð2˜Cð 2 D¨4¡Kó 2ð/�D˜‘Jó /ðV�sð V cð V°ð V¸ó Vð"!˜ð ! sð !¨t°d©{ó !ð˜ó ð˜cð ¨ó ð #ð °Só ð˜sð °cð Èð ÐQUó ð˜#ð  #ó ð&˜C $™Jð &¨4°$©;ó &ð.˜# ™*ô r   