Implement LinkedIn-based church management system with FastAPI
- Complete FastAPI application with authentication and JWT tokens - SQLite database with SQLAlchemy ORM and Alembic migrations - User management with profile features and search functionality - LinkedIn-style networking with connection requests and acceptance - Social features: posts, likes, comments, announcements, prayer requests - Event management with registration system and capacity limits - RESTful API endpoints for all features with proper authorization - Comprehensive documentation and setup instructions Key Features: - JWT-based authentication with bcrypt password hashing - User profiles with bio, position, contact information - Connection system for church member networking - Community feed with post interactions - Event creation, registration, and attendance tracking - Admin role-based permissions - Health check endpoint and API documentation Environment Variables Required: - SECRET_KEY: JWT secret key for token generation
This commit is contained in:
parent
a08e94c27e
commit
771ee5214f
186
README.md
186
README.md
@ -1,3 +1,185 @@
|
||||
# FastAPI Application
|
||||
# LinkedIn-Based Church Management System
|
||||
|
||||
This is a FastAPI application bootstrapped by BackendIM, the AI-powered backend generation platform.
|
||||
A comprehensive church management system built with FastAPI that incorporates LinkedIn-style networking features to help church members connect, share updates, and manage events.
|
||||
|
||||
## Features
|
||||
|
||||
### Authentication & User Management
|
||||
- User registration and login with JWT tokens
|
||||
- Profile management with bio, position, and contact information
|
||||
- User search functionality
|
||||
- Profile pictures and personal information
|
||||
|
||||
### LinkedIn-Style Networking
|
||||
- Send connection requests to other church members
|
||||
- Accept/reject connection requests
|
||||
- View connections and networking activity
|
||||
- Professional-style member profiles
|
||||
|
||||
### Social Features
|
||||
- Create and share posts (announcements, prayer requests, general updates)
|
||||
- Like and comment on posts
|
||||
- Community feed with chronological posts
|
||||
- Different post types (announcements, prayer requests)
|
||||
|
||||
### Event Management
|
||||
- Create and manage church events
|
||||
- Event registration system with capacity limits
|
||||
- Public/private event settings
|
||||
- Event attendance tracking
|
||||
- RSVP management
|
||||
|
||||
### Church Member Management
|
||||
- Member directory with search capabilities
|
||||
- Role-based permissions (admin/member)
|
||||
- Member profile management
|
||||
- Contact information management
|
||||
|
||||
## Tech Stack
|
||||
|
||||
- **Backend**: FastAPI (Python)
|
||||
- **Database**: SQLite with SQLAlchemy ORM
|
||||
- **Authentication**: JWT tokens with bcrypt password hashing
|
||||
- **Migrations**: Alembic
|
||||
- **Validation**: Pydantic
|
||||
- **Code Quality**: Ruff for linting
|
||||
|
||||
## Installation & Setup
|
||||
|
||||
1. **Clone the repository**
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd linkedinbasedchurchmanagementsystem-7j9m7d
|
||||
```
|
||||
|
||||
2. **Install dependencies**
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
3. **Set up environment variables**
|
||||
Create a `.env` file in the root directory with:
|
||||
```
|
||||
SECRET_KEY=your-secret-key-here-change-this-in-production
|
||||
```
|
||||
|
||||
4. **Run database migrations**
|
||||
```bash
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
5. **Start the application**
|
||||
```bash
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
The application will be available at `http://localhost:8000`
|
||||
|
||||
## API Documentation
|
||||
|
||||
Once the server is running, you can access:
|
||||
- **Swagger UI**: `http://localhost:8000/docs`
|
||||
- **ReDoc**: `http://localhost:8000/redoc`
|
||||
- **OpenAPI JSON**: `http://localhost:8000/openapi.json`
|
||||
|
||||
## API Endpoints
|
||||
|
||||
### Authentication
|
||||
- `POST /auth/register` - Register a new user
|
||||
- `POST /auth/token` - Login and get access token
|
||||
- `GET /auth/me` - Get current user information
|
||||
|
||||
### Users
|
||||
- `GET /users/` - Get all users (paginated)
|
||||
- `GET /users/{user_id}` - Get specific user
|
||||
- `PUT /users/me` - Update current user profile
|
||||
- `GET /users/search/{query}` - Search users
|
||||
|
||||
### Posts
|
||||
- `POST /posts/` - Create a new post
|
||||
- `GET /posts/` - Get all posts (paginated)
|
||||
- `GET /posts/{post_id}` - Get specific post
|
||||
- `PUT /posts/{post_id}` - Update post
|
||||
- `DELETE /posts/{post_id}` - Delete post
|
||||
- `POST /posts/{post_id}/like` - Like a post
|
||||
- `DELETE /posts/{post_id}/like` - Unlike a post
|
||||
- `POST /posts/{post_id}/comments` - Add comment to post
|
||||
- `GET /posts/{post_id}/comments` - Get post comments
|
||||
|
||||
### Events
|
||||
- `POST /events/` - Create a new event
|
||||
- `GET /events/` - Get all events
|
||||
- `GET /events/{event_id}` - Get specific event
|
||||
- `PUT /events/{event_id}` - Update event
|
||||
- `DELETE /events/{event_id}` - Delete event
|
||||
- `POST /events/{event_id}/register` - Register for event
|
||||
- `DELETE /events/{event_id}/register` - Unregister from event
|
||||
- `GET /events/{event_id}/registrations` - Get event registrations
|
||||
|
||||
### Connections
|
||||
- `POST /connections/` - Send connection request
|
||||
- `GET /connections/sent` - Get sent connection requests
|
||||
- `GET /connections/received` - Get received connection requests
|
||||
- `GET /connections/accepted` - Get accepted connections
|
||||
- `PUT /connections/{connection_id}` - Accept/reject connection
|
||||
- `DELETE /connections/{connection_id}` - Remove connection
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The following environment variables need to be set:
|
||||
|
||||
- `SECRET_KEY` - JWT secret key for token generation (required)
|
||||
- `PORT` - Port number for the application (default: 8000)
|
||||
|
||||
## Database
|
||||
|
||||
The application uses SQLite as the database, stored at `/app/storage/db/db.sqlite`. The database schema includes:
|
||||
|
||||
- **Users**: User accounts with profiles and authentication
|
||||
- **Connections**: LinkedIn-style connections between users
|
||||
- **Posts**: Social media style posts with likes and comments
|
||||
- **Events**: Church events with registration management
|
||||
- **Comments**: Comments on posts
|
||||
- **Likes**: Like system for posts
|
||||
- **Event Registrations**: Event attendance tracking
|
||||
|
||||
## Health Check
|
||||
|
||||
The application provides a health check endpoint at `/health` that returns the service status and version information.
|
||||
|
||||
## Development
|
||||
|
||||
### Running with Auto-reload
|
||||
```bash
|
||||
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
The project uses Ruff for code formatting and linting:
|
||||
```bash
|
||||
ruff check .
|
||||
ruff format .
|
||||
```
|
||||
|
||||
### Database Migrations
|
||||
To create a new migration:
|
||||
```bash
|
||||
alembic revision --autogenerate -m "Description of changes"
|
||||
```
|
||||
|
||||
To apply migrations:
|
||||
```bash
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
1. Fork the repository
|
||||
2. Create a feature branch
|
||||
3. Make your changes
|
||||
4. Run tests and linting
|
||||
5. Submit a pull request
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under the MIT License.
|
||||
|
42
alembic.ini
Normal file
42
alembic.ini
Normal file
@ -0,0 +1,42 @@
|
||||
[alembic]
|
||||
script_location = alembic
|
||||
prepend_sys_path = .
|
||||
version_path_separator = os
|
||||
|
||||
sqlalchemy.url = sqlite:////app/storage/db/db.sqlite
|
||||
|
||||
[post_write_hooks]
|
||||
|
||||
[loggers]
|
||||
keys = root,sqlalchemy,alembic
|
||||
|
||||
[handlers]
|
||||
keys = console
|
||||
|
||||
[formatters]
|
||||
keys = generic
|
||||
|
||||
[logger_root]
|
||||
level = WARN
|
||||
handlers = console
|
||||
qualname =
|
||||
|
||||
[logger_sqlalchemy]
|
||||
level = WARN
|
||||
handlers =
|
||||
qualname = sqlalchemy.engine
|
||||
|
||||
[logger_alembic]
|
||||
level = INFO
|
||||
handlers =
|
||||
qualname = alembic
|
||||
|
||||
[handler_console]
|
||||
class = StreamHandler
|
||||
args = (sys.stderr,)
|
||||
level = NOTSET
|
||||
formatter = generic
|
||||
|
||||
[formatter_generic]
|
||||
format = %(levelname)-5.5s [%(name)s] %(message)s
|
||||
datefmt = %H:%M:%S
|
51
alembic/env.py
Normal file
51
alembic/env.py
Normal file
@ -0,0 +1,51 @@
|
||||
from logging.config import fileConfig
|
||||
from sqlalchemy import engine_from_config
|
||||
from sqlalchemy import pool
|
||||
from alembic import context
|
||||
import sys
|
||||
import os
|
||||
|
||||
# Add the project root directory to the Python path
|
||||
sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from app.db.base import Base
|
||||
|
||||
config = context.config
|
||||
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def run_migrations_online() -> None:
|
||||
connectable = engine_from_config(
|
||||
config.get_section(config.config_ini_section, {}),
|
||||
prefix="sqlalchemy.",
|
||||
poolclass=pool.NullPool,
|
||||
)
|
||||
|
||||
with connectable.connect() as connection:
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
run_migrations_online()
|
26
alembic/script.py.mako
Normal file
26
alembic/script.py.mako
Normal file
@ -0,0 +1,26 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
${downgrades if downgrades else "pass"}
|
186
alembic/versions/001_initial_migration.py
Normal file
186
alembic/versions/001_initial_migration.py
Normal file
@ -0,0 +1,186 @@
|
||||
"""Initial migration
|
||||
|
||||
Revision ID: 001
|
||||
Revises:
|
||||
Create Date: 2025-07-01 12:00:00.000000
|
||||
|
||||
"""
|
||||
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = "001"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create users table
|
||||
op.create_table(
|
||||
"users",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("email", sa.String(), nullable=False),
|
||||
sa.Column("first_name", sa.String(), nullable=False),
|
||||
sa.Column("last_name", sa.String(), nullable=False),
|
||||
sa.Column("hashed_password", sa.String(), nullable=False),
|
||||
sa.Column("is_active", sa.Boolean(), nullable=True),
|
||||
sa.Column("is_admin", sa.Boolean(), nullable=True),
|
||||
sa.Column("profile_picture", sa.String(), nullable=True),
|
||||
sa.Column("bio", sa.Text(), nullable=True),
|
||||
sa.Column("position", sa.String(), nullable=True),
|
||||
sa.Column("phone", sa.String(), nullable=True),
|
||||
sa.Column("address", sa.Text(), nullable=True),
|
||||
sa.Column("date_joined", sa.DateTime(), nullable=False),
|
||||
sa.Column("last_login", sa.DateTime(), nullable=True),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_users_email"), "users", ["email"], unique=True)
|
||||
op.create_index(op.f("ix_users_id"), "users", ["id"], unique=False)
|
||||
|
||||
# Create connections table
|
||||
op.create_table(
|
||||
"connections",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("sender_id", sa.Integer(), nullable=False),
|
||||
sa.Column("receiver_id", sa.Integer(), nullable=False),
|
||||
sa.Column("status", sa.String(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["receiver_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["sender_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_connections_id"), "connections", ["id"], unique=False)
|
||||
|
||||
# Create posts table
|
||||
op.create_table(
|
||||
"posts",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("author_id", sa.Integer(), nullable=False),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("image_url", sa.String(), nullable=True),
|
||||
sa.Column("is_announcement", sa.Boolean(), nullable=True),
|
||||
sa.Column("is_prayer_request", sa.Boolean(), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["author_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_posts_id"), "posts", ["id"], unique=False)
|
||||
|
||||
# Create events table
|
||||
op.create_table(
|
||||
"events",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("title", sa.String(), nullable=False),
|
||||
sa.Column("description", sa.Text(), nullable=True),
|
||||
sa.Column("location", sa.String(), nullable=True),
|
||||
sa.Column("start_date", sa.DateTime(), nullable=False),
|
||||
sa.Column("end_date", sa.DateTime(), nullable=True),
|
||||
sa.Column("max_attendees", sa.Integer(), nullable=True),
|
||||
sa.Column("is_public", sa.Boolean(), nullable=True),
|
||||
sa.Column("image_url", sa.String(), nullable=True),
|
||||
sa.Column("created_by", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["created_by"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_events_id"), "events", ["id"], unique=False)
|
||||
|
||||
# Create comments table
|
||||
op.create_table(
|
||||
"comments",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("post_id", sa.Integer(), nullable=False),
|
||||
sa.Column("author_id", sa.Integer(), nullable=False),
|
||||
sa.Column("content", sa.Text(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.Column("updated_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["author_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["post_id"],
|
||||
["posts.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_comments_id"), "comments", ["id"], unique=False)
|
||||
|
||||
# Create likes table
|
||||
op.create_table(
|
||||
"likes",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("post_id", sa.Integer(), nullable=False),
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["post_id"],
|
||||
["posts.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(op.f("ix_likes_id"), "likes", ["id"], unique=False)
|
||||
|
||||
# Create event_registrations table
|
||||
op.create_table(
|
||||
"event_registrations",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column("event_id", sa.Integer(), nullable=False),
|
||||
sa.Column("user_id", sa.Integer(), nullable=False),
|
||||
sa.Column("status", sa.String(), nullable=True),
|
||||
sa.Column("registered_at", sa.DateTime(), nullable=False),
|
||||
sa.ForeignKeyConstraint(
|
||||
["event_id"],
|
||||
["events.id"],
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
)
|
||||
op.create_index(
|
||||
op.f("ix_event_registrations_id"), "event_registrations", ["id"], unique=False
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(op.f("ix_event_registrations_id"), table_name="event_registrations")
|
||||
op.drop_table("event_registrations")
|
||||
op.drop_index(op.f("ix_likes_id"), table_name="likes")
|
||||
op.drop_table("likes")
|
||||
op.drop_index(op.f("ix_comments_id"), table_name="comments")
|
||||
op.drop_table("comments")
|
||||
op.drop_index(op.f("ix_events_id"), table_name="events")
|
||||
op.drop_table("events")
|
||||
op.drop_index(op.f("ix_posts_id"), table_name="posts")
|
||||
op.drop_table("posts")
|
||||
op.drop_index(op.f("ix_connections_id"), table_name="connections")
|
||||
op.drop_table("connections")
|
||||
op.drop_index(op.f("ix_users_id"), table_name="users")
|
||||
op.drop_index(op.f("ix_users_email"), table_name="users")
|
||||
op.drop_table("users")
|
0
app/__init__.py
Normal file
0
app/__init__.py
Normal file
0
app/api/__init__.py
Normal file
0
app/api/__init__.py
Normal file
100
app/api/auth.py
Normal file
100
app/api/auth.py
Normal file
@ -0,0 +1,100 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||
from sqlalchemy.orm import Session
|
||||
from datetime import datetime, timedelta
|
||||
from app.db.base import get_db
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserCreate, UserResponse, Token
|
||||
from app.core.security import (
|
||||
verify_password,
|
||||
get_password_hash,
|
||||
create_access_token,
|
||||
verify_token,
|
||||
)
|
||||
|
||||
router = APIRouter()
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")
|
||||
|
||||
|
||||
def get_user_by_email(db: Session, email: str):
|
||||
return db.query(User).filter(User.email == email).first()
|
||||
|
||||
|
||||
def create_user(db: Session, user: UserCreate):
|
||||
hashed_password = get_password_hash(user.password)
|
||||
db_user = User(
|
||||
email=user.email,
|
||||
first_name=user.first_name,
|
||||
last_name=user.last_name,
|
||||
hashed_password=hashed_password,
|
||||
position=user.position,
|
||||
phone=user.phone,
|
||||
address=user.address,
|
||||
bio=user.bio,
|
||||
)
|
||||
db.add(db_user)
|
||||
db.commit()
|
||||
db.refresh(db_user)
|
||||
return db_user
|
||||
|
||||
|
||||
def authenticate_user(db: Session, email: str, password: str):
|
||||
user = get_user_by_email(db, email)
|
||||
if not user:
|
||||
return False
|
||||
if not verify_password(password, user.hashed_password):
|
||||
return False
|
||||
return user
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)
|
||||
):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
email = verify_token(token)
|
||||
if email is None:
|
||||
raise credentials_exception
|
||||
user = get_user_by_email(db, email=email)
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
|
||||
@router.post("/register", response_model=UserResponse)
|
||||
def register(user: UserCreate, db: Session = Depends(get_db)):
|
||||
db_user = get_user_by_email(db, email=user.email)
|
||||
if db_user:
|
||||
raise HTTPException(status_code=400, detail="Email already registered")
|
||||
return create_user(db=db, user=user)
|
||||
|
||||
|
||||
@router.post("/token", response_model=Token)
|
||||
def login_for_access_token(
|
||||
form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)
|
||||
):
|
||||
user = authenticate_user(db, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
access_token_expires = timedelta(minutes=30)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.email}, expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
# Update last login
|
||||
user.last_login = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
|
||||
@router.get("/me", response_model=UserResponse)
|
||||
def read_users_me(current_user: User = Depends(get_current_user)):
|
||||
return current_user
|
182
app/api/connections.py
Normal file
182
app/api/connections.py
Normal file
@ -0,0 +1,182 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.db.base import get_db
|
||||
from app.models.user import User, Connection
|
||||
from app.schemas.connection import (
|
||||
ConnectionCreate,
|
||||
ConnectionResponse,
|
||||
ConnectionUpdate,
|
||||
)
|
||||
from app.api.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/", response_model=ConnectionResponse)
|
||||
def send_connection_request(
|
||||
connection: ConnectionCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
# Check if receiver exists
|
||||
receiver = db.query(User).filter(User.id == connection.receiver_id).first()
|
||||
if not receiver:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
|
||||
# Check if trying to connect to self
|
||||
if connection.receiver_id == current_user.id:
|
||||
raise HTTPException(status_code=400, detail="Cannot connect to yourself")
|
||||
|
||||
# Check if connection already exists
|
||||
existing_connection = (
|
||||
db.query(Connection)
|
||||
.filter(
|
||||
(
|
||||
(Connection.sender_id == current_user.id)
|
||||
& (Connection.receiver_id == connection.receiver_id)
|
||||
)
|
||||
| (
|
||||
(Connection.sender_id == connection.receiver_id)
|
||||
& (Connection.receiver_id == current_user.id)
|
||||
)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing_connection:
|
||||
raise HTTPException(status_code=400, detail="Connection already exists")
|
||||
|
||||
db_connection = Connection(
|
||||
sender_id=current_user.id, receiver_id=connection.receiver_id
|
||||
)
|
||||
db.add(db_connection)
|
||||
db.commit()
|
||||
db.refresh(db_connection)
|
||||
|
||||
db_connection.sender_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
db_connection.receiver_name = f"{receiver.first_name} {receiver.last_name}"
|
||||
|
||||
return db_connection
|
||||
|
||||
|
||||
@router.get("/sent", response_model=List[ConnectionResponse])
|
||||
def get_sent_connections(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
connections = (
|
||||
db.query(Connection).filter(Connection.sender_id == current_user.id).all()
|
||||
)
|
||||
|
||||
for connection in connections:
|
||||
connection.sender_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
connection.receiver_name = (
|
||||
f"{connection.receiver.first_name} {connection.receiver.last_name}"
|
||||
)
|
||||
|
||||
return connections
|
||||
|
||||
|
||||
@router.get("/received", response_model=List[ConnectionResponse])
|
||||
def get_received_connections(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
connections = (
|
||||
db.query(Connection).filter(Connection.receiver_id == current_user.id).all()
|
||||
)
|
||||
|
||||
for connection in connections:
|
||||
connection.sender_name = (
|
||||
f"{connection.sender.first_name} {connection.sender.last_name}"
|
||||
)
|
||||
connection.receiver_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
|
||||
return connections
|
||||
|
||||
|
||||
@router.get("/accepted", response_model=List[ConnectionResponse])
|
||||
def get_my_connections(
|
||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_user)
|
||||
):
|
||||
connections = (
|
||||
db.query(Connection)
|
||||
.filter(
|
||||
(
|
||||
(Connection.sender_id == current_user.id)
|
||||
| (Connection.receiver_id == current_user.id)
|
||||
)
|
||||
& (Connection.status == "accepted")
|
||||
)
|
||||
.all()
|
||||
)
|
||||
|
||||
for connection in connections:
|
||||
connection.sender_name = (
|
||||
f"{connection.sender.first_name} {connection.sender.last_name}"
|
||||
)
|
||||
connection.receiver_name = (
|
||||
f"{connection.receiver.first_name} {connection.receiver.last_name}"
|
||||
)
|
||||
|
||||
return connections
|
||||
|
||||
|
||||
@router.put("/{connection_id}", response_model=ConnectionResponse)
|
||||
def update_connection(
|
||||
connection_id: int,
|
||||
connection_update: ConnectionUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
connection = db.query(Connection).filter(Connection.id == connection_id).first()
|
||||
|
||||
if not connection:
|
||||
raise HTTPException(status_code=404, detail="Connection not found")
|
||||
|
||||
# Only receiver can update the connection status
|
||||
if connection.receiver_id != current_user.id:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Not authorized to update this connection"
|
||||
)
|
||||
|
||||
# Only allow updating pending connections
|
||||
if connection.status != "pending":
|
||||
raise HTTPException(
|
||||
status_code=400, detail="Can only update pending connections"
|
||||
)
|
||||
|
||||
connection.status = connection_update.status
|
||||
db.commit()
|
||||
db.refresh(connection)
|
||||
|
||||
connection.sender_name = (
|
||||
f"{connection.sender.first_name} {connection.sender.last_name}"
|
||||
)
|
||||
connection.receiver_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
|
||||
return connection
|
||||
|
||||
|
||||
@router.delete("/{connection_id}")
|
||||
def delete_connection(
|
||||
connection_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
connection = db.query(Connection).filter(Connection.id == connection_id).first()
|
||||
|
||||
if not connection:
|
||||
raise HTTPException(status_code=404, detail="Connection not found")
|
||||
|
||||
# Only sender or receiver can delete the connection
|
||||
if (
|
||||
connection.sender_id != current_user.id
|
||||
and connection.receiver_id != current_user.id
|
||||
):
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Not authorized to delete this connection"
|
||||
)
|
||||
|
||||
db.delete(connection)
|
||||
db.commit()
|
||||
return {"message": "Connection deleted successfully"}
|
223
app/api/events.py
Normal file
223
app/api/events.py
Normal file
@ -0,0 +1,223 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.db.base import get_db
|
||||
from app.models.user import User
|
||||
from app.models.event import Event, EventRegistration
|
||||
from app.schemas.event import (
|
||||
EventCreate,
|
||||
EventUpdate,
|
||||
EventResponse,
|
||||
EventRegistrationResponse,
|
||||
)
|
||||
from app.api.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/", response_model=EventResponse)
|
||||
def create_event(
|
||||
event: EventCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
db_event = Event(**event.dict(), created_by=current_user.id)
|
||||
db.add(db_event)
|
||||
db.commit()
|
||||
db.refresh(db_event)
|
||||
return db_event
|
||||
|
||||
|
||||
@router.get("/", response_model=List[EventResponse])
|
||||
def get_events(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
events = db.query(Event).filter(Event.is_public).offset(skip).limit(limit).all()
|
||||
|
||||
# Add additional info for each event
|
||||
for event in events:
|
||||
event.creator_name = f"{event.creator.first_name} {event.creator.last_name}"
|
||||
event.registered_count = (
|
||||
db.query(EventRegistration)
|
||||
.filter(EventRegistration.event_id == event.id)
|
||||
.count()
|
||||
)
|
||||
event.is_registered = (
|
||||
db.query(EventRegistration)
|
||||
.filter(
|
||||
EventRegistration.event_id == event.id,
|
||||
EventRegistration.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
|
||||
return events
|
||||
|
||||
|
||||
@router.get("/{event_id}", response_model=EventResponse)
|
||||
def get_event(
|
||||
event_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
if event is None:
|
||||
raise HTTPException(status_code=404, detail="Event not found")
|
||||
|
||||
event.creator_name = f"{event.creator.first_name} {event.creator.last_name}"
|
||||
event.registered_count = (
|
||||
db.query(EventRegistration)
|
||||
.filter(EventRegistration.event_id == event.id)
|
||||
.count()
|
||||
)
|
||||
event.is_registered = (
|
||||
db.query(EventRegistration)
|
||||
.filter(
|
||||
EventRegistration.event_id == event.id,
|
||||
EventRegistration.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
is not None
|
||||
)
|
||||
|
||||
return event
|
||||
|
||||
|
||||
@router.put("/{event_id}", response_model=EventResponse)
|
||||
def update_event(
|
||||
event_id: int,
|
||||
event_update: EventUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
if event is None:
|
||||
raise HTTPException(status_code=404, detail="Event not found")
|
||||
|
||||
if event.created_by != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Not authorized to update this event"
|
||||
)
|
||||
|
||||
for field, value in event_update.dict(exclude_unset=True).items():
|
||||
setattr(event, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(event)
|
||||
return event
|
||||
|
||||
|
||||
@router.delete("/{event_id}")
|
||||
def delete_event(
|
||||
event_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
if event is None:
|
||||
raise HTTPException(status_code=404, detail="Event not found")
|
||||
|
||||
if event.created_by != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Not authorized to delete this event"
|
||||
)
|
||||
|
||||
db.delete(event)
|
||||
db.commit()
|
||||
return {"message": "Event deleted successfully"}
|
||||
|
||||
|
||||
@router.post("/{event_id}/register", response_model=EventRegistrationResponse)
|
||||
def register_for_event(
|
||||
event_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
if event is None:
|
||||
raise HTTPException(status_code=404, detail="Event not found")
|
||||
|
||||
# Check if already registered
|
||||
existing_registration = (
|
||||
db.query(EventRegistration)
|
||||
.filter(
|
||||
EventRegistration.event_id == event_id,
|
||||
EventRegistration.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if existing_registration:
|
||||
raise HTTPException(status_code=400, detail="Already registered for this event")
|
||||
|
||||
# Check if event is full
|
||||
if event.max_attendees:
|
||||
current_count = (
|
||||
db.query(EventRegistration)
|
||||
.filter(EventRegistration.event_id == event_id)
|
||||
.count()
|
||||
)
|
||||
if current_count >= event.max_attendees:
|
||||
raise HTTPException(status_code=400, detail="Event is full")
|
||||
|
||||
registration = EventRegistration(event_id=event_id, user_id=current_user.id)
|
||||
db.add(registration)
|
||||
db.commit()
|
||||
db.refresh(registration)
|
||||
|
||||
registration.user_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
return registration
|
||||
|
||||
|
||||
@router.delete("/{event_id}/register")
|
||||
def unregister_from_event(
|
||||
event_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
registration = (
|
||||
db.query(EventRegistration)
|
||||
.filter(
|
||||
EventRegistration.event_id == event_id,
|
||||
EventRegistration.user_id == current_user.id,
|
||||
)
|
||||
.first()
|
||||
)
|
||||
|
||||
if registration is None:
|
||||
raise HTTPException(status_code=404, detail="Registration not found")
|
||||
|
||||
db.delete(registration)
|
||||
db.commit()
|
||||
return {"message": "Unregistered successfully"}
|
||||
|
||||
|
||||
@router.get("/{event_id}/registrations", response_model=List[EventRegistrationResponse])
|
||||
def get_event_registrations(
|
||||
event_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
event = db.query(Event).filter(Event.id == event_id).first()
|
||||
if event is None:
|
||||
raise HTTPException(status_code=404, detail="Event not found")
|
||||
|
||||
if event.created_by != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Not authorized to view registrations"
|
||||
)
|
||||
|
||||
registrations = (
|
||||
db.query(EventRegistration).filter(EventRegistration.event_id == event_id).all()
|
||||
)
|
||||
|
||||
for registration in registrations:
|
||||
registration.user_name = (
|
||||
f"{registration.user.first_name} {registration.user.last_name}"
|
||||
)
|
||||
|
||||
return registrations
|
202
app/api/posts.py
Normal file
202
app/api/posts.py
Normal file
@ -0,0 +1,202 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.db.base import get_db
|
||||
from app.models.user import User
|
||||
from app.models.post import Post, Comment, Like
|
||||
from app.schemas.post import (
|
||||
PostCreate,
|
||||
PostUpdate,
|
||||
PostResponse,
|
||||
CommentCreate,
|
||||
CommentResponse,
|
||||
)
|
||||
from app.api.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/", response_model=PostResponse)
|
||||
def create_post(
|
||||
post: PostCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
db_post = Post(**post.dict(), author_id=current_user.id)
|
||||
db.add(db_post)
|
||||
db.commit()
|
||||
db.refresh(db_post)
|
||||
|
||||
db_post.author_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
db_post.likes_count = 0
|
||||
db_post.comments_count = 0
|
||||
|
||||
return db_post
|
||||
|
||||
|
||||
@router.get("/", response_model=List[PostResponse])
|
||||
def get_posts(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
posts = (
|
||||
db.query(Post).order_by(Post.created_at.desc()).offset(skip).limit(limit).all()
|
||||
)
|
||||
|
||||
for post in posts:
|
||||
post.author_name = f"{post.author.first_name} {post.author.last_name}"
|
||||
post.likes_count = db.query(Like).filter(Like.post_id == post.id).count()
|
||||
post.comments_count = (
|
||||
db.query(Comment).filter(Comment.post_id == post.id).count()
|
||||
)
|
||||
|
||||
return posts
|
||||
|
||||
|
||||
@router.get("/{post_id}", response_model=PostResponse)
|
||||
def get_post(
|
||||
post_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="Post not found")
|
||||
|
||||
post.author_name = f"{post.author.first_name} {post.author.last_name}"
|
||||
post.likes_count = db.query(Like).filter(Like.post_id == post.id).count()
|
||||
post.comments_count = db.query(Comment).filter(Comment.post_id == post.id).count()
|
||||
|
||||
return post
|
||||
|
||||
|
||||
@router.put("/{post_id}", response_model=PostResponse)
|
||||
def update_post(
|
||||
post_id: int,
|
||||
post_update: PostUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="Post not found")
|
||||
|
||||
if post.author_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Not authorized to update this post"
|
||||
)
|
||||
|
||||
for field, value in post_update.dict(exclude_unset=True).items():
|
||||
setattr(post, field, value)
|
||||
|
||||
db.commit()
|
||||
db.refresh(post)
|
||||
return post
|
||||
|
||||
|
||||
@router.delete("/{post_id}")
|
||||
def delete_post(
|
||||
post_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="Post not found")
|
||||
|
||||
if post.author_id != current_user.id and not current_user.is_admin:
|
||||
raise HTTPException(
|
||||
status_code=403, detail="Not authorized to delete this post"
|
||||
)
|
||||
|
||||
db.delete(post)
|
||||
db.commit()
|
||||
return {"message": "Post deleted successfully"}
|
||||
|
||||
|
||||
@router.post("/{post_id}/like")
|
||||
def like_post(
|
||||
post_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="Post not found")
|
||||
|
||||
existing_like = (
|
||||
db.query(Like)
|
||||
.filter(Like.post_id == post_id, Like.user_id == current_user.id)
|
||||
.first()
|
||||
)
|
||||
if existing_like:
|
||||
raise HTTPException(status_code=400, detail="Already liked this post")
|
||||
|
||||
like = Like(post_id=post_id, user_id=current_user.id)
|
||||
db.add(like)
|
||||
db.commit()
|
||||
return {"message": "Post liked successfully"}
|
||||
|
||||
|
||||
@router.delete("/{post_id}/like")
|
||||
def unlike_post(
|
||||
post_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
like = (
|
||||
db.query(Like)
|
||||
.filter(Like.post_id == post_id, Like.user_id == current_user.id)
|
||||
.first()
|
||||
)
|
||||
if not like:
|
||||
raise HTTPException(status_code=404, detail="Like not found")
|
||||
|
||||
db.delete(like)
|
||||
db.commit()
|
||||
return {"message": "Post unliked successfully"}
|
||||
|
||||
|
||||
@router.post("/{post_id}/comments", response_model=CommentResponse)
|
||||
def create_comment(
|
||||
post_id: int,
|
||||
comment: CommentCreate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="Post not found")
|
||||
|
||||
db_comment = Comment(**comment.dict(), post_id=post_id, author_id=current_user.id)
|
||||
db.add(db_comment)
|
||||
db.commit()
|
||||
db.refresh(db_comment)
|
||||
|
||||
db_comment.author_name = f"{current_user.first_name} {current_user.last_name}"
|
||||
return db_comment
|
||||
|
||||
|
||||
@router.get("/{post_id}/comments", response_model=List[CommentResponse])
|
||||
def get_comments(
|
||||
post_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
post = db.query(Post).filter(Post.id == post_id).first()
|
||||
if not post:
|
||||
raise HTTPException(status_code=404, detail="Post not found")
|
||||
|
||||
comments = (
|
||||
db.query(Comment)
|
||||
.filter(Comment.post_id == post_id)
|
||||
.order_by(Comment.created_at.asc())
|
||||
.all()
|
||||
)
|
||||
|
||||
for comment in comments:
|
||||
comment.author_name = f"{comment.author.first_name} {comment.author.last_name}"
|
||||
|
||||
return comments
|
68
app/api/users.py
Normal file
68
app/api/users.py
Normal file
@ -0,0 +1,68 @@
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from typing import List
|
||||
from app.db.base import get_db
|
||||
from app.models.user import User
|
||||
from app.schemas.user import UserResponse, UserUpdate
|
||||
from app.api.auth import get_current_user
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/", response_model=List[UserResponse])
|
||||
def get_users(
|
||||
skip: int = 0,
|
||||
limit: int = 100,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
users = db.query(User).filter(User.is_active).offset(skip).limit(limit).all()
|
||||
return users
|
||||
|
||||
|
||||
@router.get("/{user_id}", response_model=UserResponse)
|
||||
def get_user(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
user = db.query(User).filter(User.id == user_id, User.is_active).first()
|
||||
if user is None:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
return user
|
||||
|
||||
|
||||
@router.put("/me", response_model=UserResponse)
|
||||
def update_my_profile(
|
||||
user_update: UserUpdate,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
for field, value in user_update.dict(exclude_unset=True).items():
|
||||
setattr(current_user, field, value)
|
||||
db.commit()
|
||||
db.refresh(current_user)
|
||||
return current_user
|
||||
|
||||
|
||||
@router.get("/search/{query}", response_model=List[UserResponse])
|
||||
def search_users(
|
||||
query: str,
|
||||
db: Session = Depends(get_db),
|
||||
current_user: User = Depends(get_current_user),
|
||||
):
|
||||
users = (
|
||||
db.query(User)
|
||||
.filter(
|
||||
User.is_active,
|
||||
(
|
||||
User.first_name.contains(query)
|
||||
| User.last_name.contains(query)
|
||||
| User.email.contains(query)
|
||||
| User.position.contains(query)
|
||||
),
|
||||
)
|
||||
.limit(20)
|
||||
.all()
|
||||
)
|
||||
return users
|
0
app/core/__init__.py
Normal file
0
app/core/__init__.py
Normal file
7
app/core/config.py
Normal file
7
app/core/config.py
Normal file
@ -0,0 +1,7 @@
|
||||
from decouple import config
|
||||
|
||||
SECRET_KEY = config(
|
||||
"SECRET_KEY", default="your-secret-key-here-change-this-in-production"
|
||||
)
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
37
app/core/security.py
Normal file
37
app/core/security.py
Normal file
@ -0,0 +1,37 @@
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from app.core.config import SECRET_KEY, ALGORITHM, ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
return pwd_context.verify(plain_password, hashed_password)
|
||||
|
||||
|
||||
def get_password_hash(password: str) -> str:
|
||||
return pwd_context.hash(password)
|
||||
|
||||
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
def verify_token(token: str):
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
email: str = payload.get("sub")
|
||||
if email is None:
|
||||
return None
|
||||
return email
|
||||
except JWTError:
|
||||
return None
|
0
app/db/__init__.py
Normal file
0
app/db/__init__.py
Normal file
25
app/db/base.py
Normal file
25
app/db/base.py
Normal file
@ -0,0 +1,25 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
from pathlib import Path
|
||||
|
||||
DB_DIR = Path("/app/storage/db")
|
||||
DB_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
SQLALCHEMY_DATABASE_URL = f"sqlite:///{DB_DIR}/db.sqlite"
|
||||
|
||||
engine = create_engine(
|
||||
SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False}
|
||||
)
|
||||
|
||||
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
0
app/models/__init__.py
Normal file
0
app/models/__init__.py
Normal file
43
app/models/event.py
Normal file
43
app/models/event.py
Normal file
@ -0,0 +1,43 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class Event(Base):
|
||||
__tablename__ = "events"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
title = Column(String, nullable=False)
|
||||
description = Column(Text, nullable=True)
|
||||
location = Column(String, nullable=True)
|
||||
start_date = Column(DateTime, nullable=False)
|
||||
end_date = Column(DateTime, nullable=True)
|
||||
max_attendees = Column(Integer, nullable=True)
|
||||
is_public = Column(Boolean, default=True)
|
||||
image_url = Column(String, nullable=True)
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
updated_at = Column(
|
||||
DateTime, nullable=False, default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
creator = relationship("User")
|
||||
registrations = relationship(
|
||||
"EventRegistration", back_populates="event", cascade="all, delete-orphan"
|
||||
)
|
||||
|
||||
|
||||
class EventRegistration(Base):
|
||||
__tablename__ = "event_registrations"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
event_id = Column(Integer, ForeignKey("events.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
status = Column(String, default="registered") # registered, attended, cancelled
|
||||
registered_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
event = relationship("Event", back_populates="registrations")
|
||||
user = relationship("User", back_populates="event_registrations")
|
57
app/models/post.py
Normal file
57
app/models/post.py
Normal file
@ -0,0 +1,57 @@
|
||||
from sqlalchemy import Column, Integer, String, DateTime, Text, ForeignKey, Boolean
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class Post(Base):
|
||||
__tablename__ = "posts"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
author_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
title = Column(String, nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
image_url = Column(String, nullable=True)
|
||||
is_announcement = Column(Boolean, default=False)
|
||||
is_prayer_request = Column(Boolean, default=False)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
updated_at = Column(
|
||||
DateTime, nullable=False, default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
author = relationship("User", back_populates="posts")
|
||||
comments = relationship(
|
||||
"Comment", back_populates="post", cascade="all, delete-orphan"
|
||||
)
|
||||
likes = relationship("Like", back_populates="post", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class Comment(Base):
|
||||
__tablename__ = "comments"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
post_id = Column(Integer, ForeignKey("posts.id"), nullable=False)
|
||||
author_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
content = Column(Text, nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
updated_at = Column(
|
||||
DateTime, nullable=False, default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
post = relationship("Post", back_populates="comments")
|
||||
author = relationship("User", back_populates="comments")
|
||||
|
||||
|
||||
class Like(Base):
|
||||
__tablename__ = "likes"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
post_id = Column(Integer, ForeignKey("posts.id"), nullable=False)
|
||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
|
||||
# Relationships
|
||||
post = relationship("Post", back_populates="likes")
|
||||
user = relationship("User", back_populates="likes")
|
56
app/models/user.py
Normal file
56
app/models/user.py
Normal file
@ -0,0 +1,56 @@
|
||||
from sqlalchemy import Boolean, Column, Integer, String, DateTime, Text, ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql import func
|
||||
from app.db.base import Base
|
||||
|
||||
|
||||
class User(Base):
|
||||
__tablename__ = "users"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
email = Column(String, unique=True, index=True, nullable=False)
|
||||
first_name = Column(String, nullable=False)
|
||||
last_name = Column(String, nullable=False)
|
||||
hashed_password = Column(String, nullable=False)
|
||||
is_active = Column(Boolean, default=True)
|
||||
is_admin = Column(Boolean, default=False)
|
||||
profile_picture = Column(String, nullable=True)
|
||||
bio = Column(Text, nullable=True)
|
||||
position = Column(String, nullable=True) # Church position/role
|
||||
phone = Column(String, nullable=True)
|
||||
address = Column(Text, nullable=True)
|
||||
date_joined = Column(DateTime, nullable=False, default=func.now())
|
||||
last_login = Column(DateTime, nullable=True)
|
||||
|
||||
# Relationships
|
||||
sent_connections = relationship(
|
||||
"Connection", foreign_keys="Connection.sender_id", back_populates="sender"
|
||||
)
|
||||
received_connections = relationship(
|
||||
"Connection", foreign_keys="Connection.receiver_id", back_populates="receiver"
|
||||
)
|
||||
posts = relationship("Post", back_populates="author")
|
||||
comments = relationship("Comment", back_populates="author")
|
||||
likes = relationship("Like", back_populates="user")
|
||||
event_registrations = relationship("EventRegistration", back_populates="user")
|
||||
|
||||
|
||||
class Connection(Base):
|
||||
__tablename__ = "connections"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
sender_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
receiver_id = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
status = Column(String, default="pending") # pending, accepted, rejected
|
||||
created_at = Column(DateTime, nullable=False, default=func.now())
|
||||
updated_at = Column(
|
||||
DateTime, nullable=False, default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
# Relationships
|
||||
sender = relationship(
|
||||
"User", foreign_keys=[sender_id], back_populates="sent_connections"
|
||||
)
|
||||
receiver = relationship(
|
||||
"User", foreign_keys=[receiver_id], back_populates="received_connections"
|
||||
)
|
0
app/schemas/__init__.py
Normal file
0
app/schemas/__init__.py
Normal file
29
app/schemas/connection.py
Normal file
29
app/schemas/connection.py
Normal file
@ -0,0 +1,29 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class ConnectionBase(BaseModel):
|
||||
receiver_id: int
|
||||
|
||||
|
||||
class ConnectionCreate(ConnectionBase):
|
||||
pass
|
||||
|
||||
|
||||
class ConnectionResponse(BaseModel):
|
||||
id: int
|
||||
sender_id: int
|
||||
receiver_id: int
|
||||
status: str
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
sender_name: Optional[str] = None
|
||||
receiver_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class ConnectionUpdate(BaseModel):
|
||||
status: str # accepted or rejected
|
54
app/schemas/event.py
Normal file
54
app/schemas/event.py
Normal file
@ -0,0 +1,54 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class EventBase(BaseModel):
|
||||
title: str
|
||||
description: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
start_date: datetime
|
||||
end_date: Optional[datetime] = None
|
||||
max_attendees: Optional[int] = None
|
||||
is_public: bool = True
|
||||
image_url: Optional[str] = None
|
||||
|
||||
|
||||
class EventCreate(EventBase):
|
||||
pass
|
||||
|
||||
|
||||
class EventUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
location: Optional[str] = None
|
||||
start_date: Optional[datetime] = None
|
||||
end_date: Optional[datetime] = None
|
||||
max_attendees: Optional[int] = None
|
||||
is_public: Optional[bool] = None
|
||||
image_url: Optional[str] = None
|
||||
|
||||
|
||||
class EventResponse(EventBase):
|
||||
id: int
|
||||
created_by: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
creator_name: Optional[str] = None
|
||||
registered_count: int = 0
|
||||
is_registered: bool = False
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class EventRegistrationResponse(BaseModel):
|
||||
id: int
|
||||
event_id: int
|
||||
user_id: int
|
||||
status: str
|
||||
registered_at: datetime
|
||||
user_name: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
56
app/schemas/post.py
Normal file
56
app/schemas/post.py
Normal file
@ -0,0 +1,56 @@
|
||||
from pydantic import BaseModel
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class PostBase(BaseModel):
|
||||
title: str
|
||||
content: str
|
||||
image_url: Optional[str] = None
|
||||
is_announcement: bool = False
|
||||
is_prayer_request: bool = False
|
||||
|
||||
|
||||
class PostCreate(PostBase):
|
||||
pass
|
||||
|
||||
|
||||
class PostUpdate(BaseModel):
|
||||
title: Optional[str] = None
|
||||
content: Optional[str] = None
|
||||
image_url: Optional[str] = None
|
||||
is_announcement: Optional[bool] = None
|
||||
is_prayer_request: Optional[bool] = None
|
||||
|
||||
|
||||
class PostResponse(PostBase):
|
||||
id: int
|
||||
author_id: int
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
author_name: Optional[str] = None
|
||||
likes_count: int = 0
|
||||
comments_count: int = 0
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class CommentBase(BaseModel):
|
||||
content: str
|
||||
|
||||
|
||||
class CommentCreate(CommentBase):
|
||||
pass
|
||||
|
||||
|
||||
class CommentResponse(CommentBase):
|
||||
id: int
|
||||
post_id: int
|
||||
author_id: int
|
||||
author_name: Optional[str] = None
|
||||
created_at: datetime
|
||||
updated_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
48
app/schemas/user.py
Normal file
48
app/schemas/user.py
Normal file
@ -0,0 +1,48 @@
|
||||
from pydantic import BaseModel, EmailStr
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class UserBase(BaseModel):
|
||||
email: EmailStr
|
||||
first_name: str
|
||||
last_name: str
|
||||
position: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
bio: Optional[str] = None
|
||||
|
||||
|
||||
class UserCreate(UserBase):
|
||||
password: str
|
||||
|
||||
|
||||
class UserUpdate(BaseModel):
|
||||
first_name: Optional[str] = None
|
||||
last_name: Optional[str] = None
|
||||
position: Optional[str] = None
|
||||
phone: Optional[str] = None
|
||||
address: Optional[str] = None
|
||||
bio: Optional[str] = None
|
||||
profile_picture: Optional[str] = None
|
||||
|
||||
|
||||
class UserResponse(UserBase):
|
||||
id: int
|
||||
is_active: bool
|
||||
is_admin: bool
|
||||
profile_picture: Optional[str] = None
|
||||
date_joined: datetime
|
||||
last_login: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class Token(BaseModel):
|
||||
access_token: str
|
||||
token_type: str
|
||||
|
||||
|
||||
class TokenData(BaseModel):
|
||||
email: Optional[str] = None
|
52
main.py
Normal file
52
main.py
Normal file
@ -0,0 +1,52 @@
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from app.api import auth, users, posts, events, connections
|
||||
import os
|
||||
|
||||
app = FastAPI(
|
||||
title="LinkedIn-Based Church Management System",
|
||||
description="A church management system with LinkedIn-style networking features",
|
||||
version="1.0.0",
|
||||
openapi_url="/openapi.json",
|
||||
)
|
||||
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# Include routers
|
||||
app.include_router(auth.router, prefix="/auth", tags=["Authentication"])
|
||||
app.include_router(users.router, prefix="/users", tags=["Users"])
|
||||
app.include_router(posts.router, prefix="/posts", tags=["Posts"])
|
||||
app.include_router(events.router, prefix="/events", tags=["Events"])
|
||||
app.include_router(connections.router, prefix="/connections", tags=["Connections"])
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
return {
|
||||
"title": "LinkedIn-Based Church Management System",
|
||||
"documentation": "/docs",
|
||||
"health": "/health",
|
||||
}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
return {
|
||||
"status": "healthy",
|
||||
"service": "LinkedIn-Based Church Management System",
|
||||
"version": "1.0.0",
|
||||
}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
|
||||
uvicorn.run(
|
||||
"main:app", host="0.0.0.0", port=int(os.getenv("PORT", 8000)), reload=True
|
||||
)
|
11
requirements.txt
Normal file
11
requirements.txt
Normal file
@ -0,0 +1,11 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
sqlalchemy==2.0.23
|
||||
alembic==1.12.1
|
||||
python-multipart==0.0.6
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
python-decouple==3.8
|
||||
ruff==0.1.6
|
||||
pydantic==2.5.0
|
||||
email-validator==2.1.0
|
Loading…
x
Reference in New Issue
Block a user