Permalink
Browse files

first commit

  • Loading branch information...
0 parents commit 895dd073a87fec5a0d963d172730fab1682bd3dc @maccman committed Dec 23, 2008
Showing 2,446 changed files with 366,201 additions and 0 deletions.
19 Capfile
@@ -0,0 +1,19 @@
+# Configuration
+role :app, "account.crowdmod.com"
+role :web, "account.crowdmod.com"
+role :db, "account.crowdmod.com", :primary => true
+
+set :application, "saas"
+set :use_mysql, false
+set :thin_socket, true
+set :thin_port, 8000
+set :thin_number, 1
+
+set :svn_user, ENV['svn_user'] || "alex"
+set :svn_url, "svn://svn.alexmaccaw.co.uk:8100"
+
+ssh_options[:keys] = File.expand_path('~/keys/aireo_keypair')
+
+load 'deploy' if respond_to?(:namespace) # cap2 differentiator
+Dir['vendor/plugins/*/recipes/*.rb'].each { |plugin| load(plugin) }
+load 'config/deploy'
@@ -0,0 +1,20 @@
+Copyright (c) 2008 Made By Many
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
87 README
@@ -0,0 +1,87 @@
+== Welcome to Saasy
+
+Saasy is a Rails app that bills and authenticates, so you don't have to.
+The idea is that you host Saasy on a subdomain, and communicate with it using SSO/REST protocols. That means you're free to do more interesting coding.
+
+*This is alpha code - use at your own risk*
+
+I'd like to thank "Made by Many":http://madebymany.co.uk for supporting this project.
+
+"Alex MacCaw":http://eribium.org (info@eribium.org).
+
+== Screenshots
+* "Billing":doc/Saasy_Billing.png
+* "Sign up":doc/Saasy_Signup.png
+* "Edit profile":doc/Saasy_Edit_Profile.png
+
+== Overview
+
+* Subscription management
+* Recurring billing
+* Credit card management
+* User authentication and SSO
+
+== Features
+
+* No local credit card storage
+* Automated billing script that should be run nightly
+* Configurable subscription plans (price/duration)
+* SSL protection for account creation (and when updating CC info)
+* Account can have multiple users, interface for adding more
+* Trial ending mailer
+* Invoice mailer
+* Automated notification and retry of failed renewals
+* Plan upgrade/downgrades
+* PDF invoices
+* Forgot password retrieval
+* OpenID support
+* Shared secret SSO
+* Credit card verification
+* REST API for users and subscriptions
+
+== Getting Started
+
+# cp config/application.example.yml config/application.yml
+# cp config/subscription.example.yml config/subscription.yml
+# rake db:schema:load
+# Setup a cron job to run `rake sub:daily` daily
+# configure application.yml
+# configure subscriptions.yml
+
+== Gateways
+
+Currently the following gateways are supported:
+* Braintree
+* TrustCommerce
+* PaymentExpress
+
+== Choosing a Gateway
+
+Braintree seems to be a good choice, and they're friendly to Railers to:
+* http://groups.google.com/group/rails-business/msg/53da3705df6063a2
+
+== Test transactions
+
+As far as I could tell, Braintree are the only Gateway that lets you
+test transactions without signing up.
+* http://dev.braintreepaymentsolutions.com/test-transaction/
+
+== SSO (single sign on)
+
+I've implemented a simple SSO using shared secrets.
+have a look at lib/sso.rb for more information.
+
+== Gotchas
+
+I've made some extensions to various plugins/libs which I've
+yet to push back:
+
+* Extended ActiveMerchant's Braintree and Trust Commerce gateways (see initializers)
+* Edited acts_as_state_machine:
+ * Added named_scope 'in_state'
+ * Stopped it overriding any states I specified before creation
+ * Make it update all the attributes on save, not just the state column
+* Used the Rails 2.2 version of prawnto (http://github.com/filiptepper/prawnto/tree/master)
+* Edited ssl_requirement so that it's disabled in development/test mode.
+* Rails error_messages_for now uses spans, instead of divs, to be standards compliant
+* Add a float component to Prawn (see initializers)
@@ -0,0 +1,10 @@
+# Add your own tasks in files placed in lib/tasks ending in .rake,
+# for example lib/tasks/capistrano.rake, and they will automatically be available to Rake.
+
+require(File.join(File.dirname(__FILE__), 'config', 'boot'))
+
+require 'rake'
+require 'rake/testtask'
+require 'rake/rdoctask'
+
+require 'tasks/rails'
@@ -0,0 +1,41 @@
+# Filters added to this controller apply to all controllers in the application.
+# Likewise, all the methods added will be available for all controllers.
+
+class ApplicationController < ActionController::Base
+ helper :all # include all helpers, all the time
+
+ # See ActionController::RequestForgeryProtection for details
+ # Uncomment the :secret if you're not using the cookie session store
+ protect_from_forgery # :secret => 'bc11bd14c7337ac474db42c2b08fc7e3'
+
+ # See ActionController::Base for details
+ # Uncomment this to filter the contents of submitted sensitive data parameters
+ # from your application log (in this case, all fields with names like "password").
+ filter_parameter_logging :password, :password_confirmation, :card
+
+ include HoptoadNotifier::Catcher if defined?(HoptoadNotifier)
+
+ include AuthenticatedSystem
+
+ include SslRequirement
+
+ rescue_from RestResponses::BaseError do |exception|
+ responds_error(exception.http_status)
+ end
+
+ include SSO::Server
+ sso :secret => AppConfig.sso_secret, :salt => AppConfig.sso_salt
+
+ protected
+
+ def login_from_sso
+ sso_session &&
+ sso_session[:user_id] &&
+ User.find(sso_session[:user_id])
+ end
+
+ # Override AuthenticatedSystem
+ def current_user
+ @current_user ||= (login_from_session || login_from_sso || login_from_cookie) unless @current_user == false
+ end
+end
@@ -0,0 +1,53 @@
+class BillingController < ApplicationController
+ before_filter :login_required, :account_owner_required
+ before_filter :find_user, :only => [:show, :cancel]
+ before_filter :find_subscription, :only => [:show, :invoice, :change_plan]
+
+ def show
+ @transactions = @subscription.transactions
+ end
+
+ def invoice
+ @transaction = Transaction.find(params[:id])
+ @subscription_address = @subscription.subscription_address
+ prawnto :inline => false, :filename => "invoice_#{@subscription.id}_#{@transaction.id}.pdf"
+ end
+
+ def change_plan
+ params[:subscription] ||= {}
+ @subscription.plan_name = params[:subscription][:plan_name]
+ if @subscription.save
+ flash[:notice] = "Successfully changed plans"
+ else
+ flash[:error] = "Error changing plans"
+ end
+ redirect_to :action => "show"
+ end
+
+ def change_owner
+ @user = User.find(params[:owner_id])
+ current_account.owner = @user
+ current_account.save!
+ flash[:notice] = "Successfully changed account owner"
+ redirect_back_or_default('/')
+ end
+
+ def cancel
+ if request.post?
+ current_account.suspend!
+ reset_session
+ flash[:notice] = "Successfully removed account"
+ redirect_to login_url
+ end
+ end
+
+ private
+
+ def find_user
+ @user = current_user
+ end
+
+ def find_subscription
+ @subscription = current_account.subscription
+ end
+end
@@ -0,0 +1,79 @@
+class SessionsController < ApplicationController
+ ssl_required :new, :create, :unless => SubConfig.test
+
+ def new
+ sso_record_params!
+ if logged_in?
+ successful_login(current_user)
+ end
+ end
+
+ def create
+ using_open_id? ? open_id_auth : normal_auth
+ end
+
+ def destroy
+ logout_killing_session!
+ sso_record_params!
+ sso_forget!
+ flash[:notice] = "You have been logged out."
+ redirect_to :action => :new
+ end
+
+ private
+
+ def normal_auth
+ logout_keeping_session!
+ user = User.authenticate(params[:email], params[:password])
+ user ? successful_login(user) : failed_login
+ end
+
+ def open_id_auth
+ # Catch user before they go too far
+ if openid_url = params[:openid_url]
+ openid_url = OpenIdAuthentication.normalize_url(openid_url)
+ user = User.find_by_identity_url(openid_url)
+ failed_login('Unknown user - please sign up') and return unless user
+ end
+
+ authenticate_with_open_id(params[:openid_url]) do |result, identity_url, registration|
+ if result.successful?
+ user = User.authenticate_by_identity_url(identity_url)
+ user ? successful_login(user) : failed_login
+ else
+ failed_login result.message
+ end
+ end
+ end
+
+ def failed_login(message = "Authentication failed")
+ flash.now[:error] = message
+ logger.warn "Failed login for '#{params[:login]}' from #{request.remote_ip} at #{Time.now.utc}"
+ render :action => 'new'
+ end
+
+ def successful_login(user)
+ # Get this before we call
+ # sso_authorize! or reset
+ # the session
+ is_sso = sso?
+ sso_url = sso_client_url
+
+ # Save user_id in the sso session
+ # and then save to cache
+ self.sso_session[:user_id] = user.id
+ sso_authorize!
+
+ reset_session
+ self.current_user = user
+ new_cookie_flag = (params[:remember_me] == "1")
+ handle_remember_cookie! new_cookie_flag
+ if is_sso
+ redirect_to sso_url
+ elsif AppConfig.other_site
+ redirect_back_or_default(AppConfig.other_site + '/login')
+ else
+ redirect_back_or_default('/')
+ end
+ end
+end
@@ -0,0 +1,23 @@
+class SubscriptionAddressesController < ApplicationController
+ before_filter :login_required, :account_owner_required
+ before_filter :find_subscription_address
+
+ def edit
+ end
+
+ def update
+ if @subscription_address.update_attributes(params[:subscription_address])
+ flash[:notice] = "Successfully updated address"
+ redirect_to :controller => "billing"
+ else
+ flash.now[:error] = "Error updating address"
+ render :action => "edit"
+ end
+ end
+
+ private
+
+ def find_subscription_address
+ @subscription_address = current_account.subscription.subscription_address
+ end
+end
@@ -0,0 +1,35 @@
+class SubscriptionsController < ApplicationController
+ before_filter :login_required, :account_owner_required
+ before_filter :find_subscription, :only => [:show, :edit, :update]
+ ssl_required :edit, :update
+
+ def show
+ respond_to do |format|
+ format.html {
+ flash.keep
+ redirect_to :controller => "billing"
+ }
+ format.xml { render :xml => @subscription }
+ end
+ end
+
+ def edit
+ end
+
+ def update
+ @subscription.card = params[:card]
+ if @subscription.save
+ flash[:notice] = "Successfully updated card"
+ redirect_to :action => "show"
+ else
+ flash.now[:error] = "Errors updating card"
+ render :action => "edit"
+ end
+ end
+
+ private
+
+ def find_subscription
+ @subscription = current_account.subscription
+ end
+end
Oops, something went wrong.

0 comments on commit 895dd07

Please sign in to comment.