Compare commits
	
		
			9 commits
		
	
	
		
			ac0b09ad0e
			...
			111aab26bb
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
							 | 
						111aab26bb | ||
| 
							 | 
						3028e8f067 | ||
| 
							 | 
						5e0ac13488 | ||
| 
							 | 
						485320737b | ||
| 
							 | 
						bb91aff5fd | ||
| 
							 | 
						3d9de8ba11 | ||
| 
							 | 
						3ed698918b | ||
| 
							 | 
						e40a119bdc | ||
| 
							 | 
						ee836fa4c8 | 
					 21 changed files with 1342 additions and 601 deletions
				
			
		| 
						 | 
					@ -1,9 +1,13 @@
 | 
				
			||||||
 | 
					.env
 | 
				
			||||||
.git
 | 
					.git
 | 
				
			||||||
__pycache__/
 | 
					.gitignore
 | 
				
			||||||
*.py[cod]
 | 
					README.md
 | 
				
			||||||
 | 
					Dockerfile
 | 
				
			||||||
 | 
					.dockerignore
 | 
				
			||||||
 | 
					.air.toml
 | 
				
			||||||
 | 
					tmp/
 | 
				
			||||||
 | 
					.vscode/
 | 
				
			||||||
 | 
					.idea/
 | 
				
			||||||
*.log
 | 
					*.log
 | 
				
			||||||
!.env
 | 
					coverage.out
 | 
				
			||||||
venv/
 | 
					rideaware-api
 | 
				
			||||||
.venv/
 | 
					 | 
				
			||||||
dist/
 | 
					 | 
				
			||||||
build/
 | 
					 | 
				
			||||||
							
								
								
									
										71
									
								
								Dockerfile
									
										
									
									
									
								
							
							
						
						
									
										71
									
								
								Dockerfile
									
										
									
									
									
								
							| 
						 | 
					@ -1,53 +1,52 @@
 | 
				
			||||||
FROM python:3.10-slim AS builder
 | 
					# Build stage
 | 
				
			||||||
 | 
					FROM golang:1.21-alpine AS builder
 | 
				
			||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
 | 
					 | 
				
			||||||
    PYTHONUNBUFFERED=1 \
 | 
					 | 
				
			||||||
    PIP_NO_CACHE_DIR=1
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Set working directory
 | 
				
			||||||
WORKDIR /app
 | 
					WORKDIR /app
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
 | 
					# Install git (needed for some Go modules)
 | 
				
			||||||
    build-essential gcc \
 | 
					RUN apk add --no-cache git
 | 
				
			||||||
 && rm -rf /var/lib/apt/lists/*
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
COPY requirements.txt .
 | 
					# Copy go mod files
 | 
				
			||||||
 | 
					COPY go.mod go.sum ./
 | 
				
			||||||
 | 
					
 | 
				
			||||||
RUN python -m pip install --upgrade pip && \
 | 
					# Download dependencies
 | 
				
			||||||
    pip wheel --no-deps -r requirements.txt -w /wheels && \
 | 
					RUN go mod download
 | 
				
			||||||
    pip wheel --no-deps gunicorn -w /wheels
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
FROM python:3.10-slim AS runtime
 | 
					# Copy source code
 | 
				
			||||||
 | 
					COPY . .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
ENV PYTHONDONTWRITEBYTECODE=1 \
 | 
					# Build the application
 | 
				
			||||||
    PYTHONUNBUFFERED=1 \
 | 
					RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o rideaware-api .
 | 
				
			||||||
    PIP_NO_CACHE_DIR=1 \
 | 
					 | 
				
			||||||
    PORT=5000 \
 | 
					 | 
				
			||||||
    WSGI_MODULE=server:app \
 | 
					 | 
				
			||||||
    GUNICORN_WORKERS=2 \
 | 
					 | 
				
			||||||
    GUNICORN_THREADS=4 \
 | 
					 | 
				
			||||||
    GUNICORN_TIMEOUT=60 \
 | 
					 | 
				
			||||||
    FLASK_APP=server.py
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
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
 | 
					# Create non-root user
 | 
				
			||||||
RUN pip install --no-cache-dir /wheels/* && rm -rf /wheels
 | 
					RUN addgroup -g 1001 -S appgroup && \
 | 
				
			||||||
 | 
					    adduser -u 1001 -S appuser -G appgroup
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Install python-dotenv if not already in requirements.txt
 | 
					# Set working directory
 | 
				
			||||||
RUN pip install python-dotenv
 | 
					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
 | 
					# Switch to non-root user
 | 
				
			||||||
COPY --chown=app:app .env .env
 | 
					USER appuser
 | 
				
			||||||
 | 
					
 | 
				
			||||||
EXPOSE 5000
 | 
					# Expose port
 | 
				
			||||||
 | 
					EXPOSE 8080
 | 
				
			||||||
 | 
					
 | 
				
			||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
 | 
					# Health check
 | 
				
			||||||
  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()"
 | 
					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"]
 | 
					# Run the application
 | 
				
			||||||
 | 
					CMD ["./rideaware-api"]
 | 
				
			||||||
							
								
								
									
										122
									
								
								README.md
									
										
									
									
									
								
							
							
						
						
									
										122
									
								
								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:
 | 
					Ensure you have the following installed on your system:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Docker
 | 
					- Docker
 | 
				
			||||||
- Python 3.10 or later
 | 
					- Go 1.21 or later
 | 
				
			||||||
- pip
 | 
					- PostgreSQL (for local development, optional)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Setting Up the Project
 | 
					### Setting Up the Project
 | 
				
			||||||
 | 
					
 | 
				
			||||||
1. **Clone the Repository**  
 | 
					1. **Clone the Repository**  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
   ```bash
 | 
					   ```bash
 | 
				
			||||||
   git clone https://github.com/VeloInnovate/rideaware-api.git
 | 
					   git clone https://github.com/rideaware/rideaware-api.git
 | 
				
			||||||
   cd rideaware-api
 | 
					   cd rideaware-api
 | 
				
			||||||
   ```
 | 
					   ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
2. **Create a Virtual Environment**  
 | 
					2. **Install Go Dependencies**  
 | 
				
			||||||
   It is recommended to use a Python virtual environment to isolate dependencies.
 | 
					 | 
				
			||||||
   
 | 
					   
 | 
				
			||||||
	```bash
 | 
						```bash
 | 
				
			||||||
   python3 -m venv .venv
 | 
					   go mod tidy
 | 
				
			||||||
   ```
 | 
					   ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
3. **Activate the Virtual Environment**
 | 
					3. **Build the Application**
 | 
				
			||||||
   - 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
 | 
					   ```bash
 | 
				
			||||||
   pip install -r requirements.txt
 | 
					   go build -o rideaware-api
 | 
				
			||||||
   ```
 | 
					   ```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Configuration
 | 
					### Configuration
 | 
				
			||||||
 | 
					
 | 
				
			||||||
The application uses environment variables for configuration. Create a `.env` file in the root directory and define the following variables:
 | 
					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=<your_database_connection_string>
 | 
					
 | 
				
			||||||
 | 
					### Running the Application
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Development Mode
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					```bash
 | 
				
			||||||
 | 
					go run main.go
 | 
				
			||||||
```
 | 
					```
 | 
				
			||||||
- Replace `<your_database_connection_string>` 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
 | 
					### 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**:  
 | 
					1. **Build the Docker Image**:  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -72,14 +97,60 @@ docker build -t rideaware-api .
 | 
				
			||||||
2. **Run the Container**
 | 
					2. **Run the Container**
 | 
				
			||||||
 | 
					
 | 
				
			||||||
```bash
 | 
					```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
 | 
					### 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
 | 
					## Contributing
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -88,4 +159,3 @@ Contributions are welcome! Please create a pull request or open an issue for any
 | 
				
			||||||
## License
 | 
					## License
 | 
				
			||||||
 | 
					
 | 
				
			||||||
This project is licensed under the AGPL-3.0 License.
 | 
					This project is licensed under the AGPL-3.0 License.
 | 
				
			||||||
 | 
					 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										28
									
								
								config/database.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								config/database.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										915
									
								
								diff.txt
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										915
									
								
								diff.txt
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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=<your_database_connection_string>
 | 
				
			||||||
 | 
					+
 | 
				
			||||||
 | 
					+### 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 `<your_database_connection_string>` 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
 | 
				
			||||||
							
								
								
									
										47
									
								
								go.mod
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								go.mod
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										153
									
								
								go.sum
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										153
									
								
								go.sum
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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=
 | 
				
			||||||
							
								
								
									
										56
									
								
								main.go
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								main.go
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1 +0,0 @@
 | 
				
			||||||
Single-database configuration for Flask.
 | 
					 | 
				
			||||||
| 
						 | 
					@ -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
 | 
					 | 
				
			||||||
| 
						 | 
					@ -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()
 | 
					 | 
				
			||||||
| 
						 | 
					@ -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"}
 | 
					 | 
				
			||||||
| 
						 | 
					@ -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 ###
 | 
					 | 
				
			||||||
| 
						 | 
					@ -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=""
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
| 
						 | 
					@ -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')
 | 
					 | 
				
			||||||
| 
						 | 
					@ -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)
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,8 +0,0 @@
 | 
				
			||||||
Flask
 | 
					 | 
				
			||||||
flask_bcrypt
 | 
					 | 
				
			||||||
flask_cors
 | 
					 | 
				
			||||||
flask_sqlalchemy
 | 
					 | 
				
			||||||
python-dotenv
 | 
					 | 
				
			||||||
werkzeug
 | 
					 | 
				
			||||||
psycopg2-binary
 | 
					 | 
				
			||||||
Flask-Migrate
 | 
					 | 
				
			||||||
| 
						 | 
					@ -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
 | 
					 | 
				
			||||||
| 
						 | 
					@ -1,8 +0,0 @@
 | 
				
			||||||
#!/bin/bash
 | 
					 | 
				
			||||||
set -e
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
echo "Running database migrations..."
 | 
					 | 
				
			||||||
flask db upgrade
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
echo "Starting application..."
 | 
					 | 
				
			||||||
exec "$@"
 | 
					 | 
				
			||||||
							
								
								
									
										33
									
								
								server.py
									
										
									
									
									
								
							
							
						
						
									
										33
									
								
								server.py
									
										
									
									
									
								
							| 
						 | 
					@ -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()
 | 
					 | 
				
			||||||
| 
						 | 
					@ -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
 | 
					 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue