Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions Backend/app/database.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,14 @@ def init_db(app):

use_cloud_sql = os.getenv("USE_CLOUD_SQL", "true").lower() == "true"

print("\n" + "="*60)
if use_cloud_sql:
instance_name = os.getenv("INSTANCE_CONNECTION_NAME", "N/A")
db_name = os.getenv("DB_NAME", "N/A")
print("🌐 DATABASE: Google Cloud SQL (MySQL)")
print(f" Instance: {instance_name}")
print(f" Database: {db_name}")

engine = connect_with_connector()
app.config["SQLALCHEMY_DATABASE_URI"] = "mysql+pymysql://"
app.config["SQLALCHEMY_ENGINE_OPTIONS"] = {"creator": engine.raw_connection}
Expand All @@ -65,7 +72,12 @@ def init_db(app):
if not os.path.isabs(db_path):
db_path = os.path.abspath(db_path)
os.makedirs(os.path.dirname(db_path), exist_ok=True)
print("💾 DATABASE: Local SQLite")
print(f" Path: {db_path}")

app.config["SQLALCHEMY_DATABASE_URI"] = f"sqlite:///{db_path}"

print("="*60 + "\n")

app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
db.init_app(app)
Expand Down
13 changes: 0 additions & 13 deletions Backend/app/schema.sql

This file was deleted.

1 change: 1 addition & 0 deletions Backend/app/tabs/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# app/tabs/__init__.py
from app.tabs.model import Tab
40 changes: 39 additions & 1 deletion Backend/app/tabs/model.py
Original file line number Diff line number Diff line change
@@ -1 +1,39 @@
# app/tabs/model.py
import uuid
from typing import Optional

from sqlalchemy import String, Integer, ForeignKey, JSON
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.database import db


class Tab(db.Model):
"""Tab model using SQLAlchemy ORM"""
__tablename__ = 'tabs'

# Primary key
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)

# Foreign key
uid: Mapped[str] = mapped_column(String(36), ForeignKey('users.uid'), nullable=False, index=True)

# Tab fields
tab_name: Mapped[str] = mapped_column(String(100), nullable=False)
map: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) # JSON field for map data
pin: Mapped[Optional[dict]] = mapped_column(JSON, nullable=True) # JSON field for pin data

# Relationship
user: Mapped["User"] = relationship("User", back_populates="tabs")

def __repr__(self) -> str:
return f"<Tab(id={self.id}, uid='{self.uid}', tab_name='{self.tab_name}')>"

def to_dict(self) -> dict:
"""Convert model to dictionary"""
return {
'id': self.id,
'uid': self.uid,
'tab_name': self.tab_name,
'map': self.map,
'pin': self.pin,
}
37 changes: 36 additions & 1 deletion Backend/app/tabs/schema.py
Original file line number Diff line number Diff line change
@@ -1 +1,36 @@
# app/tabs/schema.py
from typing import Optional, Dict, Any
from pydantic import BaseModel


class TabBase(BaseModel):
"""Base tab schema"""
tab_name: str
map: Optional[Dict[str, Any]] = None
pin: Optional[Dict[str, Any]] = None


class TabCreate(TabBase):
"""Schema for creating a tab"""
pass


class TabUpdate(BaseModel):
"""Schema for updating a tab (all fields optional)"""
tab_name: Optional[str] = None
map: Optional[Dict[str, Any]] = None
pin: Optional[Dict[str, Any]] = None


class TabResponse(TabBase):
"""Schema for tab response"""
id: int
uid: str

class Config:
from_attributes = True


class TabListResponse(BaseModel):
"""Schema for list of tabs"""
tabs: list[TabResponse]
total: int
1 change: 1 addition & 0 deletions Backend/app/user/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# User module
from app.user.model import User, UserProfile
43 changes: 22 additions & 21 deletions Backend/app/user/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,37 @@ def get(self):



@api.route('/users/<string:user_id>')
@api.route('/users/<string:uid>')
class UserApi(Resource):
def put(self, user_id):
def put(self, uid):
"""Update user by ID"""
data = request.get_json()

username = data.get('username')
name = data.get('name')
email = data.get('email')
password = data.get('password')

try:
user = user_service.update_user(user_id, username, email, password)
user = user_service.update_user(uid, name, email, password)
if not user:
api.abort(404, f'User {user_id} not found')
api.abort(404, f'User {uid} not found')

return {
'success': True,
'message': 'User updated successfully',
'user': to_dict(user)
}
except IntegrityError as e:
if 'UNIQUE constraint failed: users.email' in str(e):
if 'UNIQUE constraint failed: users.email' in str(e) or 'Duplicate entry' in str(e):
api.abort(400, 'Email already exists')
elif 'UNIQUE constraint failed: users.username' in str(e):
api.abort(400, 'Username already exists')
else:
api.abort(400, 'Database constraint violation')

def delete(self, user_id):
def delete(self, uid):
"""Delete user by ID"""
success = user_service.delete_user(user_id)
success = user_service.delete_user(uid)
if not success:
api.abort(404, f'User {user_id} not found')
api.abort(404, f'User {uid} not found')

return {
'success': True,
Expand All @@ -65,23 +63,24 @@ def post(self):
"""Create a new user"""
data = request.get_json()

username = data.get('username')
name = data.get('name')
email = data.get('email')
password = data.get('password')

if not name or not email or not password:
api.abort(400, 'Name, email and password are required')

try:
user = user_service.create_user(username, email, password)
user = user_service.create_user(name, email, password)
return {
'success': True,
'message': 'User created successfully',
'user': to_dict(user)
}, 201
except IntegrityError as e:
# handle unique constraint error
if 'UNIQUE constraint failed: users.email' in str(e):
if 'UNIQUE constraint failed: users.email' in str(e) or 'Duplicate entry' in str(e):
api.abort(400, 'Email already exists')
elif 'UNIQUE constraint failed: users.username' in str(e):
api.abort(400, 'Username already exists')
else:
api.abort(400, 'Database constraint violation')

Expand All @@ -92,11 +91,13 @@ def post(self):
"""User login"""
data = request.get_json()

username = data.get('username')
email = data.get('email')
password = data.get('password')

user = user_service.authenticate_user(username, email, password)
if not email or not password:
api.abort(400, 'Email and password are required')

user = user_service.authenticate_user(email, password)
if not user:
api.abort(401, 'Invalid credentials')

Expand All @@ -123,7 +124,7 @@ def get(self):
"""Get current user profile (dummy)"""
return {
'user': {
'username': 'dummy_user',
'name': 'dummy_user',
'email': '[email protected]'
}
}
Expand All @@ -134,7 +135,7 @@ class MySettingApi(Resource):
def get(self):
"""Get current user's setting (dummy)"""
return {
'user_id': 'me',
'uid': 'me',
'settings': {
'theme': 'light',
'language': 'en-US'
Expand All @@ -146,7 +147,7 @@ def put(self):
data = request.get_json() or {}
return {
'success': True,
'user_id': 'me',
'uid': 'me',
'updated_settings': data
}

Expand Down
59 changes: 43 additions & 16 deletions Backend/app/user/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
from datetime import datetime
from typing import Optional

from sqlalchemy import String, DateTime
from sqlalchemy.orm import Mapped, mapped_column
from sqlalchemy import String, DateTime, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.database import db

Expand All @@ -14,34 +14,61 @@ class User(db.Model):
__tablename__ = 'users'

# Primary key
user_id: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))
uid: Mapped[str] = mapped_column(String(36), primary_key=True, default=lambda: str(uuid.uuid4()))

# User fields
username: Mapped[str] = mapped_column(String(50), unique=True, nullable=False, index=True)
name: Mapped[str] = mapped_column(String(100), nullable=False)
email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True)
password: Mapped[str] = mapped_column(String(255), nullable=False)
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow, nullable=False)

# Relationships
profile: Mapped[Optional["UserProfile"]] = relationship("UserProfile", back_populates="user", uselist=False, cascade="all, delete-orphan")
tabs: Mapped[list["Tab"]] = relationship("Tab", back_populates="user", cascade="all, delete-orphan")

def __repr__(self) -> str:
return f"<User(user_id='{self.user_id}', username='{self.username}', email='{self.email}')>"
return f"<User(uid='{self.uid}', name='{self.name}', email='{self.email}')>"

def set_password(self, password: str) -> None:
"""Hash and set password"""
# salt = bcrypt.gensalt()
# self.password = bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')
self.password = password
salt = bcrypt.gensalt()
self.password = bcrypt.hashpw(password.encode('utf-8'), salt).decode('utf-8')

def check_password(self, password: str) -> bool:
"""Check if provided password matches stored hash"""
# return bcrypt.checkpw(password.encode('utf-8'), self.password.encode('utf-8'))
return self.password == password
return bcrypt.checkpw(password.encode('utf-8'), self.password.encode('utf-8'))

def to_dict(self) -> dict:
"""Convert model to dictionary"""
"""Convert model to dictionary (exclude password)"""
return {
'user_id': self.user_id,
'username': self.username,
'uid': self.uid,
'name': self.name,
'email': self.email,
'password': self.password, # WARNING: don't return password in production
'created_at': self.created_at.isoformat() if self.created_at else None
}


class UserProfile(db.Model):
"""User profile model"""
__tablename__ = 'user_profiles'

# Primary key
uid: Mapped[str] = mapped_column(String(36), ForeignKey('users.uid'), primary_key=True)

# Profile fields
color: Mapped[Optional[str]] = mapped_column(String(1000), nullable=True) # JSON string of list
favour_tabs: Mapped[Optional[str]] = mapped_column(String(100), nullable=True) # tab id reference
pic: Mapped[Optional[str]] = mapped_column(String(500), nullable=True) # picture URL or path

# Relationship
user: Mapped["User"] = relationship("User", back_populates="profile")

def __repr__(self) -> str:
return f"<UserProfile(uid='{self.uid}')>"

def to_dict(self) -> dict:
"""Convert model to dictionary"""
return {
'uid': self.uid,
'color': self.color,
'favour_tabs': self.favour_tabs,
'pic': self.pic,
}
Loading