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 ``` -- Replace `` with the URI of your database (e.g., SQLite, PostgreSQL). + +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 ### 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://127.0.0.1:5000. +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 ### 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/config/database.go b/config/database.go new file mode 100644 index 0000000..be6ad8e --- /dev/null +++ b/config/database.go @@ -0,0 +1,28 @@ +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 new file mode 100644 index 0000000..7394524 --- /dev/null +++ b/diff.txt @@ -0,0 +1,915 @@ +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 new file mode 100644 index 0000000..4f00f00 --- /dev/null +++ b/go.mod @@ -0,0 +1,47 @@ +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 new file mode 100644 index 0000000..12370ad --- /dev/null +++ b/go.sum @@ -0,0 +1,153 @@ +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 new file mode 100644 index 0000000..8128f9f --- /dev/null +++ b/main.go @@ -0,0 +1,56 @@ +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 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