Spaces:
Running
Running
Navid Arabi
commited on
Commit
·
bc1cd44
1
Parent(s):
0fb23b8
base app
Browse files- app.py +2 -0
- components/header.py +1 -1
- components/login_page.py +7 -6
- create_new_user.py +23 -0
- data/models.py +96 -15
- data/repository/annotator_repo.py +80 -0
- requirements.txt +3 -1
- utils/auth.py +45 -22
- utils/database.py +2 -0
- utils/security.py +29 -0
app.py
CHANGED
@@ -5,9 +5,11 @@ from pathlib import Path
|
|
5 |
from utils.logger import Logger
|
6 |
from components.login_page import LoginPage
|
7 |
from components.dashboard_page import DashboardPage
|
|
|
8 |
from config import conf
|
9 |
|
10 |
log = Logger()
|
|
|
11 |
CSS_FILE = Path(__file__).parent / "assets" / "styles.css"
|
12 |
custom_css = CSS_FILE.read_text(encoding="utf-8")
|
13 |
|
|
|
5 |
from utils.logger import Logger
|
6 |
from components.login_page import LoginPage
|
7 |
from components.dashboard_page import DashboardPage
|
8 |
+
from utils.database import initialize_database
|
9 |
from config import conf
|
10 |
|
11 |
log = Logger()
|
12 |
+
initialize_database()
|
13 |
CSS_FILE = Path(__file__).parent / "assets" / "styles.css"
|
14 |
custom_css = CSS_FILE.read_text(encoding="utf-8")
|
15 |
|
components/header.py
CHANGED
@@ -8,7 +8,6 @@ class Header:
|
|
8 |
def __init__(self):
|
9 |
with gr.Row(variant="panel", elem_classes="header-row") as self.container:
|
10 |
self.welcome = gr.Markdown()
|
11 |
-
|
12 |
self.logout_btn = gr.Button("Log out", scale=0, min_width=90)
|
13 |
|
14 |
# ---------------- wiring ----------------
|
@@ -19,6 +18,7 @@ class Header:
|
|
19 |
outputs=[
|
20 |
login_page.container,
|
21 |
dashboard_page.container,
|
|
|
22 |
login_page.message,
|
23 |
],
|
24 |
)
|
|
|
8 |
def __init__(self):
|
9 |
with gr.Row(variant="panel", elem_classes="header-row") as self.container:
|
10 |
self.welcome = gr.Markdown()
|
|
|
11 |
self.logout_btn = gr.Button("Log out", scale=0, min_width=90)
|
12 |
|
13 |
# ---------------- wiring ----------------
|
|
|
18 |
outputs=[
|
19 |
login_page.container,
|
20 |
dashboard_page.container,
|
21 |
+
self.welcome,
|
22 |
login_page.message,
|
23 |
],
|
24 |
)
|
components/login_page.py
CHANGED
@@ -31,10 +31,11 @@ class LoginPage:
|
|
31 |
self.login_btn.click(
|
32 |
fn=AuthService.login,
|
33 |
inputs=[self.username, self.password, session_state],
|
34 |
-
outputs=[
|
|
|
|
|
|
|
|
|
|
|
35 |
concurrency_limit=10,
|
36 |
-
)
|
37 |
-
lambda s: f"### User: {s.get('user', '')}",
|
38 |
-
inputs=session_state,
|
39 |
-
outputs=header.welcome,
|
40 |
-
)
|
|
|
31 |
self.login_btn.click(
|
32 |
fn=AuthService.login,
|
33 |
inputs=[self.username, self.password, session_state],
|
34 |
+
outputs=[
|
35 |
+
self.message, # پیام خطا یا None
|
36 |
+
self.container, # فرم لاگین (hide/show)
|
37 |
+
dashboard_page.container,# داشبورد (show/hide)
|
38 |
+
header.welcome, # متن هدر ← NEW
|
39 |
+
],
|
40 |
concurrency_limit=10,
|
41 |
+
)
|
|
|
|
|
|
|
|
create_new_user.py
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from utils.database import get_db
|
2 |
+
from data.repository.annotator_repo import AnnotatorRepo
|
3 |
+
from utils.logger import Logger
|
4 |
+
|
5 |
+
log = Logger()
|
6 |
+
|
7 |
+
try:
|
8 |
+
with get_db() as session:
|
9 |
+
repo = AnnotatorRepo(session)
|
10 |
+
|
11 |
+
# ---------------------- Add new user ---------------------- #
|
12 |
+
new_user = repo.add_new_user("navid", "123")
|
13 |
+
log.info(
|
14 |
+
f"User created successfully (id={new_user.id}, username='{new_user.name}')"
|
15 |
+
)
|
16 |
+
|
17 |
+
new_user = repo.add_new_user("vargha", "111")
|
18 |
+
log.info(
|
19 |
+
f"User created successfully (id={new_user.id}, username='{new_user.name}')"
|
20 |
+
)
|
21 |
+
|
22 |
+
except Exception as exc:
|
23 |
+
log.error(f"An error occurred in user operations: {exc}")
|
data/models.py
CHANGED
@@ -1,38 +1,99 @@
|
|
1 |
-
from sqlalchemy import
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
2 |
from sqlalchemy.orm import relationship, declarative_base
|
3 |
|
4 |
Base = declarative_base()
|
5 |
|
6 |
-
|
|
|
|
|
7 |
class TTSData(Base):
|
8 |
__tablename__ = "tts_data"
|
9 |
|
10 |
id = Column(Integer, primary_key=True)
|
11 |
-
filename = Column(String, nullable=False, unique=True)
|
12 |
-
sentence = Column(
|
13 |
|
14 |
|
|
|
|
|
|
|
15 |
class Annotator(Base):
|
16 |
__tablename__ = "annotators"
|
17 |
|
18 |
id = Column(Integer, primary_key=True)
|
19 |
-
name = Column(String, nullable=False, unique=True)
|
20 |
-
password = Column(String, nullable=False)
|
21 |
last_login = Column(DateTime)
|
22 |
is_active = Column(Boolean, default=True)
|
23 |
-
|
24 |
-
|
25 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
26 |
|
27 |
|
|
|
|
|
|
|
28 |
class Validator(Base):
|
29 |
__tablename__ = "validators"
|
30 |
|
31 |
id = Column(Integer, primary_key=True)
|
32 |
-
|
33 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
34 |
|
35 |
|
|
|
|
|
|
|
36 |
class AnnotationInterval(Base):
|
37 |
__tablename__ = "annotation_intervals"
|
38 |
|
@@ -41,21 +102,33 @@ class AnnotationInterval(Base):
|
|
41 |
start_index = Column(Integer, nullable=True)
|
42 |
end_index = Column(Integer, nullable=True)
|
43 |
|
|
|
|
|
44 |
|
|
|
|
|
|
|
45 |
class Annotation(Base):
|
46 |
__tablename__ = "annotations"
|
47 |
|
48 |
id = Column(Integer, primary_key=True)
|
49 |
-
annotated_sentence = Column(
|
50 |
validated = Column(Boolean, nullable=False)
|
51 |
annotated_at = Column(DateTime, nullable=False)
|
52 |
annotator_id = Column(Integer, ForeignKey("annotators.id"))
|
53 |
-
|
|
|
|
|
|
|
|
|
54 |
validations = relationship(
|
55 |
-
"Validation", cascade="all, delete",
|
56 |
)
|
57 |
|
58 |
|
|
|
|
|
|
|
59 |
class AudioTrim(Base):
|
60 |
__tablename__ = "audio_trims"
|
61 |
|
@@ -64,7 +137,12 @@ class AudioTrim(Base):
|
|
64 |
end = Column(Float, nullable=False)
|
65 |
annotation_id = Column(Integer, ForeignKey("annotations.id"))
|
66 |
|
|
|
67 |
|
|
|
|
|
|
|
|
|
68 |
class Validation(Base):
|
69 |
__tablename__ = "validations"
|
70 |
|
@@ -72,5 +150,8 @@ class Validation(Base):
|
|
72 |
annotation_id = Column(Integer, ForeignKey("annotations.id"))
|
73 |
validator_id = Column(Integer, ForeignKey("validators.id"))
|
74 |
validated = Column(Boolean, nullable=False)
|
75 |
-
description = Column(
|
76 |
validated_at = Column(DateTime, nullable=False)
|
|
|
|
|
|
|
|
1 |
+
from sqlalchemy import (
|
2 |
+
Column,
|
3 |
+
Integer,
|
4 |
+
String,
|
5 |
+
Float,
|
6 |
+
Boolean,
|
7 |
+
DateTime,
|
8 |
+
ForeignKey,
|
9 |
+
Text,
|
10 |
+
)
|
11 |
from sqlalchemy.orm import relationship, declarative_base
|
12 |
|
13 |
Base = declarative_base()
|
14 |
|
15 |
+
# --------------------------------------------------------------------------- #
|
16 |
+
# TTSData #
|
17 |
+
# --------------------------------------------------------------------------- #
|
18 |
class TTSData(Base):
|
19 |
__tablename__ = "tts_data"
|
20 |
|
21 |
id = Column(Integer, primary_key=True)
|
22 |
+
filename = Column(String(255), nullable=False, unique=True)
|
23 |
+
sentence = Column(Text, nullable=False)
|
24 |
|
25 |
|
26 |
+
# --------------------------------------------------------------------------- #
|
27 |
+
# Annotator #
|
28 |
+
# --------------------------------------------------------------------------- #
|
29 |
class Annotator(Base):
|
30 |
__tablename__ = "annotators"
|
31 |
|
32 |
id = Column(Integer, primary_key=True)
|
33 |
+
name = Column(String(100), nullable=False, unique=True)
|
34 |
+
password = Column(String(255), nullable=False)
|
35 |
last_login = Column(DateTime)
|
36 |
is_active = Column(Boolean, default=True)
|
37 |
+
|
38 |
+
# رابطه با Annotation
|
39 |
+
annotations = relationship(
|
40 |
+
"Annotation",
|
41 |
+
back_populates="annotator",
|
42 |
+
cascade="all, delete-orphan",
|
43 |
+
)
|
44 |
+
|
45 |
+
# رابطه با Validator ـ نقش «کسی که کارش ارزیابی میشود»
|
46 |
+
validation_records = relationship(
|
47 |
+
"Validator",
|
48 |
+
back_populates="annotated_annotator",
|
49 |
+
foreign_keys="Validator.annotator_id",
|
50 |
+
cascade="all, delete-orphan",
|
51 |
+
)
|
52 |
+
|
53 |
+
# رابطه با Validator ـ نقش «کسی که اعتبارسنجی میکند»
|
54 |
+
validations_done = relationship(
|
55 |
+
"Validator",
|
56 |
+
back_populates="validator_user",
|
57 |
+
foreign_keys="Validator.validator_id",
|
58 |
+
cascade="all, delete-orphan",
|
59 |
+
)
|
60 |
+
|
61 |
+
# بازههای واگذاری کار
|
62 |
+
annotation_intervals = relationship(
|
63 |
+
"AnnotationInterval",
|
64 |
+
back_populates="annotator",
|
65 |
+
cascade="all, delete-orphan",
|
66 |
+
)
|
67 |
|
68 |
|
69 |
+
# --------------------------------------------------------------------------- #
|
70 |
+
# Validator #
|
71 |
+
# --------------------------------------------------------------------------- #
|
72 |
class Validator(Base):
|
73 |
__tablename__ = "validators"
|
74 |
|
75 |
id = Column(Integer, primary_key=True)
|
76 |
+
# کسی که جمله را حاشیهنویسی کرده
|
77 |
+
annotator_id = Column(Integer, ForeignKey("annotators.id"), nullable=False)
|
78 |
+
# کسی که آن را اعتبارسنجی کرده
|
79 |
+
validator_id = Column(Integer, ForeignKey("annotators.id"), nullable=False)
|
80 |
+
|
81 |
+
# رابطهها
|
82 |
+
annotated_annotator = relationship(
|
83 |
+
"Annotator",
|
84 |
+
foreign_keys=[annotator_id],
|
85 |
+
back_populates="validation_records",
|
86 |
+
)
|
87 |
+
validator_user = relationship(
|
88 |
+
"Annotator",
|
89 |
+
foreign_keys=[validator_id],
|
90 |
+
back_populates="validations_done",
|
91 |
+
)
|
92 |
|
93 |
|
94 |
+
# --------------------------------------------------------------------------- #
|
95 |
+
# AnnotationInterval #
|
96 |
+
# --------------------------------------------------------------------------- #
|
97 |
class AnnotationInterval(Base):
|
98 |
__tablename__ = "annotation_intervals"
|
99 |
|
|
|
102 |
start_index = Column(Integer, nullable=True)
|
103 |
end_index = Column(Integer, nullable=True)
|
104 |
|
105 |
+
annotator = relationship("Annotator", back_populates="annotation_intervals")
|
106 |
+
|
107 |
|
108 |
+
# --------------------------------------------------------------------------- #
|
109 |
+
# Annotation #
|
110 |
+
# --------------------------------------------------------------------------- #
|
111 |
class Annotation(Base):
|
112 |
__tablename__ = "annotations"
|
113 |
|
114 |
id = Column(Integer, primary_key=True)
|
115 |
+
annotated_sentence = Column(Text, nullable=False)
|
116 |
validated = Column(Boolean, nullable=False)
|
117 |
annotated_at = Column(DateTime, nullable=False)
|
118 |
annotator_id = Column(Integer, ForeignKey("annotators.id"))
|
119 |
+
|
120 |
+
annotator = relationship("Annotator", back_populates="annotations")
|
121 |
+
audio_trims = relationship(
|
122 |
+
"AudioTrim", cascade="all, delete-orphan", back_populates="annotation"
|
123 |
+
)
|
124 |
validations = relationship(
|
125 |
+
"Validation", cascade="all, delete-orphan", back_populates="annotation"
|
126 |
)
|
127 |
|
128 |
|
129 |
+
# --------------------------------------------------------------------------- #
|
130 |
+
# AudioTrim #
|
131 |
+
# --------------------------------------------------------------------------- #
|
132 |
class AudioTrim(Base):
|
133 |
__tablename__ = "audio_trims"
|
134 |
|
|
|
137 |
end = Column(Float, nullable=False)
|
138 |
annotation_id = Column(Integer, ForeignKey("annotations.id"))
|
139 |
|
140 |
+
annotation = relationship("Annotation", back_populates="audio_trims")
|
141 |
|
142 |
+
|
143 |
+
# --------------------------------------------------------------------------- #
|
144 |
+
# Validation #
|
145 |
+
# --------------------------------------------------------------------------- #
|
146 |
class Validation(Base):
|
147 |
__tablename__ = "validations"
|
148 |
|
|
|
150 |
annotation_id = Column(Integer, ForeignKey("annotations.id"))
|
151 |
validator_id = Column(Integer, ForeignKey("validators.id"))
|
152 |
validated = Column(Boolean, nullable=False)
|
153 |
+
description = Column(Text, nullable=True)
|
154 |
validated_at = Column(DateTime, nullable=False)
|
155 |
+
|
156 |
+
annotation = relationship("Annotation", back_populates="validations")
|
157 |
+
validator = relationship("Validator")
|
data/repository/annotator_repo.py
ADDED
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from typing import Optional
|
2 |
+
from sqlalchemy.orm import Session
|
3 |
+
|
4 |
+
from data.models import Annotator
|
5 |
+
from utils.logger import Logger
|
6 |
+
from utils.security import hash_password
|
7 |
+
|
8 |
+
log = Logger()
|
9 |
+
|
10 |
+
|
11 |
+
class AnnotatorRepo:
|
12 |
+
"""
|
13 |
+
Data-Access-Layer for Annotator table.
|
14 |
+
"""
|
15 |
+
|
16 |
+
def __init__(self, db: Session) -> None:
|
17 |
+
self.db = db
|
18 |
+
|
19 |
+
# ------------------------------------------------------------------ #
|
20 |
+
# READ METHODS
|
21 |
+
# ------------------------------------------------------------------ #
|
22 |
+
def get_user_by_username(self, username: str) -> Optional[Annotator]:
|
23 |
+
try:
|
24 |
+
return (
|
25 |
+
self.db.query(Annotator)
|
26 |
+
.filter(Annotator.name == username)
|
27 |
+
.first()
|
28 |
+
)
|
29 |
+
except Exception as exc:
|
30 |
+
log.error(f"Unable to fetch user <username={username}> : {exc}")
|
31 |
+
raise
|
32 |
+
|
33 |
+
def get_user_by_id(self, user_id: int) -> Optional[Annotator]:
|
34 |
+
try:
|
35 |
+
return (
|
36 |
+
self.db.query(Annotator)
|
37 |
+
.filter(Annotator.id == user_id)
|
38 |
+
.first()
|
39 |
+
)
|
40 |
+
except Exception as exc:
|
41 |
+
log.error(f"Unable to fetch user <id={user_id}> : {exc}")
|
42 |
+
raise
|
43 |
+
|
44 |
+
# ------------------------------------------------------------------ #
|
45 |
+
# WRITE METHODS
|
46 |
+
# ------------------------------------------------------------------ #
|
47 |
+
def add_new_user(
|
48 |
+
self,
|
49 |
+
username: str,
|
50 |
+
password: str,
|
51 |
+
*,
|
52 |
+
is_active: bool = True,
|
53 |
+
) -> Annotator:
|
54 |
+
"""
|
55 |
+
Create a new Annotator with a hashed password.
|
56 |
+
Raises:
|
57 |
+
ValueError: if username already exists.
|
58 |
+
"""
|
59 |
+
try:
|
60 |
+
if self.get_user_by_username(username):
|
61 |
+
raise ValueError(f"Username `{username}` already exists.")
|
62 |
+
|
63 |
+
# ------------------ HASH PASSWORD ------------------ #
|
64 |
+
hashed_pass = hash_password(password)
|
65 |
+
|
66 |
+
user = Annotator(
|
67 |
+
name=username,
|
68 |
+
password=hashed_pass,
|
69 |
+
is_active=is_active,
|
70 |
+
)
|
71 |
+
self.db.add(user)
|
72 |
+
self.db.flush() # Ensure PK generated
|
73 |
+
self.db.refresh(user)
|
74 |
+
|
75 |
+
log.info(f"New user created <id={user.id} username={username}>")
|
76 |
+
return user
|
77 |
+
except Exception as exc:
|
78 |
+
self.db.rollback()
|
79 |
+
log.error(f"Unable to create user `{username}` : {exc}")
|
80 |
+
raise
|
requirements.txt
CHANGED
@@ -4,4 +4,6 @@ pydantic
|
|
4 |
mysql-connector-python
|
5 |
soundfile
|
6 |
librosa
|
7 |
-
pydantic-settings
|
|
|
|
|
|
4 |
mysql-connector-python
|
5 |
soundfile
|
6 |
librosa
|
7 |
+
pydantic-settings
|
8 |
+
pymysql
|
9 |
+
bcrypt
|
utils/auth.py
CHANGED
@@ -1,42 +1,65 @@
|
|
1 |
-
# utils/auth.py
|
2 |
-
|
3 |
import gradio as gr
|
4 |
from utils.logger import Logger
|
|
|
|
|
|
|
5 |
|
6 |
log = Logger()
|
7 |
|
8 |
|
9 |
class AuthService:
|
10 |
-
|
|
|
|
|
11 |
|
12 |
-
#
|
13 |
@staticmethod
|
14 |
def login(username: str, password: str, session: dict):
|
15 |
log.info(f"Login attempt: username={username}")
|
16 |
-
|
17 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
18 |
return (
|
19 |
-
|
20 |
-
gr.update(),
|
21 |
-
gr.update(visible=
|
|
|
22 |
)
|
23 |
|
24 |
-
|
25 |
-
session["user"] = username
|
26 |
-
log.info(f"User '{username}' logged in successfully.")
|
27 |
-
return (
|
28 |
-
None,
|
29 |
-
gr.update(visible=False), # hide login form
|
30 |
-
gr.update(visible=True), # show dashboard
|
31 |
-
)
|
32 |
-
|
33 |
@staticmethod
|
34 |
def logout(session: dict):
|
35 |
-
username = session.get("
|
36 |
session.clear()
|
37 |
log.info(f"User '{username}' logged out.")
|
38 |
return (
|
39 |
-
gr.update(visible=True),
|
40 |
-
gr.update(visible=False),
|
41 |
-
gr.update(value=""),
|
|
|
42 |
)
|
|
|
|
|
|
|
1 |
import gradio as gr
|
2 |
from utils.logger import Logger
|
3 |
+
from utils.database import get_db
|
4 |
+
from data.repository.annotator_repo import AnnotatorRepo
|
5 |
+
from utils.security import verify_password
|
6 |
|
7 |
log = Logger()
|
8 |
|
9 |
|
10 |
class AuthService:
|
11 |
+
"""
|
12 |
+
Authenticate users against DB and drive Gradio UI states.
|
13 |
+
"""
|
14 |
|
15 |
+
# --------------- LOGIN --------------- #
|
16 |
@staticmethod
|
17 |
def login(username: str, password: str, session: dict):
|
18 |
log.info(f"Login attempt: username={username}")
|
19 |
+
|
20 |
+
with get_db() as db:
|
21 |
+
repo = AnnotatorRepo(db)
|
22 |
+
user = repo.get_user_by_username(username)
|
23 |
+
|
24 |
+
if user is None or not user.is_active:
|
25 |
+
log.warning(
|
26 |
+
f"Failed login for username='{username}' (not found / inactive)."
|
27 |
+
)
|
28 |
+
return (
|
29 |
+
"❌ Wrong username or password!",
|
30 |
+
gr.update(),
|
31 |
+
gr.update(visible=False),
|
32 |
+
gr.update(value=""),
|
33 |
+
)
|
34 |
+
|
35 |
+
if not verify_password(password, user.password):
|
36 |
+
log.warning(f"Failed login; bad password for '{username}'.")
|
37 |
+
return (
|
38 |
+
"❌ Wrong username or password!",
|
39 |
+
gr.update(),
|
40 |
+
gr.update(visible=False),
|
41 |
+
gr.update(value=""),
|
42 |
+
)
|
43 |
+
|
44 |
+
session["user_id"] = user.id
|
45 |
+
session["username"] = user.name
|
46 |
+
log.info(f"User '{username}' logged in successfully.")
|
47 |
return (
|
48 |
+
None,
|
49 |
+
gr.update(visible=False),
|
50 |
+
gr.update(visible=True),
|
51 |
+
gr.update(value=f"👋 Welcome, {user.name}!"),
|
52 |
)
|
53 |
|
54 |
+
# --------------- LOGOUT --------------- #
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
55 |
@staticmethod
|
56 |
def logout(session: dict):
|
57 |
+
username = session.get("username", "unknown")
|
58 |
session.clear()
|
59 |
log.info(f"User '{username}' logged out.")
|
60 |
return (
|
61 |
+
gr.update(visible=True),
|
62 |
+
gr.update(visible=False),
|
63 |
+
gr.update(value=""),
|
64 |
+
gr.update(value=""),
|
65 |
)
|
utils/database.py
CHANGED
@@ -1,3 +1,5 @@
|
|
|
|
|
|
1 |
from sqlalchemy import create_engine
|
2 |
from sqlalchemy.orm import sessionmaker
|
3 |
from contextlib import contextmanager
|
|
|
1 |
+
# utils/database.py
|
2 |
+
|
3 |
from sqlalchemy import create_engine
|
4 |
from sqlalchemy.orm import sessionmaker
|
5 |
from contextlib import contextmanager
|
utils/security.py
ADDED
@@ -0,0 +1,29 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import bcrypt
|
2 |
+
from utils.logger import Logger
|
3 |
+
|
4 |
+
log = Logger()
|
5 |
+
|
6 |
+
|
7 |
+
def hash_password(plain_password: str) -> str:
|
8 |
+
"""
|
9 |
+
Hash a plaintext password using bcrypt.
|
10 |
+
"""
|
11 |
+
try:
|
12 |
+
hashed = bcrypt.hashpw(plain_password.encode("utf-8"), bcrypt.gensalt())
|
13 |
+
return hashed.decode("utf-8")
|
14 |
+
except Exception as exc:
|
15 |
+
log.error(f"Password hashing failed: {exc}")
|
16 |
+
raise
|
17 |
+
|
18 |
+
|
19 |
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
20 |
+
"""
|
21 |
+
Verify a plaintext password against its bcrypt hash.
|
22 |
+
"""
|
23 |
+
try:
|
24 |
+
return bcrypt.checkpw(
|
25 |
+
plain_password.encode("utf-8"), hashed_password.encode("utf-8")
|
26 |
+
)
|
27 |
+
except Exception as exc:
|
28 |
+
log.error(f"Password verification failed: {exc}")
|
29 |
+
return False
|