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.go b/models/user.go deleted file mode 100644 index 70b4da1..0000000 --- a/models/user.go +++ /dev/null @@ -1,40 +0,0 @@ -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 -} diff --git a/models/user_profile.go b/models/user_profile.go deleted file mode 100644 index f628328..0000000 --- a/models/user_profile.go +++ /dev/null @@ -1,12 +0,0 @@ -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"` -} 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/auth.go b/routes/auth.go deleted file mode 100644 index 5e290b8..0000000 --- a/routes/auth.go +++ /dev/null @@ -1,89 +0,0 @@ -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("/auth") - { - 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"}) - } -} 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/email_service.go b/services/email_service.go deleted file mode 100644 index 30a8c93..0000000 --- a/services/email_service.go +++ /dev/null @@ -1,34 +0,0 @@ -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)) -} diff --git a/services/user_service.go b/services/user_service.go deleted file mode 100644 index 1640cb4..0000000 --- a/services/user_service.go +++ /dev/null @@ -1,73 +0,0 @@ -package services - -import ( - "errors" - "log" - "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) { - 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 !strings.Contains(email, "@") { - 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") - } - - // Create new user - user := models.User{ - Username: username, - Email: 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 - // Allow login with either username or email - if err := s.db.Where("username = ? OR email = ?", username, username).First(&user).Error; err != nil { - log.Printf("User not found: %s", username) - return nil, errors.New("invalid username or password") - } - - if !user.CheckPassword(password) { - log.Printf("Invalid password for user: %s", username) - return nil, errors.New("invalid username or password") - } - - log.Printf("User verified: %s", username) - return &user, nil -}