diff --git a/.dockerignore b/.dockerignore index 3f2b844..d9b625e 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,13 +1,9 @@ -.env .git -.gitignore -README.md -Dockerfile -.dockerignore -.air.toml -tmp/ -.vscode/ -.idea/ +__pycache__/ +*.py[cod] *.log -coverage.out -rideaware-api \ No newline at end of file +!.env +venv/ +.venv/ +dist/ +build/ \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 7aa361b..c72dff0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,52 +1,53 @@ -# Build stage -FROM golang:1.21-alpine AS builder +FROM python:3.10-slim AS builder + +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 -# Set working directory WORKDIR /app -# Install git (needed for some Go modules) -RUN apk add --no-cache git +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential gcc \ + && rm -rf /var/lib/apt/lists/* -# Copy go mod files -COPY go.mod go.sum ./ +COPY requirements.txt . -# Download dependencies -RUN go mod download +RUN python -m pip install --upgrade pip && \ + pip wheel --no-deps -r requirements.txt -w /wheels && \ + pip wheel --no-deps gunicorn -w /wheels -# Copy source code -COPY . . +FROM python:3.10-slim AS runtime -# Build the application -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o rideaware-api . +ENV PYTHONDONTWRITEBYTECODE=1 \ + PYTHONUNBUFFERED=1 \ + PIP_NO_CACHE_DIR=1 \ + PORT=5000 \ + WSGI_MODULE=server:app \ + GUNICORN_WORKERS=2 \ + GUNICORN_THREADS=4 \ + GUNICORN_TIMEOUT=60 \ + FLASK_APP=server.py -# Production stage -FROM alpine:latest +WORKDIR /app -# Install ca-certificates for HTTPS requests and timezone data -RUN apk --no-cache add ca-certificates tzdata +RUN groupadd -g 10001 app && useradd -m -u 10001 -g app app -# Create non-root user -RUN addgroup -g 1001 -S appgroup && \ - adduser -u 1001 -S appuser -G appgroup +COPY --from=builder /wheels /wheels +RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels -# Set working directory -WORKDIR /home/appuser +# Install python-dotenv if not already in requirements.txt +RUN pip install python-dotenv -# Copy binary from builder stage -COPY --from=builder /app/rideaware-api . +USER app -# Change ownership to non-root user -RUN chown -R appuser:appgroup /home/appuser +COPY --chown=app:app . . -# Switch to non-root user -USER appuser +# Copy .env file specifically +COPY --chown=app:app .env .env -# Expose port -EXPOSE 8080 +EXPOSE 5000 -# Health check -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 +HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ + CMD python -c "import os,socket; s=socket.socket(); s.settimeout(2); s.connect(('127.0.0.1', int(os.getenv('PORT', '5000')))); s.close()" -# Run the application -CMD ["./rideaware-api"] \ No newline at end of file +CMD ["sh", "-c", "exec gunicorn $WSGI_MODULE --bind=0.0.0.0:$PORT --workers=$GUNICORN_WORKERS --threads=$GUNICORN_THREADS --timeout=$GUNICORN_TIMEOUT --access-logfile=- --error-logfile=- --keep-alive=5"] \ No newline at end of file diff --git a/README.md b/README.md index 65ce1b9..cda9868 100644 --- a/README.md +++ b/README.md @@ -15,78 +15,53 @@ Whether you're building a structured training plan, analyzing ride data, or comp Ensure you have the following installed on your system: - Docker -- Go 1.21 or later -- PostgreSQL (for local development, optional) +- Python 3.10 or later +- pip ### Setting Up the Project 1. **Clone the Repository** ```bash - git clone https://github.com/rideaware/rideaware-api.git + git clone https://github.com/VeloInnovate/rideaware-api.git cd rideaware-api ``` -2. **Install Go Dependencies** +2. **Create a Virtual Environment** + It is recommended to use a Python virtual environment to isolate dependencies. ```bash - go mod tidy + python3 -m venv .venv ``` -3. **Build the Application** +3. **Activate the Virtual Environment** + - On Linux/Mac: + ```bash + source .venv/bin/activate + ``` + - On Windows: + ```cmd + .venv\Scripts\activate + ``` + +4. **Install Requirements** + Install the required Python packages using pip: ```bash - go build -o rideaware-api + pip install -r requirements.txt ``` ### Configuration The application uses environment variables for configuration. Create a `.env` file in the root directory and define the following variables: -```env -# Database Configuration -PG_HOST=your_postgres_host -PG_PORT=5432 -PG_DATABASE=rideaware -PG_USER=your_postgres_user -PG_PASSWORD=your_postgres_password - -# Application Configuration -SECRET_KEY=your_secret_key_for_sessions -PORT=8080 - -# Email Configuration (Optional) -SMTP_SERVER=your_smtp_server -SMTP_PORT=465 -SMTP_USER=your_email@domain.com -SMTP_PASSWORD=your_email_password ``` - -### Running the Application - -#### Development Mode - -```bash -go run main.go +DATABASE= ``` - -The application will be available at http://localhost:8080. - -#### Production Mode - -```bash -./rideaware-api -``` - -### API Endpoints - -- `GET /health` - Health check endpoint -- `POST /auth/signup` - User registration -- `POST /auth/login` - User authentication -- `POST /auth/logout` - User logout +- Replace `` with the URI of your database (e.g., SQLite, PostgreSQL). ### Running with Docker -To run the application in a containerized environment: +To run the application in a containerized environment, you can use the provided Dockerfile. 1. **Build the Docker Image**: @@ -97,60 +72,14 @@ docker build -t rideaware-api . 2. **Run the Container** ```bash -docker run -d -p 8080:8080 --env-file .env rideaware-api +docker run -d -p 5000:5000 --env-file .env rideaware-api ``` -The application will be available at http://localhost:8080. - -### Example Dockerfile - -```dockerfile -FROM golang:1.21-alpine AS builder - -WORKDIR /app -COPY go.mod go.sum ./ -RUN go mod download - -COPY . . -RUN go build -o rideaware-api - -FROM alpine:latest -RUN apk --no-cache add ca-certificates -WORKDIR /root/ - -COPY --from=builder /app/rideaware-api . -CMD ["./rideaware-api"] -``` - -### Database Migration - -The application automatically runs database migrations on startup using GORM's AutoMigrate feature. This will create the necessary tables: - -- `users` - User accounts -- `user_profiles` - User profile information +The application will be available at http://127.0.0.1:5000. ### Running Tests -To run tests: - -```bash -go test ./... -``` - -To run tests with coverage: - -```bash -go test -cover ./... -``` - -### Development - -To add new features: - -1. Create models in the `models/` directory -2. Add business logic in the `services/` directory -3. Define API routes in the `routes/` directory -4. Register routes in `main.go` +To be added. ## Contributing @@ -158,4 +87,5 @@ Contributions are welcome! Please create a pull request or open an issue for any ## License -This project is licensed under the AGPL-3.0 License. \ No newline at end of file +This project is licensed under the AGPL-3.0 License. + diff --git a/config/database.go b/config/database.go deleted file mode 100644 index be6ad8e..0000000 --- a/config/database.go +++ /dev/null @@ -1,28 +0,0 @@ -package config - -import ( - "fmt" - "log" - "os" - - "gorm.io/driver/postgres" - "gorm.io/gorm" -) - -func InitDB() *gorm.DB { - host := os.Getenv("PG_HOST") - port := os.Getenv("PG_PORT") - database := os.Getenv("PG_DATABASE") - user := os.Getenv("PG_USER") - password := os.Getenv("PG_PASSWORD") - - // Try with quoted password - dsn := fmt.Sprintf(`host=%s port=%s user=%s password='%s' dbname=%s sslmode=disable`, - host, port, user, password, database) - - db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{}) - if err != nil { - log.Fatal("Failed to connect to database:", err) - } - return db -} diff --git a/diff.txt b/diff.txt deleted file mode 100644 index 7394524..0000000 --- a/diff.txt +++ /dev/null @@ -1,915 +0,0 @@ -diff --git a/.dockerignore b/.dockerignore -index d9b625e..3f2b844 100644 ---- a/.dockerignore -+++ b/.dockerignore -@@ -1,9 +1,13 @@ -+.env - .git --__pycache__/ --*.py[cod] -+.gitignore -+README.md -+Dockerfile -+.dockerignore -+.air.toml -+tmp/ -+.vscode/ -+.idea/ - *.log --!.env --venv/ --.venv/ --dist/ --build/ -\ No newline at end of file -+coverage.out -+rideaware-api -\ No newline at end of file -diff --git a/Dockerfile b/Dockerfile -index c72dff0..7aa361b 100644 ---- a/Dockerfile -+++ b/Dockerfile -@@ -1,53 +1,52 @@ --FROM python:3.10-slim AS builder -- --ENV PYTHONDONTWRITEBYTECODE=1 \ -- PYTHONUNBUFFERED=1 \ -- PIP_NO_CACHE_DIR=1 -+# Build stage -+FROM golang:1.21-alpine AS builder - -+# Set working directory - WORKDIR /app - --RUN apt-get update && apt-get install -y --no-install-recommends \ -- build-essential gcc \ -- && rm -rf /var/lib/apt/lists/* -+# Install git (needed for some Go modules) -+RUN apk add --no-cache git - --COPY requirements.txt . -+# Copy go mod files -+COPY go.mod go.sum ./ - --RUN python -m pip install --upgrade pip && \ -- pip wheel --no-deps -r requirements.txt -w /wheels && \ -- pip wheel --no-deps gunicorn -w /wheels -+# Download dependencies -+RUN go mod download - --FROM python:3.10-slim AS runtime -+# Copy source code -+COPY . . - --ENV PYTHONDONTWRITEBYTECODE=1 \ -- PYTHONUNBUFFERED=1 \ -- PIP_NO_CACHE_DIR=1 \ -- PORT=5000 \ -- WSGI_MODULE=server:app \ -- GUNICORN_WORKERS=2 \ -- GUNICORN_THREADS=4 \ -- GUNICORN_TIMEOUT=60 \ -- FLASK_APP=server.py -+# Build the application -+RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o rideaware-api . - --WORKDIR /app -+# Production stage -+FROM alpine:latest - --RUN groupadd -g 10001 app && useradd -m -u 10001 -g app app -+# Install ca-certificates for HTTPS requests and timezone data -+RUN apk --no-cache add ca-certificates tzdata - --COPY --from=builder /wheels /wheels --RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels -+# Create non-root user -+RUN addgroup -g 1001 -S appgroup && \ -+ adduser -u 1001 -S appuser -G appgroup - --# Install python-dotenv if not already in requirements.txt --RUN pip install python-dotenv -+# Set working directory -+WORKDIR /home/appuser - --USER app -+# Copy binary from builder stage -+COPY --from=builder /app/rideaware-api . - --COPY --chown=app:app . . -+# Change ownership to non-root user -+RUN chown -R appuser:appgroup /home/appuser - --# Copy .env file specifically --COPY --chown=app:app .env .env -+# Switch to non-root user -+USER appuser - --EXPOSE 5000 -+# Expose port -+EXPOSE 8080 - --HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \ -- CMD python -c "import os,socket; s=socket.socket(); s.settimeout(2); s.connect(('127.0.0.1', int(os.getenv('PORT', '5000')))); s.close()" -+# Health check -+HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ -+ CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1 - --CMD ["sh", "-c", "exec gunicorn $WSGI_MODULE --bind=0.0.0.0:$PORT --workers=$GUNICORN_WORKERS --threads=$GUNICORN_THREADS --timeout=$GUNICORN_TIMEOUT --access-logfile=- --error-logfile=- --keep-alive=5"] -\ No newline at end of file -+# Run the application -+CMD ["./rideaware-api"] -\ No newline at end of file -diff --git a/README.md b/README.md -index cda9868..65ce1b9 100644 ---- a/README.md -+++ b/README.md -@@ -15,53 +15,78 @@ Whether you're building a structured training plan, analyzing ride data, or comp - Ensure you have the following installed on your system: - - - Docker --- Python 3.10 or later --- pip -+- Go 1.21 or later -+- PostgreSQL (for local development, optional) - - ### Setting Up the Project - - 1. **Clone the Repository** - - ```bash -- git clone https://github.com/VeloInnovate/rideaware-api.git -+ git clone https://github.com/rideaware/rideaware-api.git - cd rideaware-api - ``` - --2. **Create a Virtual Environment** -- It is recommended to use a Python virtual environment to isolate dependencies. -+2. **Install Go Dependencies** - - ```bash -- python3 -m venv .venv -+ go mod tidy - ``` - --3. **Activate the Virtual Environment** -- - On Linux/Mac: -- ```bash -- source .venv/bin/activate -- ``` -- - On Windows: -- ```cmd -- .venv\Scripts\activate -- ``` -- --4. **Install Requirements** -- Install the required Python packages using pip: -+3. **Build the Application** - ```bash -- pip install -r requirements.txt -+ go build -o rideaware-api - ``` - - ### Configuration - - The application uses environment variables for configuration. Create a `.env` file in the root directory and define the following variables: - -+```env -+# Database Configuration -+PG_HOST=your_postgres_host -+PG_PORT=5432 -+PG_DATABASE=rideaware -+PG_USER=your_postgres_user -+PG_PASSWORD=your_postgres_password -+ -+# Application Configuration -+SECRET_KEY=your_secret_key_for_sessions -+PORT=8080 -+ -+# Email Configuration (Optional) -+SMTP_SERVER=your_smtp_server -+SMTP_PORT=465 -+SMTP_USER=your_email@domain.com -+SMTP_PASSWORD=your_email_password - ``` --DATABASE= -+ -+### Running the Application -+ -+#### Development Mode -+ -+```bash -+go run main.go -+``` -+ -+The application will be available at http://localhost:8080. -+ -+#### Production Mode -+ -+```bash -+./rideaware-api - ``` --- Replace `` with the URI of your database (e.g., SQLite, PostgreSQL). -+ -+### API Endpoints -+ -+- `GET /health` - Health check endpoint -+- `POST /auth/signup` - User registration -+- `POST /auth/login` - User authentication -+- `POST /auth/logout` - User logout - - ### Running with Docker - --To run the application in a containerized environment, you can use the provided Dockerfile. -+To run the application in a containerized environment: - - 1. **Build the Docker Image**: - -@@ -72,14 +97,60 @@ docker build -t rideaware-api . - 2. **Run the Container** - - ```bash --docker run -d -p 5000:5000 --env-file .env rideaware-api -+docker run -d -p 8080:8080 --env-file .env rideaware-api -+``` -+ -+The application will be available at http://localhost:8080. -+ -+### Example Dockerfile -+ -+```dockerfile -+FROM golang:1.21-alpine AS builder -+ -+WORKDIR /app -+COPY go.mod go.sum ./ -+RUN go mod download -+ -+COPY . . -+RUN go build -o rideaware-api -+ -+FROM alpine:latest -+RUN apk --no-cache add ca-certificates -+WORKDIR /root/ -+ -+COPY --from=builder /app/rideaware-api . -+CMD ["./rideaware-api"] - ``` - --The application will be available at http://127.0.0.1:5000. -+### Database Migration -+ -+The application automatically runs database migrations on startup using GORM's AutoMigrate feature. This will create the necessary tables: -+ -+- `users` - User accounts -+- `user_profiles` - User profile information - - ### Running Tests - --To be added. -+To run tests: -+ -+```bash -+go test ./... -+``` -+ -+To run tests with coverage: -+ -+```bash -+go test -cover ./... -+``` -+ -+### Development -+ -+To add new features: -+ -+1. Create models in the `models/` directory -+2. Add business logic in the `services/` directory -+3. Define API routes in the `routes/` directory -+4. Register routes in `main.go` - - ## Contributing - -@@ -87,5 +158,4 @@ Contributions are welcome! Please create a pull request or open an issue for any - - ## License - --This project is licensed under the AGPL-3.0 License. -- -+This project is licensed under the AGPL-3.0 License. -\ No newline at end of file -diff --git a/migrations/README b/migrations/README -deleted file mode 100644 -index 0e04844..0000000 ---- a/migrations/README -+++ /dev/null -@@ -1 +0,0 @@ --Single-database configuration for Flask. -diff --git a/migrations/alembic.ini b/migrations/alembic.ini -deleted file mode 100644 -index ec9d45c..0000000 ---- a/migrations/alembic.ini -+++ /dev/null -@@ -1,50 +0,0 @@ --# A generic, single database configuration. -- --[alembic] --# template used to generate migration files --# file_template = %%(rev)s_%%(slug)s -- --# set to 'true' to run the environment during --# the 'revision' command, regardless of autogenerate --# revision_environment = false -- -- --# Logging configuration --[loggers] --keys = root,sqlalchemy,alembic,flask_migrate -- --[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 -- --[logger_flask_migrate] --level = INFO --handlers = --qualname = flask_migrate -- --[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 -diff --git a/migrations/env.py b/migrations/env.py -deleted file mode 100644 -index 4c97092..0000000 ---- a/migrations/env.py -+++ /dev/null -@@ -1,113 +0,0 @@ --import logging --from logging.config import fileConfig -- --from flask import current_app -- --from alembic import context -- --# this is the Alembic Config object, which provides --# access to the values within the .ini file in use. --config = context.config -- --# Interpret the config file for Python logging. --# This line sets up loggers basically. --fileConfig(config.config_file_name) --logger = logging.getLogger('alembic.env') -- -- --def get_engine(): -- try: -- # this works with Flask-SQLAlchemy<3 and Alchemical -- return current_app.extensions['migrate'].db.get_engine() -- except (TypeError, AttributeError): -- # this works with Flask-SQLAlchemy>=3 -- return current_app.extensions['migrate'].db.engine -- -- --def get_engine_url(): -- try: -- return get_engine().url.render_as_string(hide_password=False).replace( -- '%', '%%') -- except AttributeError: -- return str(get_engine().url).replace('%', '%%') -- -- --# add your model's MetaData object here --# for 'autogenerate' support --# from myapp import mymodel --# target_metadata = mymodel.Base.metadata --config.set_main_option('sqlalchemy.url', get_engine_url()) --target_db = current_app.extensions['migrate'].db -- --# other values from the config, defined by the needs of env.py, --# can be acquired: --# my_important_option = config.get_main_option("my_important_option") --# ... etc. -- -- --def get_metadata(): -- if hasattr(target_db, 'metadatas'): -- return target_db.metadatas[None] -- return target_db.metadata -- -- --def run_migrations_offline(): -- """Run migrations in 'offline' mode. -- -- This configures the context with just a URL -- and not an Engine, though an Engine is acceptable -- here as well. By skipping the Engine creation -- we don't even need a DBAPI to be available. -- -- Calls to context.execute() here emit the given string to the -- script output. -- -- """ -- url = config.get_main_option("sqlalchemy.url") -- context.configure( -- url=url, target_metadata=get_metadata(), literal_binds=True -- ) -- -- with context.begin_transaction(): -- context.run_migrations() -- -- --def run_migrations_online(): -- """Run migrations in 'online' mode. -- -- In this scenario we need to create an Engine -- and associate a connection with the context. -- -- """ -- -- # this callback is used to prevent an auto-migration from being generated -- # when there are no changes to the schema -- # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html -- def process_revision_directives(context, revision, directives): -- if getattr(config.cmd_opts, 'autogenerate', False): -- script = directives[0] -- if script.upgrade_ops.is_empty(): -- directives[:] = [] -- logger.info('No changes in schema detected.') -- -- conf_args = current_app.extensions['migrate'].configure_args -- if conf_args.get("process_revision_directives") is None: -- conf_args["process_revision_directives"] = process_revision_directives -- -- connectable = get_engine() -- -- with connectable.connect() as connection: -- context.configure( -- connection=connection, -- target_metadata=get_metadata(), -- **conf_args -- ) -- -- with context.begin_transaction(): -- context.run_migrations() -- -- --if context.is_offline_mode(): -- run_migrations_offline() --else: -- run_migrations_online() -diff --git a/migrations/script.py.mako b/migrations/script.py.mako -deleted file mode 100644 -index 2c01563..0000000 ---- a/migrations/script.py.mako -+++ /dev/null -@@ -1,24 +0,0 @@ --"""${message} -- --Revision ID: ${up_revision} --Revises: ${down_revision | comma,n} --Create Date: ${create_date} -- --""" --from alembic import op --import sqlalchemy as sa --${imports if imports else ""} -- --# revision identifiers, used by Alembic. --revision = ${repr(up_revision)} --down_revision = ${repr(down_revision)} --branch_labels = ${repr(branch_labels)} --depends_on = ${repr(depends_on)} -- -- --def upgrade(): -- ${upgrades if upgrades else "pass"} -- -- --def downgrade(): -- ${downgrades if downgrades else "pass"} -diff --git a/migrations/versions/0e07095d2961_initial_migration.py b/migrations/versions/0e07095d2961_initial_migration.py -deleted file mode 100644 -index 594c8d6..0000000 ---- a/migrations/versions/0e07095d2961_initial_migration.py -+++ /dev/null -@@ -1,99 +0,0 @@ --"""Initial migration -- --Revision ID: 0e07095d2961 --Revises: --Create Date: 2025-08-29 01:28:57.822103 -- --""" --from alembic import op --import sqlalchemy as sa --from sqlalchemy.dialects import postgresql -- --# revision identifiers, used by Alembic. --revision = '0e07095d2961' --down_revision = None --branch_labels = None --depends_on = None -- -- --def upgrade(): -- # ### commands auto generated by Alembic - please adjust! ### -- op.drop_table('admins') -- with op.batch_alter_table('subscribers', schema=None) as batch_op: -- batch_op.drop_index(batch_op.f('idx_subscribers_created_at')) -- batch_op.drop_index(batch_op.f('idx_subscribers_email')) -- batch_op.drop_index(batch_op.f('idx_subscribers_status')) -- -- op.drop_table('subscribers') -- op.drop_table('admin_users') -- op.drop_table('email_deliveries') -- with op.batch_alter_table('newsletters', schema=None) as batch_op: -- batch_op.drop_index(batch_op.f('idx_newsletters_sent_at')) -- -- op.drop_table('newsletters') -- # ### end Alembic commands ### -- -- --def downgrade(): -- # ### commands auto generated by Alembic - please adjust! ### -- op.create_table('newsletters', -- sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('newsletters_id_seq'::regclass)"), autoincrement=True, nullable=False), -- sa.Column('subject', sa.TEXT(), autoincrement=False, nullable=False), -- sa.Column('body', sa.TEXT(), autoincrement=False, nullable=False), -- sa.Column('sent_at', postgresql.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), -- sa.Column('sent_by', sa.TEXT(), autoincrement=False, nullable=True), -- sa.Column('recipient_count', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True), -- sa.Column('success_count', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True), -- sa.Column('failure_count', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True), -- sa.PrimaryKeyConstraint('id', name='newsletters_pkey'), -- postgresql_ignore_search_path=False -- ) -- with op.batch_alter_table('newsletters', schema=None) as batch_op: -- batch_op.create_index(batch_op.f('idx_newsletters_sent_at'), [sa.literal_column('sent_at DESC')], unique=False) -- -- op.create_table('email_deliveries', -- sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), -- sa.Column('newsletter_id', sa.INTEGER(), autoincrement=False, nullable=True), -- sa.Column('email', sa.TEXT(), autoincrement=False, nullable=False), -- sa.Column('status', sa.TEXT(), autoincrement=False, nullable=True), -- sa.Column('sent_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), -- sa.Column('error_message', sa.TEXT(), autoincrement=False, nullable=True), -- sa.CheckConstraint("status = ANY (ARRAY['sent'::text, 'failed'::text, 'bounced'::text])", name=op.f('email_deliveries_status_check')), -- sa.ForeignKeyConstraint(['newsletter_id'], ['newsletters.id'], name=op.f('email_deliveries_newsletter_id_fkey')), -- sa.PrimaryKeyConstraint('id', name=op.f('email_deliveries_pkey')) -- ) -- op.create_table('admin_users', -- sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), -- sa.Column('username', sa.TEXT(), autoincrement=False, nullable=False), -- sa.Column('password', sa.TEXT(), autoincrement=False, nullable=False), -- sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), -- sa.Column('last_login', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), -- sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), -- sa.PrimaryKeyConstraint('id', name=op.f('admin_users_pkey')), -- sa.UniqueConstraint('username', name=op.f('admin_users_username_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) -- ) -- op.create_table('subscribers', -- sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), -- sa.Column('email', sa.TEXT(), autoincrement=False, nullable=False), -- sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), -- sa.Column('subscribed_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), -- sa.Column('status', sa.TEXT(), server_default=sa.text("'active'::text"), autoincrement=False, nullable=True), -- sa.Column('source', sa.TEXT(), server_default=sa.text("'manual'::text"), autoincrement=False, nullable=True), -- sa.CheckConstraint("status = ANY (ARRAY['active'::text, 'unsubscribed'::text])", name=op.f('subscribers_status_check')), -- sa.PrimaryKeyConstraint('id', name=op.f('subscribers_pkey')), -- sa.UniqueConstraint('email', name=op.f('subscribers_email_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) -- ) -- with op.batch_alter_table('subscribers', schema=None) as batch_op: -- batch_op.create_index(batch_op.f('idx_subscribers_status'), ['status'], unique=False) -- batch_op.create_index(batch_op.f('idx_subscribers_email'), ['email'], unique=False) -- batch_op.create_index(batch_op.f('idx_subscribers_created_at'), [sa.literal_column('created_at DESC')], unique=False) -- -- op.create_table('admins', -- sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), -- sa.Column('username', sa.VARCHAR(length=100), autoincrement=False, nullable=False), -- sa.Column('password_hash', sa.VARCHAR(length=255), autoincrement=False, nullable=False), -- sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), -- sa.PrimaryKeyConstraint('id', name=op.f('admins_pkey')), -- sa.UniqueConstraint('username', name=op.f('admins_username_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) -- ) -- # ### end Alembic commands ### -diff --git a/models/User/user.py b/models/User/user.py -deleted file mode 100644 -index 552796c..0000000 ---- a/models/User/user.py -+++ /dev/null -@@ -1,40 +0,0 @@ --from models.UserProfile.user_profile import UserProfile --from werkzeug.security import generate_password_hash, check_password_hash --from models import db --from sqlalchemy import event -- --class User(db.Model): -- __tablename__ = 'users' -- -- id = db.Column(db.Integer, primary_key=True) -- username = db.Column(db.String(80), unique=True, nullable=False) -- email = db.Column(db.String(120), unique=True, nullable=False) # Add email field -- _password = db.Column("password", db.String(255), nullable=False) -- -- profile = db.relationship('UserProfile', back_populates='user', uselist=False, cascade="all, delete-orphan") -- -- @property -- def password(self): -- return self._password -- -- @password.setter -- def password(self, raw_password): -- if not raw_password.startswith("pbkdf2:sha256:"): -- self._password = generate_password_hash(raw_password) -- else: -- self._password = raw_password -- -- def check_password(self, password): -- return check_password_hash(self._password, password) -- --@event.listens_for(User, 'after_insert') --def create_user_profile(mapper, connection, target): -- connection.execute( -- UserProfile.__table__.insert().values( -- user_id=target.id, -- first_name="", -- last_name="", -- bio="", -- profile_picture="" -- ) -- ) -\ No newline at end of file -diff --git a/models/UserProfile/user_profile.py b/models/UserProfile/user_profile.py -deleted file mode 100644 -index d3fa194..0000000 ---- a/models/UserProfile/user_profile.py -+++ /dev/null -@@ -1,13 +0,0 @@ --from models import db -- --class UserProfile(db.Model): -- __tablename__ = 'user_profiles' -- -- id = db.Column(db.Integer, primary_key=True) -- user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) -- first_name = db.Column(db.String(50), nullable=False, default="") -- last_name = db.Column(db.String(50), nullable=False, default="") -- bio = db.Column(db.Text, default="") -- profile_picture = db.Column(db.String(255), default="") -- -- user = db.relationship('User', back_populates='profile') -\ No newline at end of file -diff --git a/models/__init__.py b/models/__init__.py -deleted file mode 100644 -index 8dd3fe9..0000000 ---- a/models/__init__.py -+++ /dev/null -@@ -1,22 +0,0 @@ --import os --from flask_sqlalchemy import SQLAlchemy --from dotenv import load_dotenv --from urllib.parse import quote_plus -- --load_dotenv() -- --PG_USER = quote_plus(os.getenv("PG_USER", "postgres")) --PG_PASSWORD = quote_plus(os.getenv("PG_PASSWORD", "postgres")) --PG_HOST = os.getenv("PG_HOST", "localhost") --PG_PORT = os.getenv("PG_PORT", "5432") --PG_DATABASE = os.getenv("PG_DATABASE", "rideaware") -- --DATABASE_URI = f"postgresql+psycopg2://{PG_USER}:{PG_PASSWORD}@{PG_HOST}:{PG_PORT}/{PG_DATABASE}" -- --db = SQLAlchemy() -- --def init_db(app): -- """Initialize the SQLAlchemy app with the configuration.""" -- app.config['SQLALCHEMY_DATABASE_URI'] = DATABASE_URI -- app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False -- db.init_app(app) -diff --git a/requirements.txt b/requirements.txt -deleted file mode 100644 -index 001e473..0000000 ---- a/requirements.txt -+++ /dev/null -@@ -1,8 +0,0 @@ --Flask --flask_bcrypt --flask_cors --flask_sqlalchemy --python-dotenv --werkzeug --psycopg2-binary --Flask-Migrate -\ No newline at end of file -diff --git a/routes/user_auth/auth.py b/routes/user_auth/auth.py -deleted file mode 100644 -index 899d7ba..0000000 ---- a/routes/user_auth/auth.py -+++ /dev/null -@@ -1,60 +0,0 @@ --from flask import Blueprint, request, jsonify, session --from services.UserService.user import UserService -- --auth_bp = Blueprint("auth", __name__, url_prefix="/api") --user_service = UserService() -- --@auth_bp.route("/signup", methods=["POST"]) --def signup(): -- data = request.get_json() -- if not data: -- return jsonify({"message": "No data provided"}), 400 -- -- required_fields = ['username', 'password'] -- for field in required_fields: -- if not data.get(field): -- return jsonify({"message": f"{field} is required"}), 400 -- -- try: -- new_user = user_service.create_user( -- username=data["username"], -- password=data["password"], -- email=data.get("email"), -- first_name=data.get("first_name"), -- last_name=data.get("last_name") -- ) -- -- return jsonify({ -- "message": "User created successfully", -- "username": new_user.username, -- "user_id": new_user.id -- }), 201 -- -- except ValueError as e: -- return jsonify({"message": str(e)}), 400 -- except Exception as e: -- # Log the error -- print(f"Signup error: {e}") -- return jsonify({"message": "Internal server error"}), 500 -- --@auth_bp.route("/login", methods=["POST"]) --def login(): -- data = request.get_json() -- username = data.get("username") -- password = data.get("password") -- print(f"Login attempt: username={username}, password={password}") -- try: -- user = user_service.verify_user(username, password) -- session["user_id"] = user.id -- return jsonify({"message": "Login successful", "user_id": user.id}), 200 -- except ValueError as e: -- print(f"Login failed: {str(e)}") -- return jsonify({"error": str(e)}), 401 -- except Exception as e: -- print(f"Login error: {e}") -- return jsonify({"error": "Internal server error"}), 500 -- --@auth_bp.route("/logout", methods=["POST"]) --def logout(): -- session.clear() -- return jsonify({"message": "Logout successful"}), 200 -\ No newline at end of file -diff --git a/scripts/migrate.sh b/scripts/migrate.sh -deleted file mode 100644 -index 405f399..0000000 ---- a/scripts/migrate.sh -+++ /dev/null -@@ -1,8 +0,0 @@ --#!/bin/bash --set -e -- --echo "Running database migrations..." --flask db upgrade -- --echo "Starting application..." --exec "$@" -\ No newline at end of file -diff --git a/server.py b/server.py -deleted file mode 100644 -index 5800353..0000000 ---- a/server.py -+++ /dev/null -@@ -1,33 +0,0 @@ --import os --from flask import Flask --from flask_cors import CORS --from dotenv import load_dotenv --from flask_migrate import Migrate --from flask.cli import FlaskGroup -- --from models import db, init_db --from routes.user_auth import auth -- --load_dotenv() -- --app = Flask(__name__) --app.config["SECRET_KEY"] = os.getenv("SECRET_KEY") --app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE") --app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False -- --CORS(app) -- --init_db(app) --migrate = Migrate(app, db) --app.register_blueprint(auth.auth_bp) -- -- --@app.route("/health") --def health_check(): -- """Health check endpoint.""" -- return "OK", 200 -- --cli = FlaskGroup(app) -- --if __name__ == "__main__": -- cli() -\ No newline at end of file -diff --git a/services/UserService/user.py b/services/UserService/user.py -deleted file mode 100644 -index 6f1c030..0000000 ---- a/services/UserService/user.py -+++ /dev/null -@@ -1,60 +0,0 @@ --from models.User.user import User --from models.UserProfile.user_profile import UserProfile --from models import db --import re -- --class UserService: -- def create_user(self, username, password, email=None, first_name=None, last_name=None): -- if not username or not password: -- raise ValueError("Username and password are required") -- -- if email: -- email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' -- if not re.match(email_regex, email): -- raise ValueError("Invalid email format") -- -- existing_user = User.query.filter( -- (User.username == username) | (User.email == email) -- ).first() -- -- if existing_user: -- if existing_user.username == username: -- raise ValueError("Username already exists") -- else: -- raise ValueError("Email already exists") -- -- if len(password) < 8: -- raise ValueError("Password must be at least 8 characters long") -- -- try: -- new_user = User( -- username=username, -- email=email or "", -- password=password -- ) -- -- db.session.add(new_user) -- db.session.flush() -- -- user_profile = UserProfile( -- user_id=new_user.id, -- first_name=first_name or "", -- last_name=last_name or "", -- bio="", -- profile_picture="" -- ) -- -- db.session.add(user_profile) -- db.session.commit() -- -- return new_user -- -- except Exception as e: -- db.session.rollback() -- raise Exception(f"Error creating user: {str(e)}") -- -- def verify_user(self, username, password): -- user = User.query.filter_by(username=username).first() -- if not user or not user.check_password(password): -- raise ValueError("Invalid username or password") -- return user -\ No newline at end of file diff --git a/go.mod b/go.mod deleted file mode 100644 index 4f00f00..0000000 --- a/go.mod +++ /dev/null @@ -1,47 +0,0 @@ -module github.com/rideaware/rideaware-api - -go 1.21 - -require ( - github.com/gin-contrib/cors v1.4.0 - github.com/gin-contrib/sessions v0.0.5 - github.com/gin-gonic/gin v1.9.1 - github.com/joho/godotenv v1.4.0 - golang.org/x/crypto v0.12.0 - gorm.io/driver/postgres v1.5.2 - gorm.io/gorm v1.25.4 -) - -require ( - github.com/bytedance/sonic v1.9.1 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/gorilla/context v1.1.1 // indirect - github.com/gorilla/securecookie v1.1.1 // indirect - github.com/gorilla/sessions v1.2.1 // indirect - github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect - github.com/jackc/pgx/v5 v5.3.1 // indirect - github.com/jinzhu/inflection v1.0.0 // indirect - github.com/jinzhu/now v1.1.5 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.11.0 // indirect - golang.org/x/text v0.12.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 12370ad..0000000 --- a/go.sum +++ /dev/null @@ -1,153 +0,0 @@ -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/gin-contrib/cors v1.4.0 h1:oJ6gwtUl3lqV0WEIwM/LxPF1QZ5qe2lGWdY2+bz7y0g= -github.com/gin-contrib/cors v1.4.0/go.mod h1:bs9pNM0x/UsmHPBWT2xZz9ROh8xYjYkiURUfmBoMlcs= -github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE= -github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= -github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= -github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= -github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= -github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= -github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= -github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= -github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgx/v5 v5.3.1 h1:Fcr8QJ1ZeLi5zsPZqQeUZhNhxfkkKBOgJuYkJHoBOtU= -github.com/jackc/pgx/v5 v5.3.1/go.mod h1:t3JDKnCBlYIc0ewLF0Q7B8MXmoIaBOZj/ic7iHozM/8= -github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= -github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= -github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= -github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= -github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= -github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= -github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc= -golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gorm.io/driver/postgres v1.5.2 h1:ytTDxxEv+MplXOfFe3Lzm7SjG09fcdb3Z/c056DTBx0= -gorm.io/driver/postgres v1.5.2/go.mod h1:fmpX0m2I1PKuR7mKZiEluwrP3hbs+ps7JIGMUBpCgl8= -gorm.io/gorm v1.25.4 h1:iyNd8fNAe8W9dvtlgeRI5zSVZPsq3OpcTu37cYcpCmw= -gorm.io/gorm v1.25.4/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/main.go b/main.go deleted file mode 100644 index 8128f9f..0000000 --- a/main.go +++ /dev/null @@ -1,56 +0,0 @@ -package main - -import ( - "log" - "os" - - "github.com/gin-contrib/cors" - "github.com/gin-contrib/sessions" - "github.com/gin-contrib/sessions/cookie" - "github.com/gin-gonic/gin" - "github.com/joho/godotenv" - - "github.com/rideaware/rideaware-api/config" - "github.com/rideaware/rideaware-api/models" - "github.com/rideaware/rideaware-api/routes" -) - -func main() { - // Load environment variables - if err := godotenv.Load(); err != nil { - log.Println("No .env file found") - } - - // Initialize database - db := config.InitDB() - - // Auto migrate models - if err := db.AutoMigrate(&models.User{}, &models.UserProfile{}); err != nil { - log.Fatal("Failed to migrate database:", err) - } - - // Initialize Gin router - r := gin.Default() - - // CORS middleware - r.Use(cors.Default()) - - // Session middleware - store := cookie.NewStore([]byte(os.Getenv("SECRET_KEY"))) - r.Use(sessions.Sessions("session", store)) - - // Health check endpoint - r.GET("/health", func(c *gin.Context) { - c.String(200, "OK") - }) - - // Register auth routes - routes.RegisterAuthRoutes(r, db) - - // Start server - port := os.Getenv("PORT") - if port == "" { - port = "8080" - } - r.Run(":" + port) -} diff --git a/migrations/README b/migrations/README new file mode 100644 index 0000000..0e04844 --- /dev/null +++ b/migrations/README @@ -0,0 +1 @@ +Single-database configuration for Flask. diff --git a/migrations/alembic.ini b/migrations/alembic.ini new file mode 100644 index 0000000..ec9d45c --- /dev/null +++ b/migrations/alembic.ini @@ -0,0 +1,50 @@ +# A generic, single database configuration. + +[alembic] +# template used to generate migration files +# file_template = %%(rev)s_%%(slug)s + +# set to 'true' to run the environment during +# the 'revision' command, regardless of autogenerate +# revision_environment = false + + +# Logging configuration +[loggers] +keys = root,sqlalchemy,alembic,flask_migrate + +[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 + +[logger_flask_migrate] +level = INFO +handlers = +qualname = flask_migrate + +[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 diff --git a/migrations/env.py b/migrations/env.py new file mode 100644 index 0000000..4c97092 --- /dev/null +++ b/migrations/env.py @@ -0,0 +1,113 @@ +import logging +from logging.config import fileConfig + +from flask import current_app + +from alembic import context + +# this is the Alembic Config object, which provides +# access to the values within the .ini file in use. +config = context.config + +# Interpret the config file for Python logging. +# This line sets up loggers basically. +fileConfig(config.config_file_name) +logger = logging.getLogger('alembic.env') + + +def get_engine(): + try: + # this works with Flask-SQLAlchemy<3 and Alchemical + return current_app.extensions['migrate'].db.get_engine() + except (TypeError, AttributeError): + # this works with Flask-SQLAlchemy>=3 + return current_app.extensions['migrate'].db.engine + + +def get_engine_url(): + try: + return get_engine().url.render_as_string(hide_password=False).replace( + '%', '%%') + except AttributeError: + return str(get_engine().url).replace('%', '%%') + + +# add your model's MetaData object here +# for 'autogenerate' support +# from myapp import mymodel +# target_metadata = mymodel.Base.metadata +config.set_main_option('sqlalchemy.url', get_engine_url()) +target_db = current_app.extensions['migrate'].db + +# other values from the config, defined by the needs of env.py, +# can be acquired: +# my_important_option = config.get_main_option("my_important_option") +# ... etc. + + +def get_metadata(): + if hasattr(target_db, 'metadatas'): + return target_db.metadatas[None] + return target_db.metadata + + +def run_migrations_offline(): + """Run migrations in 'offline' mode. + + This configures the context with just a URL + and not an Engine, though an Engine is acceptable + here as well. By skipping the Engine creation + we don't even need a DBAPI to be available. + + Calls to context.execute() here emit the given string to the + script output. + + """ + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, target_metadata=get_metadata(), literal_binds=True + ) + + with context.begin_transaction(): + context.run_migrations() + + +def run_migrations_online(): + """Run migrations in 'online' mode. + + In this scenario we need to create an Engine + and associate a connection with the context. + + """ + + # this callback is used to prevent an auto-migration from being generated + # when there are no changes to the schema + # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html + def process_revision_directives(context, revision, directives): + if getattr(config.cmd_opts, 'autogenerate', False): + script = directives[0] + if script.upgrade_ops.is_empty(): + directives[:] = [] + logger.info('No changes in schema detected.') + + conf_args = current_app.extensions['migrate'].configure_args + if conf_args.get("process_revision_directives") is None: + conf_args["process_revision_directives"] = process_revision_directives + + connectable = get_engine() + + with connectable.connect() as connection: + context.configure( + connection=connection, + target_metadata=get_metadata(), + **conf_args + ) + + with context.begin_transaction(): + context.run_migrations() + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/migrations/script.py.mako b/migrations/script.py.mako new file mode 100644 index 0000000..2c01563 --- /dev/null +++ b/migrations/script.py.mako @@ -0,0 +1,24 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade(): + ${upgrades if upgrades else "pass"} + + +def downgrade(): + ${downgrades if downgrades else "pass"} diff --git a/migrations/versions/0e07095d2961_initial_migration.py b/migrations/versions/0e07095d2961_initial_migration.py new file mode 100644 index 0000000..594c8d6 --- /dev/null +++ b/migrations/versions/0e07095d2961_initial_migration.py @@ -0,0 +1,99 @@ +"""Initial migration + +Revision ID: 0e07095d2961 +Revises: +Create Date: 2025-08-29 01:28:57.822103 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +# revision identifiers, used by Alembic. +revision = '0e07095d2961' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('admins') + with op.batch_alter_table('subscribers', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('idx_subscribers_created_at')) + batch_op.drop_index(batch_op.f('idx_subscribers_email')) + batch_op.drop_index(batch_op.f('idx_subscribers_status')) + + op.drop_table('subscribers') + op.drop_table('admin_users') + op.drop_table('email_deliveries') + with op.batch_alter_table('newsletters', schema=None) as batch_op: + batch_op.drop_index(batch_op.f('idx_newsletters_sent_at')) + + op.drop_table('newsletters') + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('newsletters', + sa.Column('id', sa.INTEGER(), server_default=sa.text("nextval('newsletters_id_seq'::regclass)"), autoincrement=True, nullable=False), + sa.Column('subject', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('body', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('sent_at', postgresql.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.Column('sent_by', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('recipient_count', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True), + sa.Column('success_count', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True), + sa.Column('failure_count', sa.INTEGER(), server_default=sa.text('0'), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name='newsletters_pkey'), + postgresql_ignore_search_path=False + ) + with op.batch_alter_table('newsletters', schema=None) as batch_op: + batch_op.create_index(batch_op.f('idx_newsletters_sent_at'), [sa.literal_column('sent_at DESC')], unique=False) + + op.create_table('email_deliveries', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('newsletter_id', sa.INTEGER(), autoincrement=False, nullable=True), + sa.Column('email', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('status', sa.TEXT(), autoincrement=False, nullable=True), + sa.Column('sent_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.Column('error_message', sa.TEXT(), autoincrement=False, nullable=True), + sa.CheckConstraint("status = ANY (ARRAY['sent'::text, 'failed'::text, 'bounced'::text])", name=op.f('email_deliveries_status_check')), + sa.ForeignKeyConstraint(['newsletter_id'], ['newsletters.id'], name=op.f('email_deliveries_newsletter_id_fkey')), + sa.PrimaryKeyConstraint('id', name=op.f('email_deliveries_pkey')) + ) + op.create_table('admin_users', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('username', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('password', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.Column('last_login', postgresql.TIMESTAMP(timezone=True), autoincrement=False, nullable=True), + sa.Column('is_active', sa.BOOLEAN(), server_default=sa.text('true'), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('admin_users_pkey')), + sa.UniqueConstraint('username', name=op.f('admin_users_username_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + op.create_table('subscribers', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('email', sa.TEXT(), autoincrement=False, nullable=False), + sa.Column('created_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.Column('subscribed_at', postgresql.TIMESTAMP(timezone=True), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.Column('status', sa.TEXT(), server_default=sa.text("'active'::text"), autoincrement=False, nullable=True), + sa.Column('source', sa.TEXT(), server_default=sa.text("'manual'::text"), autoincrement=False, nullable=True), + sa.CheckConstraint("status = ANY (ARRAY['active'::text, 'unsubscribed'::text])", name=op.f('subscribers_status_check')), + sa.PrimaryKeyConstraint('id', name=op.f('subscribers_pkey')), + sa.UniqueConstraint('email', name=op.f('subscribers_email_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + with op.batch_alter_table('subscribers', schema=None) as batch_op: + batch_op.create_index(batch_op.f('idx_subscribers_status'), ['status'], unique=False) + batch_op.create_index(batch_op.f('idx_subscribers_email'), ['email'], unique=False) + batch_op.create_index(batch_op.f('idx_subscribers_created_at'), [sa.literal_column('created_at DESC')], unique=False) + + op.create_table('admins', + sa.Column('id', sa.INTEGER(), autoincrement=True, nullable=False), + sa.Column('username', sa.VARCHAR(length=100), autoincrement=False, nullable=False), + sa.Column('password_hash', sa.VARCHAR(length=255), autoincrement=False, nullable=False), + sa.Column('created_at', postgresql.TIMESTAMP(), server_default=sa.text('CURRENT_TIMESTAMP'), autoincrement=False, nullable=True), + sa.PrimaryKeyConstraint('id', name=op.f('admins_pkey')), + sa.UniqueConstraint('username', name=op.f('admins_username_key'), postgresql_include=[], postgresql_nulls_not_distinct=False) + ) + # ### end Alembic commands ### diff --git a/models/User/user.py b/models/User/user.py new file mode 100644 index 0000000..552796c --- /dev/null +++ b/models/User/user.py @@ -0,0 +1,40 @@ +from models.UserProfile.user_profile import UserProfile +from werkzeug.security import generate_password_hash, check_password_hash +from models import db +from sqlalchemy import event + +class User(db.Model): + __tablename__ = 'users' + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(80), unique=True, nullable=False) + email = db.Column(db.String(120), unique=True, nullable=False) # Add email field + _password = db.Column("password", db.String(255), nullable=False) + + profile = db.relationship('UserProfile', back_populates='user', uselist=False, cascade="all, delete-orphan") + + @property + def password(self): + return self._password + + @password.setter + def password(self, raw_password): + if not raw_password.startswith("pbkdf2:sha256:"): + self._password = generate_password_hash(raw_password) + else: + self._password = raw_password + + def check_password(self, password): + return check_password_hash(self._password, password) + +@event.listens_for(User, 'after_insert') +def create_user_profile(mapper, connection, target): + connection.execute( + UserProfile.__table__.insert().values( + user_id=target.id, + first_name="", + last_name="", + bio="", + profile_picture="" + ) + ) \ No newline at end of file diff --git a/models/UserProfile/user_profile.py b/models/UserProfile/user_profile.py new file mode 100644 index 0000000..d3fa194 --- /dev/null +++ b/models/UserProfile/user_profile.py @@ -0,0 +1,13 @@ +from models import db + +class UserProfile(db.Model): + __tablename__ = 'user_profiles' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + first_name = db.Column(db.String(50), nullable=False, default="") + last_name = db.Column(db.String(50), nullable=False, default="") + bio = db.Column(db.Text, default="") + profile_picture = db.Column(db.String(255), default="") + + user = db.relationship('User', back_populates='profile') \ No newline at end of file diff --git a/models/__init__.py b/models/__init__.py new file mode 100644 index 0000000..8dd3fe9 --- /dev/null +++ b/models/__init__.py @@ -0,0 +1,22 @@ +import os +from flask_sqlalchemy import SQLAlchemy +from dotenv import load_dotenv +from urllib.parse import quote_plus + +load_dotenv() + +PG_USER = quote_plus(os.getenv("PG_USER", "postgres")) +PG_PASSWORD = quote_plus(os.getenv("PG_PASSWORD", "postgres")) +PG_HOST = os.getenv("PG_HOST", "localhost") +PG_PORT = os.getenv("PG_PORT", "5432") +PG_DATABASE = os.getenv("PG_DATABASE", "rideaware") + +DATABASE_URI = f"postgresql+psycopg2://{PG_USER}:{PG_PASSWORD}@{PG_HOST}:{PG_PORT}/{PG_DATABASE}" + +db = SQLAlchemy() + +def init_db(app): + """Initialize the SQLAlchemy app with the configuration.""" + app.config['SQLALCHEMY_DATABASE_URI'] = DATABASE_URI + app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False + db.init_app(app) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..001e473 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,8 @@ +Flask +flask_bcrypt +flask_cors +flask_sqlalchemy +python-dotenv +werkzeug +psycopg2-binary +Flask-Migrate \ No newline at end of file diff --git a/routes/user_auth/auth.py b/routes/user_auth/auth.py new file mode 100644 index 0000000..899d7ba --- /dev/null +++ b/routes/user_auth/auth.py @@ -0,0 +1,60 @@ +from flask import Blueprint, request, jsonify, session +from services.UserService.user import UserService + +auth_bp = Blueprint("auth", __name__, url_prefix="/api") +user_service = UserService() + +@auth_bp.route("/signup", methods=["POST"]) +def signup(): + data = request.get_json() + if not data: + return jsonify({"message": "No data provided"}), 400 + + required_fields = ['username', 'password'] + for field in required_fields: + if not data.get(field): + return jsonify({"message": f"{field} is required"}), 400 + + try: + new_user = user_service.create_user( + username=data["username"], + password=data["password"], + email=data.get("email"), + first_name=data.get("first_name"), + last_name=data.get("last_name") + ) + + return jsonify({ + "message": "User created successfully", + "username": new_user.username, + "user_id": new_user.id + }), 201 + + except ValueError as e: + return jsonify({"message": str(e)}), 400 + except Exception as e: + # Log the error + print(f"Signup error: {e}") + return jsonify({"message": "Internal server error"}), 500 + +@auth_bp.route("/login", methods=["POST"]) +def login(): + data = request.get_json() + username = data.get("username") + password = data.get("password") + print(f"Login attempt: username={username}, password={password}") + try: + user = user_service.verify_user(username, password) + session["user_id"] = user.id + return jsonify({"message": "Login successful", "user_id": user.id}), 200 + except ValueError as e: + print(f"Login failed: {str(e)}") + return jsonify({"error": str(e)}), 401 + except Exception as e: + print(f"Login error: {e}") + return jsonify({"error": "Internal server error"}), 500 + +@auth_bp.route("/logout", methods=["POST"]) +def logout(): + session.clear() + return jsonify({"message": "Logout successful"}), 200 \ No newline at end of file diff --git a/scripts/migrate.sh b/scripts/migrate.sh new file mode 100644 index 0000000..405f399 --- /dev/null +++ b/scripts/migrate.sh @@ -0,0 +1,8 @@ +#!/bin/bash +set -e + +echo "Running database migrations..." +flask db upgrade + +echo "Starting application..." +exec "$@" \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..5800353 --- /dev/null +++ b/server.py @@ -0,0 +1,33 @@ +import os +from flask import Flask +from flask_cors import CORS +from dotenv import load_dotenv +from flask_migrate import Migrate +from flask.cli import FlaskGroup + +from models import db, init_db +from routes.user_auth import auth + +load_dotenv() + +app = Flask(__name__) +app.config["SECRET_KEY"] = os.getenv("SECRET_KEY") +app.config["SQLALCHEMY_DATABASE_URI"] = os.getenv("DATABASE") +app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False + +CORS(app) + +init_db(app) +migrate = Migrate(app, db) +app.register_blueprint(auth.auth_bp) + + +@app.route("/health") +def health_check(): + """Health check endpoint.""" + return "OK", 200 + +cli = FlaskGroup(app) + +if __name__ == "__main__": + cli() \ No newline at end of file diff --git a/services/UserService/user.py b/services/UserService/user.py new file mode 100644 index 0000000..6f1c030 --- /dev/null +++ b/services/UserService/user.py @@ -0,0 +1,60 @@ +from models.User.user import User +from models.UserProfile.user_profile import UserProfile +from models import db +import re + +class UserService: + def create_user(self, username, password, email=None, first_name=None, last_name=None): + if not username or not password: + raise ValueError("Username and password are required") + + if email: + email_regex = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' + if not re.match(email_regex, email): + raise ValueError("Invalid email format") + + existing_user = User.query.filter( + (User.username == username) | (User.email == email) + ).first() + + if existing_user: + if existing_user.username == username: + raise ValueError("Username already exists") + else: + raise ValueError("Email already exists") + + if len(password) < 8: + raise ValueError("Password must be at least 8 characters long") + + try: + new_user = User( + username=username, + email=email or "", + password=password + ) + + db.session.add(new_user) + db.session.flush() + + user_profile = UserProfile( + user_id=new_user.id, + first_name=first_name or "", + last_name=last_name or "", + bio="", + profile_picture="" + ) + + db.session.add(user_profile) + db.session.commit() + + return new_user + + except Exception as e: + db.session.rollback() + raise Exception(f"Error creating user: {str(e)}") + + def verify_user(self, username, password): + user = User.query.filter_by(username=username).first() + if not user or not user.check_password(password): + raise ValueError("Invalid username or password") + return user \ No newline at end of file