Compare commits
20 commits
main
...
go-rewrite
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4036a6fc49 | ||
|
|
9f471905b5 | ||
|
|
e65b56028e | ||
|
|
7b1df7bce1 | ||
|
|
3fec9d794d | ||
|
|
daa90869cf | ||
|
|
afd12684b6 | ||
|
|
7620a938f5 | ||
|
|
56c8d3a786 | ||
|
|
a32b2a4210 | ||
|
|
18b6f74ab5 | ||
|
|
db2bedd1d4 | ||
|
|
85f8196ef0 | ||
|
|
5e0ac13488 | ||
|
|
485320737b | ||
|
|
bb91aff5fd | ||
|
|
3d9de8ba11 | ||
|
|
3ed698918b | ||
|
|
e40a119bdc | ||
|
|
ee836fa4c8 |
25 changed files with 726 additions and 599 deletions
|
|
@ -1,9 +1,20 @@
|
|||
.env
|
||||
.git
|
||||
.gitignore
|
||||
README.md
|
||||
Dockerfile
|
||||
.dockerignore
|
||||
.air.toml
|
||||
tmp/
|
||||
.vscode/
|
||||
.idea/
|
||||
*.log
|
||||
coverage.out
|
||||
rideaware-api
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.log
|
||||
!.env
|
||||
venv/
|
||||
.venv/
|
||||
venv/
|
||||
dist/
|
||||
build/
|
||||
build/
|
||||
node_modules/
|
||||
71
Dockerfile
71
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"]
|
||||
# Run the application
|
||||
CMD ["./rideaware-api"]
|
||||
128
README.md
128
README.md
|
|
@ -15,53 +15,80 @@ 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
|
||||
```bash
|
||||
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 should be a random 32+ byte string; rotate on compromise.
|
||||
SECRET_KEY=your_secret_key_for_sessions
|
||||
PORT=8080
|
||||
PG_SSLMODE=require # use "disable" only for local dev
|
||||
|
||||
# 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](http://localhost:8080).
|
||||
|
||||
#### Production Mode
|
||||
|
||||
```bash
|
||||
./rideaware-api
|
||||
```
|
||||
|
||||
### API Endpoints
|
||||
|
||||
- `GET /health` - Health check endpoint
|
||||
- `POST /auth/signup` - User registration
|
||||
- `POST /auth/login` - User authentication
|
||||
- `POST /auth/logout` - User logout
|
||||
|
||||
### Running with Docker
|
||||
|
||||
To run the application in a containerized environment, you can use the provided Dockerfile.
|
||||
To run the application in a containerized environment:
|
||||
|
||||
1. **Build the Docker Image**:
|
||||
|
||||
|
|
@ -72,14 +99,60 @@ docker build -t rideaware-api .
|
|||
2. **Run the Container**
|
||||
|
||||
```bash
|
||||
docker run -d -p 5000:5000 --env-file .env rideaware-api
|
||||
docker run -d -p 8080:8080 --env-file .env rideaware-api
|
||||
```
|
||||
|
||||
The application will be available at http://127.0.0.1:5000.
|
||||
The application will be available at [http://localhost:8080](http://localhost:8080).
|
||||
|
||||
### Example Dockerfile
|
||||
|
||||
```dockerfile
|
||||
FROM golang:1.21-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN go build -o rideaware-api
|
||||
|
||||
FROM alpine:latest
|
||||
RUN apk --no-cache add ca-certificates
|
||||
WORKDIR /root/
|
||||
|
||||
COPY --from=builder /app/rideaware-api .
|
||||
CMD ["./rideaware-api"]
|
||||
```
|
||||
|
||||
### Database Migration
|
||||
|
||||
The application automatically runs database migrations on startup using GORM's AutoMigrate feature. This will create the necessary tables:
|
||||
|
||||
- `users` - User accounts
|
||||
- `user_profiles` - User profile information
|
||||
|
||||
### Running Tests
|
||||
|
||||
To be added.
|
||||
To run tests:
|
||||
|
||||
```bash
|
||||
go test ./...
|
||||
```
|
||||
|
||||
To run tests with coverage:
|
||||
|
||||
```bash
|
||||
go test -cover ./...
|
||||
```
|
||||
|
||||
### Development
|
||||
|
||||
To add new features:
|
||||
|
||||
1. Create models in the `models/` directory
|
||||
2. Add business logic in the `services/` directory
|
||||
3. Define API routes in the `routes/` directory
|
||||
4. Register routes in `main.go`
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
@ -87,5 +160,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.
|
||||
42
config/database.go
Normal file
42
config/database.go
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"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")
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
log.Fatal("Failed to get sql.DB from gorm:", err)
|
||||
}
|
||||
|
||||
sqlDB.SetMaxOpenConns(25)
|
||||
sqlDB.SetMaxIdleConns(25)
|
||||
sqlDB.SetConnMaxLifetime(30 * time.Minute)
|
||||
|
||||
if err != nil {
|
||||
log.Fatal("Database ping failed:", err)
|
||||
}
|
||||
|
||||
return db
|
||||
}
|
||||
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=
|
||||
76
main.go
Normal file
76
main.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"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
|
||||
secret := os.Getenv("SECRET_KEY")
|
||||
if len(secret) < 32 {
|
||||
log.Fatal("SECRET_KEY must be at least 32 bytes")
|
||||
}
|
||||
|
||||
authKey := []byte(secret)
|
||||
encKey := []byte(secret[:32])
|
||||
store := cookie.NewStore(authKey, encKey)
|
||||
store.Options(sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 60 * 80 * 24 * 7, // 7 days
|
||||
HttpOnly: true,
|
||||
Secure: os.Getenv("ENV") == "production",
|
||||
SameSite: func() http.SameSite {
|
||||
if os.Getenv("CORS_ORIGINS") != "" {
|
||||
return http.SameSiteNoneMode
|
||||
}
|
||||
return http.SameSiteLaxMode
|
||||
}(),
|
||||
})
|
||||
r.Use(sessions.Sessions("rideaware-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 = "5080"
|
||||
}
|
||||
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)
|
||||
40
models/user.go
Normal file
40
models/user.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package models
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"unique;not null;size:80" json:"username"`
|
||||
Email string `gorm:"unique;not null;size:255" json:"email"` // Add this line
|
||||
Password string `gorm:"not null;size:255" json:"-"`
|
||||
|
||||
Profile *UserProfile `gorm:"constraint:OnDelete:CASCADE;" json:"profile,omitempty"`
|
||||
}
|
||||
|
||||
func (u *User) SetPassword(password string) error {
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password = string(hashedPassword)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *User) CheckPassword(password string) bool {
|
||||
err := bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func (u *User) AfterCreate(tx *gorm.DB) error {
|
||||
profile := UserProfile{
|
||||
UserID: u.ID,
|
||||
FirstName: "",
|
||||
LastName: "",
|
||||
Bio: "",
|
||||
ProfilePicture: "",
|
||||
}
|
||||
return tx.Create(&profile).Error
|
||||
}
|
||||
12
models/user_profile.go
Normal file
12
models/user_profile.go
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package models
|
||||
|
||||
type UserProfile struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
UserID uint `gorm:"not null" json:"user_id"`
|
||||
FirstName string `gorm:"size:80;not null" json:"first_name"`
|
||||
LastName string `gorm:"size:80;not null" json:"last_name"`
|
||||
Bio string `gorm:"type:text" json:"bio"`
|
||||
ProfilePicture string `gorm:"size:255" json:"profile_picture"`
|
||||
|
||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
}
|
||||
|
|
@ -1,8 +0,0 @@
|
|||
Flask
|
||||
flask_bcrypt
|
||||
flask_cors
|
||||
flask_sqlalchemy
|
||||
python-dotenv
|
||||
werkzeug
|
||||
psycopg2-binary
|
||||
Flask-Migrate
|
||||
89
routes/auth.go
Normal file
89
routes/auth.go
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
package routes
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/rideaware/rideaware-api/services"
|
||||
)
|
||||
|
||||
func RegisterAuthRoutes(r *gin.Engine, db *gorm.DB) {
|
||||
userService := services.NewUserService(db)
|
||||
|
||||
auth := r.Group("/api")
|
||||
{
|
||||
auth.POST("/signup", signup(userService))
|
||||
auth.POST("/login", login(userService))
|
||||
auth.POST("/logout", logout())
|
||||
}
|
||||
}
|
||||
|
||||
func signup(userService *services.UserService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Email string `json:"email" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := userService.CreateUser(req.Username, req.Email, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"message": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, gin.H{
|
||||
"message": "User created successfully",
|
||||
"username": user.Username,
|
||||
"email": user.Email,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func login(userService *services.UserService) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var req struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := userService.VerifyUser(req.Username, req.Password)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Set session
|
||||
session := sessions.Default(c)
|
||||
session.Set("user_id", user.ID)
|
||||
session.Save()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"message": "Login successful",
|
||||
"user_id": user.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func logout() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
session.Clear()
|
||||
session.Save()
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "Logout successful"})
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
34
services/email_service.go
Normal file
34
services/email_service.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/smtp"
|
||||
"os"
|
||||
)
|
||||
|
||||
type EmailService struct {
|
||||
smtpHost string
|
||||
smtpPort string
|
||||
smtpUser string
|
||||
smtpPassword string
|
||||
}
|
||||
|
||||
func NewEmailService() *EmailService {
|
||||
return &EmailService{
|
||||
smtpHost: os.Getenv("SMTP_SERVER"),
|
||||
smtpPort: os.Getenv("SMTP_PORT"),
|
||||
smtpUser: os.Getenv("SMTP_USER"),
|
||||
smtpPassword: os.Getenv("SMTP_PASSWORD"),
|
||||
}
|
||||
}
|
||||
|
||||
func (e *EmailService) SendEmail(to, subject, body string) error {
|
||||
from := e.smtpUser
|
||||
|
||||
msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\n\r\n%s", from, to, subject, body)
|
||||
|
||||
auth := smtp.PlainAuth("", e.smtpUser, e.smtpPassword, e.smtpHost)
|
||||
addr := fmt.Sprintf("%s:%s", e.smtpHost, e.smtpPort)
|
||||
|
||||
return smtp.SendMail(addr, auth, from, []string{to}, []byte(msg))
|
||||
}
|
||||
83
services/user_service.go
Normal file
83
services/user_service.go
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
package services
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"log"
|
||||
"net/mail"
|
||||
"strings"
|
||||
|
||||
"github.com/rideaware/rideaware-api/models"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewUserService(db *gorm.DB) *UserService {
|
||||
return &UserService{db: db}
|
||||
}
|
||||
|
||||
func (s *UserService) CreateUser(username, email, password string) (*models.User, error) {
|
||||
username = strings.TrimSpace(username)
|
||||
email = strings.ToLower(strings.TrimSpace(email))
|
||||
if username == "" || email == "" || password == "" {
|
||||
return nil, errors.New("username, email, and password are required")
|
||||
}
|
||||
|
||||
if len(username) < 3 || len(password) < 8 {
|
||||
return nil, errors.New("username must be at least 3 characters and password must be at least 8 characters")
|
||||
}
|
||||
|
||||
// Basic email validation
|
||||
if _, err := mail.ParseAddress(email); err != nil {
|
||||
return nil, errors.New("invalid email format")
|
||||
}
|
||||
|
||||
// Check if user exists (by username or email)
|
||||
var existingUser models.User
|
||||
if err := s.db.Where("username = ? OR email = ?", username, email).First(&existingUser).Error; err == nil {
|
||||
return nil, errors.New("user with this username or email already exists")
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
log.Printf("Error checking existing users: %v", err)
|
||||
return nil, errors.New("could not create user")
|
||||
}
|
||||
|
||||
// Create new user
|
||||
user := models.User{
|
||||
Username: username,
|
||||
Email: strings.ToLower(email),
|
||||
}
|
||||
if err := user.SetPassword(password); err != nil {
|
||||
log.Printf("Error hashing password: %v", err)
|
||||
return nil, errors.New("could not create user")
|
||||
}
|
||||
|
||||
if err := s.db.Create(&user).Error; err != nil {
|
||||
log.Printf("Error creating user: %v", err)
|
||||
return nil, errors.New("could not create user")
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) VerifyUser(username, password string) (*models.User, error) {
|
||||
var user models.User
|
||||
identifier := strings.TrimSpace(username)
|
||||
lid := strings.ToLower(identifier)
|
||||
if err := s.db.Where("username = ? OR LOWER(email) = ?", identifier, lid).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("invalid username or password")
|
||||
}
|
||||
log.Printf("DB error during VerifyUser: %v", err)
|
||||
return nil, errors.New("invalid username or password")
|
||||
}
|
||||
|
||||
if !user.CheckPassword(password) {
|
||||
log.Printf("Invalid credentials")
|
||||
return nil, errors.New("invalid username or password")
|
||||
}
|
||||
|
||||
log.Printf("User login succeeded")
|
||||
return &user, nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue