├── .env-example ├── .gitignore ├── Gemfile ├── Gemfile.lock ├── Procfile ├── README.md ├── Rakefile ├── app ├── controllers │ ├── api │ │ ├── contacts_controller.rb │ │ ├── subscriptions_controller.rb │ │ └── users_controller.rb │ ├── api_controller.rb │ ├── application_controller.rb │ ├── auth │ │ └── sessions_controller.rb │ └── concerns │ │ └── .keep ├── mailers │ ├── application_mailer.rb │ └── reminder_mailer.rb ├── middleware │ └── inflect_json.rb ├── models │ ├── ability.rb │ ├── access_token.rb │ ├── concerns │ │ └── .keep │ ├── contact.rb │ ├── facebook_identity.rb │ ├── identity.rb │ ├── photo.rb │ ├── reminder_mail.rb │ └── user.rb ├── serializers │ ├── access_token_serializer.rb │ ├── application_serializer.rb │ ├── contact_serializer.rb │ ├── photo_serializer.rb │ └── user_serializer.rb └── views │ └── reminder_mailer │ └── daily_email.html.erb ├── bin ├── bundle ├── rails ├── rake └── spring ├── config.ru ├── config ├── application.rb ├── boot.rb ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── active_model_serializers.rb │ ├── backtrace_silencers.rb │ ├── facebook.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ └── wrap_parameters.rb ├── locales │ └── en.yml ├── routes.rb ├── secrets.yml └── unicorn.rb ├── db ├── migrate │ ├── 20140808091949_create_users.rb │ ├── 20141218061754_create_contacts.rb │ ├── 20141218061807_create_photos.rb │ └── 20150620111310_create_reminder_mails.rb ├── schema.rb └── seeds.rb ├── lib └── tasks │ ├── .keep │ └── reminder_mail.thor ├── log └── .keep ├── public └── .gitkeep └── test ├── controllers └── .keep ├── fixtures └── .keep ├── helpers └── .keep ├── integration └── .keep ├── mailers └── .keep ├── models └── .keep └── test_helper.rb /.env-example: -------------------------------------------------------------------------------- 1 | RACK_ENV=development 2 | HOSTNAME=localhost:3000 3 | MEMAMUG_FACEBOOK_API_KEY= 4 | MEMAMUG_FACEBOOK_API_SECRET= 5 | S3_ACCESS_KEY_ID= 6 | S3_BUCKET_NAME= 7 | S3_SECRET_ACCESS_KEY= 8 | PORT=3000 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore rbenv config. 2 | .ruby-gemset 3 | .ruby-version 4 | 5 | # Ignore all logfiles and tempfiles. 6 | /log/*.log 7 | /tmp 8 | 9 | # Environment variables for foreman 10 | .env 11 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby '2.1.2' 4 | 5 | gem 'rails', '4.2.2' 6 | gem 'rails-api' 7 | 8 | # Helps with serializeing 9 | gem 'active_model_serializers', '~> 0.10.0.rc2' 10 | 11 | # Amazon S3 API 12 | gem 'aws-sdk', '< 2.0' 13 | 14 | # Access control 15 | gem 'cancancan' 16 | 17 | # For reading in the base64 encoded images we upload 18 | gem 'data_uri' 19 | 20 | # Load environment variables from `.env 21 | gem 'dotenv-rails', groups: [:development, :test] 22 | 23 | # For notifying us of errors 24 | gem 'exception_notification', '~> 4.0.1' 25 | gem 'exception_notification-rake', '~> 0.1.2' 26 | 27 | # Facebook API 28 | gem 'koala' 29 | 30 | # Image uploads 31 | gem 'paperclip', '~> 4.3' 32 | 33 | # USe PostgreSQL with Active Record 34 | gem 'pg' 35 | 36 | # Allow serving of static assets 37 | gem 'rails_12factor' 38 | 39 | # Write tasks with thor instead of rake 40 | gem 'thor', groups: [:development, :test] 41 | 42 | # Heroku recommends serving assets with unicorn 43 | gem 'unicorn' 44 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actionmailer (4.2.2) 5 | actionpack (= 4.2.2) 6 | actionview (= 4.2.2) 7 | activejob (= 4.2.2) 8 | mail (~> 2.5, >= 2.5.4) 9 | rails-dom-testing (~> 1.0, >= 1.0.5) 10 | actionpack (4.2.2) 11 | actionview (= 4.2.2) 12 | activesupport (= 4.2.2) 13 | rack (~> 1.6) 14 | rack-test (~> 0.6.2) 15 | rails-dom-testing (~> 1.0, >= 1.0.5) 16 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 17 | actionview (4.2.2) 18 | activesupport (= 4.2.2) 19 | builder (~> 3.1) 20 | erubis (~> 2.7.0) 21 | rails-dom-testing (~> 1.0, >= 1.0.5) 22 | rails-html-sanitizer (~> 1.0, >= 1.0.1) 23 | active_model_serializers (0.10.0.rc2) 24 | activemodel (>= 4.0) 25 | activejob (4.2.2) 26 | activesupport (= 4.2.2) 27 | globalid (>= 0.3.0) 28 | activemodel (4.2.2) 29 | activesupport (= 4.2.2) 30 | builder (~> 3.1) 31 | activerecord (4.2.2) 32 | activemodel (= 4.2.2) 33 | activesupport (= 4.2.2) 34 | arel (~> 6.0) 35 | activesupport (4.2.2) 36 | i18n (~> 0.7) 37 | json (~> 1.7, >= 1.7.7) 38 | minitest (~> 5.1) 39 | thread_safe (~> 0.3, >= 0.3.4) 40 | tzinfo (~> 1.1) 41 | addressable (2.3.8) 42 | arel (6.0.0) 43 | aws-sdk (1.5.8) 44 | httparty (~> 0.7) 45 | json (~> 1.4) 46 | nokogiri (>= 1.4.4) 47 | uuidtools (~> 2.1) 48 | builder (3.2.2) 49 | cancancan (1.11.0) 50 | climate_control (0.0.3) 51 | activesupport (>= 3.0) 52 | cocaine (0.5.7) 53 | climate_control (>= 0.0.3, < 1.0) 54 | data_uri (0.1.0) 55 | dotenv (2.0.2) 56 | dotenv-rails (2.0.2) 57 | dotenv (= 2.0.2) 58 | railties (~> 4.0) 59 | erubis (2.7.0) 60 | exception_notification (4.0.1) 61 | actionmailer (>= 3.0.4) 62 | activesupport (>= 3.0.4) 63 | exception_notification-rake (0.1.2) 64 | exception_notification (~> 4.0.1) 65 | rake (>= 0.9.0) 66 | faraday (0.9.1) 67 | multipart-post (>= 1.2, < 3) 68 | globalid (0.3.5) 69 | activesupport (>= 4.1.0) 70 | httparty (0.13.5) 71 | json (~> 1.8) 72 | multi_xml (>= 0.5.2) 73 | i18n (0.7.0) 74 | json (1.8.3) 75 | kgio (2.9.3) 76 | koala (2.0.0) 77 | addressable 78 | faraday 79 | multi_json 80 | loofah (2.0.2) 81 | nokogiri (>= 1.5.9) 82 | mail (2.6.3) 83 | mime-types (>= 1.16, < 3) 84 | mime-types (2.6.1) 85 | mimemagic (0.3.0) 86 | mini_portile (0.6.2) 87 | minitest (5.7.0) 88 | multi_json (1.11.1) 89 | multi_xml (0.5.5) 90 | multipart-post (2.0.0) 91 | nokogiri (1.6.6.2) 92 | mini_portile (~> 0.6.0) 93 | paperclip (4.3.0) 94 | activemodel (>= 3.2.0) 95 | activesupport (>= 3.2.0) 96 | cocaine (~> 0.5.5) 97 | mime-types 98 | mimemagic (= 0.3.0) 99 | pg (0.18.2) 100 | rack (1.6.4) 101 | rack-test (0.6.3) 102 | rack (>= 1.0) 103 | rails (4.2.2) 104 | actionmailer (= 4.2.2) 105 | actionpack (= 4.2.2) 106 | actionview (= 4.2.2) 107 | activejob (= 4.2.2) 108 | activemodel (= 4.2.2) 109 | activerecord (= 4.2.2) 110 | activesupport (= 4.2.2) 111 | bundler (>= 1.3.0, < 2.0) 112 | railties (= 4.2.2) 113 | sprockets-rails 114 | rails-api (0.4.0) 115 | actionpack (>= 3.2.11) 116 | railties (>= 3.2.11) 117 | rails-deprecated_sanitizer (1.0.3) 118 | activesupport (>= 4.2.0.alpha) 119 | rails-dom-testing (1.0.6) 120 | activesupport (>= 4.2.0.beta, < 5.0) 121 | nokogiri (~> 1.6.0) 122 | rails-deprecated_sanitizer (>= 1.0.1) 123 | rails-html-sanitizer (1.0.2) 124 | loofah (~> 2.0) 125 | rails_12factor (0.0.3) 126 | rails_serve_static_assets 127 | rails_stdout_logging 128 | rails_serve_static_assets (0.0.4) 129 | rails_stdout_logging (0.0.3) 130 | railties (4.2.2) 131 | actionpack (= 4.2.2) 132 | activesupport (= 4.2.2) 133 | rake (>= 0.8.7) 134 | thor (>= 0.18.1, < 2.0) 135 | raindrops (0.13.0) 136 | rake (10.4.2) 137 | sprockets (3.2.0) 138 | rack (~> 1.0) 139 | sprockets-rails (2.3.1) 140 | actionpack (>= 3.0) 141 | activesupport (>= 3.0) 142 | sprockets (>= 2.8, < 4.0) 143 | thor (0.19.1) 144 | thread_safe (0.3.5) 145 | tzinfo (1.2.2) 146 | thread_safe (~> 0.1) 147 | unicorn (4.9.0) 148 | kgio (~> 2.6) 149 | rack 150 | raindrops (~> 0.7) 151 | uuidtools (2.1.5) 152 | 153 | PLATFORMS 154 | ruby 155 | 156 | DEPENDENCIES 157 | active_model_serializers (~> 0.10.0.rc2) 158 | aws-sdk (< 2.0) 159 | cancancan 160 | data_uri 161 | dotenv-rails 162 | exception_notification (~> 4.0.1) 163 | exception_notification-rake (~> 0.1.2) 164 | koala 165 | paperclip (~> 4.3) 166 | pg 167 | rails (= 4.2.2) 168 | rails-api 169 | rails_12factor 170 | thor 171 | unicorn 172 | 173 | BUNDLED WITH 174 | 1.10.4 175 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec unicorn -p $PORT -c ./config/unicorn.rb -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Memamug 2 | 3 | An open-source app which helps you remember people you'd normally forget. See the live version at [memamug.com](http://www.memamug.com). 4 | 5 | This app was built to demonstrate how to write a simple web-app, without resorting to a cliché to-do list. I'll be writing a number of tutorials to explain how to build this app, from creating the initial directory structure to deploying it live. Follow [@james_k_nelson](https://twitter.com/james_k_nelson) to keep updated. 6 | 7 | ## Getting Started 8 | 9 | This server is built with [rails-api](https://github.com/rails-api/rails-api), so the installation process mostly follows that for a standard rails application, with a few differences: 10 | 11 | * It uses the `uuid-ossp` PostgreSQL extension. Add this by typing `create extension "uuid-ossp";` into `psql` once your database is setup. 12 | * You'll need to rename `.env-example` to `.env` and add your S3 and Facebook Login API keys.env` 13 | 14 | Other than that, you can just follow the usual rails setup process: 15 | 16 | * Setup your `config/database.yml` 17 | * Run `rake db:migrate` 18 | 19 | Once setup, install `foreman` and `mailcatcher` gems: 20 | 21 | ``` 22 | gem install foreman mailcatcher 23 | ``` 24 | 25 | Then start your server with 26 | 27 | ``` 28 | mailcatcher 29 | foreman start 30 | ``` 31 | 32 | Then move on to getting [memamug-client](https://github.com/jamesknelson/memamug-client) working, so you can use it! 33 | 34 | Need more specific details? I'll be writing more soon. Follow [@james_k_nelson](https://twitter.com/james_k_nelson) to stay up to date. 35 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # Add your own tasks in files placed in lib/tasks ending in .rake, 2 | # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. 3 | 4 | require File.expand_path('../config/application', __FILE__) 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/controllers/api/contacts_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::ContactsController < ApiController 2 | def index 3 | authorize! :read, Contact 4 | @contacts = current_user.contacts 5 | render json: @contacts, include: 'photos' 6 | end 7 | 8 | def show 9 | @contact = current_user.contacts.find(params[:id]) 10 | authorize! :read, @contact 11 | render json: @contact, include: 'photos' 12 | end 13 | 14 | def update 15 | @contact = current_user.contacts.find(params[:id]) 16 | authorize! :update, @contact 17 | if @contact.update(contact_params) 18 | render json: @contact, status: 200, include: 'photos' 19 | else 20 | render json: @contact.errors.full_messages, status: 400 21 | end 22 | end 23 | 24 | def create 25 | @contact = current_user.contacts.build(contact_params) 26 | @contact.subscribed_on = Time.now 27 | authorize! :create, @contact 28 | if @contact.save 29 | render json: @contact, status: 201, include: 'photos' 30 | else 31 | render json: @contact.errors, status: 400 32 | end 33 | end 34 | 35 | protected 36 | def contact_params 37 | params.permit( 38 | :display_name, :notes, :starred, 39 | photos_attributes: [:id, :image_content_type, :image_original_filename, :image_base64] 40 | ) 41 | end 42 | 43 | def contact_photos_params 44 | params.require(:photos_attributes) 45 | end 46 | end 47 | -------------------------------------------------------------------------------- /app/controllers/api/subscriptions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::SubscriptionsController < ApiController 2 | def destroy_all 3 | authorize! :update, Contact 4 | if current_user.contacts.update_all(subscribed_on: nil) 5 | render nothing: true, status: 200 6 | else 7 | render nothing: true, status: 400 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /app/controllers/api/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::UsersController < ApiController 2 | def show 3 | authorize! :read, current_user 4 | render json: current_user 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /app/controllers/api_controller.rb: -------------------------------------------------------------------------------- 1 | class ApiController < ApplicationController 2 | include ActionController::HttpAuthentication::Token::ControllerMethods 3 | include CanCan::ControllerAdditions 4 | 5 | # Make sure the user is logged in 6 | before_filter :authenticate 7 | 8 | # Make sure the user is authorized (using CanCan) 9 | check_authorization 10 | 11 | rescue_from CanCan::AccessDenied do |exception| 12 | render :json => {:error => "Access Denied"}, :status => 403 13 | end 14 | 15 | protected 16 | 17 | # 18 | # Authentication 19 | # 20 | 21 | def authenticate 22 | authenticate_token || render_unauthorized 23 | end 24 | 25 | def authenticate_token 26 | user = authenticate_with_http_token do |t, o| 27 | @current_token = t 28 | 29 | User.from_access_token(t) 30 | end 31 | 32 | if user 33 | @current_user = user 34 | else 35 | request_http_token_authentication 36 | end 37 | end 38 | 39 | def current_user 40 | @current_user 41 | end 42 | 43 | def render_unauthorized 44 | self.headers['WWW-Authenticate'] = 'Token realm="Memamug"' 45 | render json: {error: 'Bad credentials'}, status: 401 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/controllers/auth/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Auth::SessionsController < ApplicationController 2 | 3 | #rescue_from Koala::Facebook::AuthenticationError do |exception| 4 | # render json: {provider: "facebook"}, status: 401 5 | #end 6 | 7 | def create 8 | if params[:provider] == 'facebook' 9 | identity = FacebookIdentity.find_or_create_by_api_token(params[:access_token]) 10 | elsif params[:provider] == 'linkedin' 11 | identity = LinkedInIdentity.find_or_create_by_auth_code(params[:auth_code]) 12 | end 13 | 14 | # Create a new access token for the newly logged in user 15 | access_token = identity.user.access_tokens.create! 16 | 17 | render json: access_token, include: 'user' 18 | end 19 | 20 | def destroy 21 | token = AccessToken.find_by_access_token(@current_token) 22 | token.destroy! 23 | render json: {success: true} 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /app/controllers/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesknelson/memamug-server/fbb639fe2b162ad35a46a0bb5b71a9217aac698c/app/controllers/concerns/.keep -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | end 3 | -------------------------------------------------------------------------------- /app/mailers/reminder_mailer.rb: -------------------------------------------------------------------------------- 1 | class ReminderMailer < ApplicationMailer 2 | default from: "reminders@memamug.com" 3 | 4 | def daily_email(reminder_mail) 5 | @reminder_mail = reminder_mail 6 | mail(to: reminder_mail.user.email, subject: 'Remember these people?') 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/middleware/inflect_json.rb: -------------------------------------------------------------------------------- 1 | class InflectJson 2 | def initialize(app) 3 | @app = app 4 | end 5 | 6 | def call(env) 7 | request = ActionDispatch::Request.new(env) 8 | 9 | if request.content_mime_type == Mime::JSON 10 | # Convert request body from camelCase to snake_case 11 | env["RAW_POST_DATA"] = to_underscore(request.raw_post) 12 | end 13 | 14 | status, headers, response = @app.call(env) 15 | 16 | if headers["Content-Type"] and headers["Content-Type"].match("json") 17 | # Convert response body from snake_case to camelCase 18 | body = if response.respond_to? :body 19 | response.body 20 | elsif response.respond_to? :join 21 | response.join 22 | else 23 | response 24 | end 25 | [status, headers, [to_camel_case(body)]] 26 | else 27 | [status, headers, response] 28 | end 29 | end 30 | 31 | # Based on https://gist.github.com/timruffles/2780508 32 | def to_underscore(hash) 33 | convert_json hash, :underscore 34 | end 35 | def to_camel_case(hash) 36 | convert_json hash, :camelize, :lower 37 | end 38 | def convert_json(str, *method) 39 | obj = ActiveSupport::JSON.decode(str) 40 | converted = convert(obj, *method) 41 | ActiveSupport::JSON.encode(converted) 42 | end 43 | def convert(obj, *method) 44 | case obj 45 | when Hash 46 | obj.inject({}) do |h,(k,v)| 47 | v = convert v, *method 48 | h[k.send(*method)] = v 49 | h 50 | end 51 | when Array 52 | obj.map {|m| convert m, *method } 53 | else 54 | obj 55 | end 56 | end 57 | end -------------------------------------------------------------------------------- /app/models/ability.rb: -------------------------------------------------------------------------------- 1 | class Ability 2 | include CanCan::Ability 3 | 4 | def initialize(user) 5 | # Default to guest user 6 | user ||= User.new 7 | 8 | # Anybody can manage their own account 9 | can :manage, User, id: user.id 10 | can :manage, Photo, user_id: user.id 11 | can :manage, Contact, user_id: user.id 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/models/access_token.rb: -------------------------------------------------------------------------------- 1 | class AccessToken < ActiveRecord::Base 2 | belongs_to :user 3 | 4 | before_create :initialize_access_token 5 | 6 | def initialize_access_token 7 | # The database has a uniqueness constraint on access_token - so in the x in 8 | # 64^16 chance we somehow generate an existing token, just let the 9 | # application error, and the user can try to login again. 10 | self.access_token = SecureRandom.base64 11 | 12 | self.expires_at = 60.days.from_now 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /app/models/concerns/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesknelson/memamug-server/fbb639fe2b162ad35a46a0bb5b71a9217aac698c/app/models/concerns/.keep -------------------------------------------------------------------------------- /app/models/contact.rb: -------------------------------------------------------------------------------- 1 | class Contact < ActiveRecord::Base 2 | has_many :photos, {dependent: :destroy}, -> { order created_at: :asc } 3 | has_and_belongs_to_many :reminder_mails 4 | 5 | accepts_nested_attributes_for :photos, allow_destroy: true 6 | 7 | validates :photos, presence: true 8 | end 9 | -------------------------------------------------------------------------------- /app/models/facebook_identity.rb: -------------------------------------------------------------------------------- 1 | class FacebookIdentity < Identity 2 | def self.find_or_create_by_api_token(token) 3 | client = build_client(token) 4 | profile = client.get_object("me", fields: "id,first_name,last_name,email") 5 | 6 | identity = where(uid: profile['id']).first 7 | 8 | # Don't run with expired access tokens 9 | if identity and identity.expires_at < Time.now 10 | identity.destroy! 11 | identity = nil 12 | end 13 | 14 | unless identity 15 | # This facebook user isn't already linked to an account 16 | exchanged = FBOAuth.exchange_access_token_info(token) 17 | identity_options = { 18 | type: 'FacebookIdentity', 19 | uid: profile['id'], 20 | api_token: exchanged['access_token'], 21 | expires_at: exchanged['expires'].to_i.seconds.from_now 22 | } 23 | 24 | if user = User.find_by_email(profile['email']) 25 | # The user has an account already - link the new FB account 26 | identity = user.identities.create!(identity_options) 27 | else 28 | # Need to create a new user 29 | user = User.new( 30 | first_name: profile['first_name'], 31 | last_name: profile['last_name'], 32 | email: profile['email'] 33 | ) 34 | identity = user.identities.build(identity_options) 35 | user.save! 36 | end 37 | end 38 | 39 | identity 40 | end 41 | 42 | def self.build_client(token) 43 | graph = Koala::Facebook::API.new( 44 | token, 45 | Rails.application.secrets.facebook_api_secret 46 | ) 47 | end 48 | 49 | def client 50 | @client ||= self.class.build_client(api_token) 51 | end 52 | 53 | def fetch_avatar 54 | update!( 55 | avatar_url: client.get_picture("me"), 56 | avatar_url_fetched_at: Time.now 57 | ) 58 | end 59 | end 60 | -------------------------------------------------------------------------------- /app/models/identity.rb: -------------------------------------------------------------------------------- 1 | class Identity < ActiveRecord::Base 2 | belongs_to :user 3 | 4 | def fetch_avatar 5 | raise NotImplementedError.new("You must implement fetch_avatar") 6 | end 7 | 8 | def avatar_url 9 | fetch_avatar if !avatar_url_fetched_at or avatar_url_fetched_at < 1.day.ago 10 | read_attribute :avatar_url 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/models/photo.rb: -------------------------------------------------------------------------------- 1 | class Photo < ActiveRecord::Base 2 | attr_accessor :image_content_type, :image_original_filename, :image_base64 3 | 4 | belongs_to :contact 5 | belongs_to :user 6 | 7 | has_attached_file :image, 8 | storage: :s3, 9 | styles: { 10 | thumb: ["40x40#", :jpg], 11 | small: ["150x150#", :jpg], 12 | medium: ["200x200#", :jpg] 13 | }, 14 | url: ":s3_domain_url", 15 | path: "assets/:class/:id/:style.:extension", 16 | s3_protocol: "https", 17 | s3_permissions: :private, 18 | s3_credentials: { 19 | bucket: Rails.application.secrets.s3_bucket_name, 20 | access_key_id: Rails.application.secrets.s3_access_key_id, 21 | secret_access_key: Rails.application.secrets.s3_secret_access_key 22 | } 23 | 24 | before_validation :decode_base64_image 25 | 26 | validates_attachment :image, 27 | content_type: { content_type: "image/jpeg" }, 28 | presence: true, 29 | size: { less_than: 2.megabytes } 30 | 31 | protected 32 | # Create a temporary image file which paperclip can work with from the 33 | # base64 image which the client passes in 34 | def decode_base64_image 35 | if image_base64 and image_content_type and image_original_filename 36 | uri = URI::Data.new(image_base64) 37 | 38 | data = StringIO.new(uri.data) 39 | data.class_eval { attr_accessor :content_type, :original_filename } 40 | data.content_type = image_content_type 41 | data.original_filename = File.basename(image_original_filename) 42 | 43 | self.image = data 44 | end 45 | end 46 | 47 | def base64_url_decode(str) 48 | str += '=' * (4 - str.length.modulo(4)) 49 | Base64.decode64(str.tr('-_','+/')) 50 | end 51 | end 52 | 53 | -------------------------------------------------------------------------------- /app/models/reminder_mail.rb: -------------------------------------------------------------------------------- 1 | class ReminderMail < ActiveRecord::Base 2 | belongs_to :user 3 | has_and_belongs_to_many :contacts 4 | end 5 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ActiveRecord::Base 2 | SUBSCRIPTION_PERIODS = [1, 3, 7, 14, 27].map {|x| x.days} 3 | 4 | has_many :access_tokens, dependent: :destroy 5 | has_many :identities, dependent: :destroy, after_remove: :refetch_avatar 6 | has_many :contacts, dependent: :destroy 7 | has_many :reminder_mails, dependent: :destroy 8 | 9 | validates :email, 10 | uniqueness: true, 11 | format: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i 12 | 13 | validates :first_name, :last_name, 14 | presence: true 15 | 16 | 17 | def self.from_access_token(access_token) 18 | User.joins(:access_tokens). 19 | where("access_tokens.access_token = ?", access_token). 20 | where("access_tokens.expires_at > now()"). 21 | first 22 | end 23 | 24 | def reminder_contacts_for(date, except_attempted_on) 25 | subscription_dates = SUBSCRIPTION_PERIODS.map { |period| date - period } 26 | contacts 27 | .joins(''' 28 | LEFT OUTER JOIN contacts_reminder_mails 29 | ON contacts_reminder_mails.contact_id = contacts.id 30 | LEFT OUTER JOIN reminder_mails 31 | ON reminder_mails.id = contacts_reminder_mails.reminder_mail_id 32 | ''') 33 | .where('(reminder_mails.created_at IS NULL) OR (reminder_mails.created_at::date <> ?)', except_attempted_on) 34 | .where('contacts.subscribed_on::date IN (?)', subscription_dates) 35 | .distinct 36 | end 37 | 38 | def refetch_avatar(identity) 39 | if identities.count > 0 40 | avatar_identity.fetch_avatar 41 | end 42 | end 43 | 44 | def avatar_identity 45 | identities.order(:created_at).first 46 | end 47 | 48 | def avatar_url 49 | avatar_identity.avatar_url 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /app/serializers/access_token_serializer.rb: -------------------------------------------------------------------------------- 1 | class AccessTokenSerializer < ApplicationSerializer 2 | belongs_to :user 3 | attributes :access_token, :expires_at 4 | end 5 | -------------------------------------------------------------------------------- /app/serializers/application_serializer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationSerializer < ActiveModel::Serializer 2 | end -------------------------------------------------------------------------------- /app/serializers/contact_serializer.rb: -------------------------------------------------------------------------------- 1 | class ContactSerializer < ApplicationSerializer 2 | attributes :id, :starred, :display_name, :notes, :subscribed_on 3 | 4 | has_many :photos 5 | end 6 | -------------------------------------------------------------------------------- /app/serializers/photo_serializer.rb: -------------------------------------------------------------------------------- 1 | class PhotoSerializer < ApplicationSerializer 2 | attributes :id, :contact_id, :image 3 | 4 | def image 5 | { 6 | file_size: object.image_file_size, 7 | file_name: object.image_file_name, 8 | 9 | original_url: image_url("original"), 10 | medium_url: image_url("medium"), 11 | thumb_url: image_url("thumb") 12 | } 13 | end 14 | 15 | 16 | protected 17 | def image_url(style) 18 | object.image.s3_object(style).url_for(:read).to_s 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /app/serializers/user_serializer.rb: -------------------------------------------------------------------------------- 1 | class UserSerializer < ApplicationSerializer 2 | attributes :id, :first_name, :last_name, :email, :avatar_url 3 | end 4 | -------------------------------------------------------------------------------- /app/views/reminder_mailer/daily_email.html.erb: -------------------------------------------------------------------------------- 1 |
2 |

Do you remember these people?

3 |

You recently asked Memamug to send you reminders of your new contacts. Here they are!

4 |

Don't see the photos? Make sure to add reminders@memamug.com to your address book and you have images enabled for this e-mail!

5 | 6 | <% @reminder_mail 7 | .contacts 8 | .each_with_index 9 | .map {|contact, i| [contact, i]} 10 | .group_by {|contact, i| i / 3} 11 | .map do |_, row| 12 | row.map { |contact, i| contact } + Array.new(3 - row.length, "") 13 | end 14 | .each do |row| %> 15 | 16 | <% row.each do |contact| %> 17 | 30 | <% end %> 31 | 32 | <% end %> 33 | 34 | 39 | 40 |
18 | <% if contact != "" %> 19 | 20 | 21 | 26 | 27 |
22 | 23 |
24 |

<%= contact.display_name %>

25 |
28 | <% end %> 29 |
35 |
36 |
37 | Unsubscribe 38 |
41 |
42 | -------------------------------------------------------------------------------- /bin/bundle: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | load Gem.bin_path('bundler', 'bundle') 4 | -------------------------------------------------------------------------------- /bin/rails: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | APP_PATH = File.expand_path('../../config/application', __FILE__) 7 | require_relative '../config/boot' 8 | require 'rails/commands' 9 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path("../spring", __FILE__) 4 | rescue LoadError 5 | end 6 | require_relative '../config/boot' 7 | require 'rake' 8 | Rake.application.run 9 | -------------------------------------------------------------------------------- /bin/spring: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | # This file loads spring without using Bundler, in order to be fast 4 | # It gets overwritten when you run the `spring binstub` command 5 | 6 | unless defined?(Spring) 7 | require "rubygems" 8 | require "bundler" 9 | 10 | if match = Bundler.default_lockfile.read.match(/^GEM$.*?^ spring \((.*?)\)$.*?^$/m) 11 | ENV["GEM_PATH"] = ([Bundler.bundle_path.to_s] + Gem.path).join(File::PATH_SEPARATOR) 12 | ENV["GEM_HOME"] = "" 13 | Gem.paths = ENV 14 | 15 | gem "spring", match[1] 16 | require "spring/binstub" 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require ::File.expand_path('../config/environment', __FILE__) 4 | run Rails.application 5 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../boot', __FILE__) 2 | 3 | require "active_record/railtie" 4 | require "action_controller/railtie" 5 | require "action_mailer/railtie" 6 | # require "sprockets/railtie" 7 | require "rails/test_unit/railtie" 8 | 9 | # Require the gems listed in Gemfile, including any gems 10 | # you've limited to :test, :development, or :production. 11 | Bundler.require(*Rails.groups) 12 | 13 | module Api 14 | class Application < Rails::Application 15 | # Settings in config/environments/* take precedence over those specified here. 16 | # Application configuration should go into files in config/initializers 17 | # -- all .rb files in that directory are automatically loaded. 18 | 19 | # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. 20 | # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. 21 | # config.time_zone = 'Central Time (US & Canada)' 22 | 23 | # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. 24 | # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] 25 | # config.i18n.default_locale = :de 26 | 27 | # Convert incoming/outgoing JSON between camelCase and snake_case 28 | config.middleware.insert_before ActionDispatch::ParamsParser, "InflectJson" 29 | 30 | # Allow API requests from the client, which isn't hosted at the same origin 31 | # config.middleware.insert_before 0, "Rack::Cors" do 32 | # allow do 33 | # origins 'localhost:9000', 'melaleuca.local:9000', 'learn.memamug.com' 34 | # resource '*', :headers => :any, :methods => [:get, :post, :patch, :options] 35 | # end 36 | # end 37 | 38 | config.active_record.raise_in_transactional_callbacks = true 39 | 40 | # Disable asset pipeline 41 | config.assets.enabled = false 42 | 43 | # Serve the app 44 | config.serve_static_files = true 45 | 46 | config.autoload_paths += %W(#{config.root}/app/serializers) 47 | end 48 | end 49 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | # Set up gems listed in the Gemfile. 2 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) 3 | 4 | require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) 5 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: postgresql 3 | host: localhost 4 | username: james 5 | password: null 6 | database: memamug-dev 7 | pool: 5 8 | port: 5432 9 | 10 | production: 11 | adapter: postgresql 12 | host: localhost 13 | username: james 14 | password: null 15 | database: memamug-dev 16 | pool: 5 17 | port: 5432 18 | 19 | # Warning: The database defined as "test" will be erased and 20 | # re-generated from your development database when you run "rake". 21 | # Do not set this db to the same as development or production. 22 | test: 23 | adapter: postgresql 24 | host: localhost 25 | username: james 26 | password: null 27 | database: memamug-test 28 | pool: 5 29 | port: 5432 30 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require File.expand_path('../application', __FILE__) 3 | 4 | # Initialize the Rails application. 5 | Rails.application.initialize! 6 | -------------------------------------------------------------------------------- /config/environments/development.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # In the development environment your application's code is reloaded on 5 | # every request. This slows down response time but is perfect for development 6 | # since you don't have to restart the web server when you make code changes. 7 | config.cache_classes = false 8 | 9 | # Do not eager load code on boot. 10 | config.eager_load = false 11 | 12 | # Show full error reports and disable caching. 13 | config.consider_all_requests_local = true 14 | config.action_controller.perform_caching = false 15 | 16 | # Deliver to mailcatcher 17 | config.action_mailer.delivery_method = :smtp 18 | config.action_mailer.smtp_settings = { :address => "localhost", :port => 1025 } 19 | config.action_mailer.raise_delivery_errors = true 20 | 21 | # Print deprecation notices to the Rails logger. 22 | config.active_support.deprecation = :log 23 | 24 | # Raise an error on page load if there are pending migrations. 25 | config.active_record.migration_error = :page_load 26 | 27 | # Debug mode disables concatenation and preprocessing of assets. 28 | # This option may cause significant delays in view rendering with a large 29 | # number of complex assets. 30 | config.assets.debug = true 31 | 32 | # Adds additional error checking when serving assets at runtime. 33 | # Checks for improperly declared sprockets dependencies. 34 | # Raises helpful error messages. 35 | config.assets.raise_runtime_errors = true 36 | 37 | # Raises error for missing translations 38 | # config.action_view.raise_on_missing_translations = true 39 | end 40 | -------------------------------------------------------------------------------- /config/environments/production.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # Code is not reloaded between requests. 5 | config.cache_classes = true 6 | 7 | # Eager load code on boot. This eager loads most of Rails and 8 | # your application in memory, allowing both threaded web servers 9 | # and those relying on copy on write to perform better. 10 | # Rake tasks automatically ignore this option for performance. 11 | config.eager_load = true 12 | 13 | # Full error reports are disabled and caching is turned on. 14 | config.consider_all_requests_local = false 15 | config.action_controller.perform_caching = true 16 | 17 | # Enable Rack::Cache to put a simple HTTP cache in front of your application 18 | # Add `rack-cache` to your Gemfile before enabling this. 19 | # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. 20 | # config.action_dispatch.rack_cache = true 21 | 22 | # Disable Rails's static asset server (Apache or nginx will already do this). 23 | # config.serve_static_assets = false 24 | 25 | # Compress JavaScripts and CSS. 26 | # config.assets.js_compressor = :uglifier 27 | # config.assets.css_compressor = :sass 28 | 29 | # Do not fallback to assets pipeline if a precompiled asset is missed. 30 | config.assets.compile = false 31 | 32 | # Generate digests for assets URLs. 33 | config.assets.digest = true 34 | 35 | # `config.assets.precompile` has moved to config/initializers/assets.rb 36 | 37 | # Specifies the header that your server uses for sending files. 38 | # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache 39 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx 40 | 41 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 42 | # config.force_ssl = true 43 | 44 | # Set to :debug to see everything in the log. 45 | config.log_level = :info 46 | 47 | # Prepend all log lines with the following tags. 48 | # config.log_tags = [ :subdomain, :uuid ] 49 | 50 | # Use a different logger for distributed setups. 51 | # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) 52 | 53 | # Use a different cache store in production. 54 | # config.cache_store = :mem_cache_store 55 | 56 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 57 | # config.action_controller.asset_host = "http://assets.example.com" 58 | 59 | # Precompile additional assets. 60 | # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. 61 | # config.assets.precompile += %w( search.js ) 62 | 63 | # Ignore bad email addresses and do not raise email delivery errors. 64 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 65 | # config.action_mailer.raise_delivery_errors = false 66 | 67 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 68 | # the I18n.default_locale when a translation cannot be found). 69 | config.i18n.fallbacks = true 70 | 71 | # Send deprecation notices to registered listeners. 72 | config.active_support.deprecation = :notify 73 | 74 | # Disable automatic flushing of the log to improve performance. 75 | # config.autoflush_log = false 76 | 77 | # Use default logging formatter so that PID and timestamp are not suppressed. 78 | config.log_formatter = ::Logger::Formatter.new 79 | 80 | # Do not dump schema after migrations. 81 | config.active_record.dump_schema_after_migration = false 82 | 83 | # Deliver mail via mailgun 84 | ActionMailer::Base.smtp_settings = { 85 | :address => 'smtp.sendgrid.net', 86 | :port => '587', 87 | :authentication => :plain, 88 | :user_name => ENV['SENDGRID_USERNAME'], 89 | :password => ENV['SENDGRID_PASSWORD'], 90 | :domain => 'heroku.com', 91 | :enable_starttls_auto => true 92 | } 93 | config.action_mailer.delivery_method = :smtp 94 | config.action_mailer.raise_delivery_errors = true 95 | 96 | # Notify admin about exceptions 97 | config.middleware.use ExceptionNotification::Rack, 98 | :email => { 99 | :email_prefix => "[Memamug] ", 100 | :sender_address => %{"Memamug Exception Notifier" }, 101 | :exception_recipients => %w{james@jamesknelson.com}, 102 | } 103 | 104 | # Notify about rake exceptions too 105 | ExceptionNotifier::Rake.configure 106 | end 107 | -------------------------------------------------------------------------------- /config/environments/test.rb: -------------------------------------------------------------------------------- 1 | Rails.application.configure do 2 | # Settings specified here will take precedence over those in config/application.rb. 3 | 4 | # The test environment is used exclusively to run your application's 5 | # test suite. You never need to work with it otherwise. Remember that 6 | # your test database is "scratch space" for the test suite and is wiped 7 | # and recreated between test runs. Don't rely on the data there! 8 | config.cache_classes = true 9 | 10 | # Do not eager load code on boot. This avoids loading your whole application 11 | # just for the purpose of running a single test. If you are using a tool that 12 | # preloads Rails for running tests, you may have to set it to true. 13 | config.eager_load = false 14 | 15 | # Configure static asset server for tests with Cache-Control for performance. 16 | config.serve_static_assets = true 17 | config.static_cache_control = 'public, max-age=3600' 18 | 19 | # Show full error reports and disable caching. 20 | config.consider_all_requests_local = true 21 | config.action_controller.perform_caching = false 22 | 23 | # Raise exceptions instead of rendering exception templates. 24 | config.action_dispatch.show_exceptions = false 25 | 26 | # Disable request forgery protection in test environment. 27 | config.action_controller.allow_forgery_protection = false 28 | 29 | # Tell Action Mailer not to deliver emails to the real world. 30 | # The :test delivery method accumulates sent emails in the 31 | # ActionMailer::Base.deliveries array. 32 | config.action_mailer.delivery_method = :test 33 | 34 | # Print deprecation notices to the stderr. 35 | config.active_support.deprecation = :stderr 36 | 37 | # Raises error for missing translations 38 | # config.action_view.raise_on_missing_translations = true 39 | end 40 | -------------------------------------------------------------------------------- /config/initializers/active_model_serializers.rb: -------------------------------------------------------------------------------- 1 | ActiveModel::Serializer.config.adapter = :json_api -------------------------------------------------------------------------------- /config/initializers/backtrace_silencers.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. 4 | # Rails.backtrace_cleaner.add_silencer { |line| line =~ /my_noisy_library/ } 5 | 6 | # You can also remove all the silencers if you're trying to debug a problem that might stem from framework code. 7 | # Rails.backtrace_cleaner.remove_silencers! 8 | -------------------------------------------------------------------------------- /config/initializers/facebook.rb: -------------------------------------------------------------------------------- 1 | FBOAuth = Koala::Facebook::OAuth.new( 2 | Rails.application.secrets.facebook_api_key, 3 | Rails.application.secrets.facebook_api_secret 4 | ) 5 | -------------------------------------------------------------------------------- /config/initializers/filter_parameter_logging.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Configure sensitive parameters which will be filtered from the log file. 4 | Rails.application.config.filter_parameters += [:password] 5 | -------------------------------------------------------------------------------- /config/initializers/inflections.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new inflection rules using the following format. Inflections 4 | # are locale specific, and you may define rules for as many different 5 | # locales as you wish. All of these examples are active by default: 6 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 7 | # inflect.plural /^(ox)$/i, '\1en' 8 | # inflect.singular /^(ox)en/i, '\1' 9 | # inflect.irregular 'person', 'people' 10 | # inflect.uncountable %w( fish sheep ) 11 | # end 12 | 13 | # These inflection rules are supported but not enabled by default: 14 | # ActiveSupport::Inflector.inflections(:en) do |inflect| 15 | # inflect.acronym 'RESTful' 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/mime_types.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Add new mime types for use in respond_to blocks: 4 | # Mime::Type.register "text/richtext", :rtf 5 | -------------------------------------------------------------------------------- /config/initializers/wrap_parameters.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # This file contains settings for ActionController::ParamsWrapper which 4 | # is enabled by default. 5 | 6 | # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. 7 | # ActiveSupport.on_load(:action_controller) do 8 | # wrap_parameters format: [:json] if respond_to?(:wrap_parameters) 9 | # end 10 | 11 | # To enable root element in JSON for ActiveRecord objects. 12 | # ActiveSupport.on_load(:active_record) do 13 | # self.include_root_in_json = true 14 | # end 15 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # Files in the config/locales directory are used for internationalization 2 | # and are automatically loaded by Rails. If you want to use locales other 3 | # than English, add the necessary files in this directory. 4 | # 5 | # To use the locales, use `I18n.t`: 6 | # 7 | # I18n.t 'hello' 8 | # 9 | # In views, this is aliased to just `t`: 10 | # 11 | # <%= t('hello') %> 12 | # 13 | # To use a different locale, set it with `I18n.locale`: 14 | # 15 | # I18n.locale = :es 16 | # 17 | # This would use the information in config/locales/es.yml. 18 | # 19 | # To learn more, please read the Rails Internationalization guide 20 | # available at http://guides.rubyonrails.org/i18n.html. 21 | 22 | en: 23 | hello: "Hello world" 24 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | 3 | namespace 'auth', defaults: {format: :json} do 4 | post 'login' => 'sessions#create' 5 | delete 'logout' => 'sessions#destroy' 6 | end 7 | 8 | namespace 'api', defaults: {format: :json} do 9 | namespace 'v1', module: nil do 10 | resource :user 11 | resources :contacts, only: [:index, :show, :create, :update] 12 | resources :questions, only: [:index, :show, :create, :update] 13 | 14 | delete 'subscriptions' => 'subscriptions#destroy_all' 15 | end 16 | end 17 | 18 | # The priority is based upon order of creation: first created -> highest priority. 19 | # See how all your routes lay out with "rake routes". 20 | 21 | # You can have the root of your site routed with "root" 22 | # root 'welcome#index' 23 | 24 | # Example of regular route: 25 | # get 'products/:id' => 'catalog#view' 26 | 27 | # Example of named route that can be invoked with purchase_url(id: product.id) 28 | # get 'products/:id/purchase' => 'catalog#purchase', as: :purchase 29 | 30 | # Example resource route (maps HTTP verbs to controller actions automatically): 31 | # resources :products 32 | 33 | # Example resource route with options: 34 | # resources :products do 35 | # member do 36 | # get 'short' 37 | # post 'toggle' 38 | # end 39 | # 40 | # collection do 41 | # get 'sold' 42 | # end 43 | # end 44 | 45 | # Example resource route with sub-resources: 46 | # resources :products do 47 | # resources :comments, :sales 48 | # resource :seller 49 | # end 50 | 51 | # Example resource route with more complex sub-resources: 52 | # resources :products do 53 | # resources :comments 54 | # resources :sales do 55 | # get 'recent', on: :collection 56 | # end 57 | # end 58 | 59 | # Example resource route with concerns: 60 | # concern :toggleable do 61 | # post 'toggle' 62 | # end 63 | # resources :posts, concerns: :toggleable 64 | # resources :photos, concerns: :toggleable 65 | 66 | # Example resource route within a namespace: 67 | # namespace :admin do 68 | # # Directs /admin/products/* to Admin::ProductsController 69 | # # (app/controllers/admin/products_controller.rb) 70 | # resources :products 71 | # end 72 | end 73 | -------------------------------------------------------------------------------- /config/secrets.yml: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Your secret key is used for verifying the integrity of signed cookies. 4 | # If you change this key, all old signed cookies will become invalid! 5 | 6 | # Make sure the secret is at least 30 characters and all random, 7 | # no regular words or you'll be exposed to dictionary attacks. 8 | # You can use `rake secret` to generate a secure secret key. 9 | 10 | # Make sure the secrets in this file are kept private 11 | # if you're sharing your code publicly. 12 | 13 | development: 14 | secret_key_base: 420955c2afa52b9d76ffee505e5fe87b1f15d765647bb69cef874240bfe5aac8d3e8aa7374a0dba52d69121d72f75e2300e607e0a423fb228c551ad16901fb1e 15 | 16 | facebook_api_key: <%= ENV["MEMAMUG_FACEBOOK_API_KEY"] %> 17 | facebook_api_secret: <%= ENV["MEMAMUG_FACEBOOK_API_SECRET"] %> 18 | 19 | s3_access_key_id: <%= ENV["S3_ACCESS_KEY_ID"] %> 20 | s3_secret_access_key: <%= ENV["S3_SECRET_ACCESS_KEY"] %> 21 | s3_bucket_name: <%= ENV["S3_BUCKET_NAME"] %> 22 | 23 | test: 24 | secret_key_base: 3b544c7e60923a879f6f1ed42068bb6e75dee285382739fb67bbe497ec0f1408bc68bef11678c293b7c7e32c83a6673d89ef57cd7bf15c166cc36f6ea23d72de 25 | 26 | # Do not keep production secrets in the repository, 27 | # instead read values from the environment. 28 | production: 29 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 30 | 31 | facebook_api_key: <%= ENV["MEMAMUG_FACEBOOK_API_KEY"] %> 32 | facebook_api_secret: <%= ENV["MEMAMUG_FACEBOOK_API_SECRET"] %> 33 | 34 | s3_access_key_id: <%= ENV["S3_ACCESS_KEY_ID"] %> 35 | s3_secret_access_key: <%= ENV["S3_SECRET_ACCESS_KEY"] %> 36 | s3_bucket_name: <%= ENV["S3_BUCKET_NAME"] %> 37 | -------------------------------------------------------------------------------- /config/unicorn.rb: -------------------------------------------------------------------------------- 1 | worker_processes Integer(ENV["WEB_CONCURRENCY"] || 3) 2 | timeout 15 3 | preload_app true 4 | 5 | before_fork do |server, worker| 6 | Signal.trap 'TERM' do 7 | puts 'Unicorn master intercepting TERM and sending myself QUIT instead' 8 | Process.kill 'QUIT', Process.pid 9 | end 10 | 11 | defined?(ActiveRecord::Base) and 12 | ActiveRecord::Base.connection.disconnect! 13 | end 14 | 15 | after_fork do |server, worker| 16 | Signal.trap 'TERM' do 17 | puts 'Unicorn worker intercepting TERM and doing nothing. Wait for master to send QUIT' 18 | end 19 | 20 | defined?(ActiveRecord::Base) and 21 | ActiveRecord::Base.establish_connection 22 | end -------------------------------------------------------------------------------- /db/migrate/20140808091949_create_users.rb: -------------------------------------------------------------------------------- 1 | class CreateUsers < ActiveRecord::Migration 2 | def change 3 | enable_extension "uuid-ossp" 4 | 5 | create_table :users, id: :uuid do |t| 6 | t.string :first_name, null: false 7 | t.string :last_name, null: false 8 | t.string :email, null: false 9 | 10 | t.timestamps null: false 11 | 12 | t.index :email, unique: true 13 | end 14 | 15 | create_table :access_tokens, id: :uuid do |t| 16 | t.uuid :user_id, null: false 17 | t.string :access_token, null: false 18 | t.datetime :expires_at, null: false 19 | t.timestamps null: false 20 | 21 | t.index :access_token, unique: true 22 | t.index [:access_token, :expires_at] 23 | t.index :expires_at 24 | end 25 | 26 | create_table :identities, id: :uuid do |t| 27 | t.uuid :user_id, null: false 28 | t.string :type, null: false 29 | t.string :uid, null: false 30 | 31 | t.string :api_token, null: false 32 | t.datetime :expires_at 33 | 34 | t.string :avatar_url 35 | t.timestamp :avatar_url_fetched_at 36 | 37 | t.timestamps null: false 38 | 39 | t.index :user_id 40 | t.index [:uid, :type], unique: true 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /db/migrate/20141218061754_create_contacts.rb: -------------------------------------------------------------------------------- 1 | class CreateContacts < ActiveRecord::Migration 2 | def change 3 | create_table :contacts, id: :uuid do |t| 4 | t.uuid :user_id, null: false 5 | 6 | t.string :display_name, null: false 7 | t.boolean :starred 8 | t.text :notes 9 | 10 | t.timestamp :subscribed_on 11 | 12 | t.timestamps null: false 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /db/migrate/20141218061807_create_photos.rb: -------------------------------------------------------------------------------- 1 | class CreatePhotos < ActiveRecord::Migration 2 | def change 3 | create_table :photos, id: :uuid do |t| 4 | t.uuid :contact_id, null: false 5 | 6 | t.attachment :image 7 | 8 | t.timestamps null: false 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20150620111310_create_reminder_mails.rb: -------------------------------------------------------------------------------- 1 | class CreateReminderMails < ActiveRecord::Migration 2 | def change 3 | create_table :reminder_mails, id: :uuid do |t| 4 | t.uuid :user_id, null: false 5 | t.string :status 6 | t.timestamp :created_at, null: false 7 | end 8 | 9 | create_table :contacts_reminder_mails, id: false do |t| 10 | t.uuid :reminder_mail_id, null: false 11 | t.uuid :contact_id, null: false 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # encoding: UTF-8 2 | # This file is auto-generated from the current state of the database. Instead 3 | # of editing this file, please use the migrations feature of Active Record to 4 | # incrementally modify your database, and then regenerate this schema definition. 5 | # 6 | # Note that this schema.rb definition is the authoritative source for your 7 | # database schema. If you need to create the application database on another 8 | # system, you should be using db:schema:load, not running all the migrations 9 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 10 | # you'll amass, the slower it'll run and the greater likelihood for issues). 11 | # 12 | # It's strongly recommended that you check this file into your version control system. 13 | 14 | ActiveRecord::Schema.define(version: 20150620111310) do 15 | 16 | # These are extensions that must be enabled in order to support this database 17 | enable_extension "plpgsql" 18 | enable_extension "uuid-ossp" 19 | 20 | create_table "access_tokens", id: :uuid, default: "uuid_generate_v4()", force: :cascade do |t| 21 | t.uuid "user_id", null: false 22 | t.string "access_token", null: false 23 | t.datetime "expires_at", null: false 24 | t.datetime "created_at" 25 | t.datetime "updated_at" 26 | end 27 | 28 | add_index "access_tokens", ["access_token", "expires_at"], name: "index_access_tokens_on_access_token_and_expires_at", using: :btree 29 | add_index "access_tokens", ["access_token"], name: "index_access_tokens_on_access_token", unique: true, using: :btree 30 | add_index "access_tokens", ["expires_at"], name: "index_access_tokens_on_expires_at", using: :btree 31 | 32 | create_table "contacts", id: :uuid, default: "uuid_generate_v4()", force: :cascade do |t| 33 | t.uuid "user_id", null: false 34 | t.string "display_name", null: false 35 | t.boolean "starred" 36 | t.text "notes" 37 | t.datetime "subscribed_on" 38 | t.datetime "created_at", null: false 39 | t.datetime "updated_at", null: false 40 | end 41 | 42 | create_table "contacts_reminder_mails", id: false, force: :cascade do |t| 43 | t.uuid "reminder_mail_id", null: false 44 | t.uuid "contact_id", null: false 45 | end 46 | 47 | create_table "identities", id: :uuid, default: "uuid_generate_v4()", force: :cascade do |t| 48 | t.uuid "user_id", null: false 49 | t.string "type", null: false 50 | t.string "uid", null: false 51 | t.string "api_token", null: false 52 | t.datetime "expires_at" 53 | t.string "avatar_url" 54 | t.datetime "avatar_url_fetched_at" 55 | t.datetime "created_at" 56 | t.datetime "updated_at" 57 | end 58 | 59 | add_index "identities", ["uid", "type"], name: "index_identities_on_uid_and_type", unique: true, using: :btree 60 | add_index "identities", ["user_id"], name: "index_identities_on_user_id", using: :btree 61 | 62 | create_table "photos", id: :uuid, default: "uuid_generate_v4()", force: :cascade do |t| 63 | t.uuid "contact_id", null: false 64 | t.string "image_file_name" 65 | t.string "image_content_type" 66 | t.integer "image_file_size" 67 | t.datetime "image_updated_at" 68 | t.datetime "created_at", null: false 69 | t.datetime "updated_at", null: false 70 | end 71 | 72 | create_table "reminder_mails", id: :uuid, default: "uuid_generate_v4()", force: :cascade do |t| 73 | t.uuid "user_id", null: false 74 | t.string "status" 75 | t.datetime "created_at", null: false 76 | end 77 | 78 | create_table "users", id: :uuid, default: "uuid_generate_v4()", force: :cascade do |t| 79 | t.string "first_name", null: false 80 | t.string "last_name", null: false 81 | t.string "email", null: false 82 | t.datetime "created_at" 83 | t.datetime "updated_at" 84 | end 85 | 86 | add_index "users", ["email"], name: "index_users_on_email", unique: true, using: :btree 87 | 88 | end 89 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesknelson/memamug-server/fbb639fe2b162ad35a46a0bb5b71a9217aac698c/db/seeds.rb -------------------------------------------------------------------------------- /lib/tasks/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesknelson/memamug-server/fbb639fe2b162ad35a46a0bb5b71a9217aac698c/lib/tasks/.keep -------------------------------------------------------------------------------- /lib/tasks/reminder_mail.thor: -------------------------------------------------------------------------------- 1 | class ReminderMail < Thor 2 | require File.expand_path("../../../config/environment.rb", __FILE__) 3 | 4 | desc 'send', "Send a reminder mail to anybody who is expecting one today, and hasn't yet received it today" 5 | def send 6 | today = Time.zone.today 7 | User.all.each do |user| 8 | contacts = user.reminder_contacts_for(today, today) 9 | 10 | if contacts.size > 0 11 | reminder_mail = user.reminder_mails.build(status: 'new') 12 | reminder_mail.contacts << contacts 13 | reminder_mail.save! 14 | 15 | begin 16 | ReminderMailer.daily_email(reminder_mail).deliver_now 17 | reminder_mail.update_attribute('status', 'sent') 18 | rescue => error 19 | reminder_mail.update_attribute('status', 'error') 20 | end 21 | end 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesknelson/memamug-server/fbb639fe2b162ad35a46a0bb5b71a9217aac698c/log/.keep -------------------------------------------------------------------------------- /public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesknelson/memamug-server/fbb639fe2b162ad35a46a0bb5b71a9217aac698c/public/.gitkeep -------------------------------------------------------------------------------- /test/controllers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesknelson/memamug-server/fbb639fe2b162ad35a46a0bb5b71a9217aac698c/test/controllers/.keep -------------------------------------------------------------------------------- /test/fixtures/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesknelson/memamug-server/fbb639fe2b162ad35a46a0bb5b71a9217aac698c/test/fixtures/.keep -------------------------------------------------------------------------------- /test/helpers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesknelson/memamug-server/fbb639fe2b162ad35a46a0bb5b71a9217aac698c/test/helpers/.keep -------------------------------------------------------------------------------- /test/integration/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesknelson/memamug-server/fbb639fe2b162ad35a46a0bb5b71a9217aac698c/test/integration/.keep -------------------------------------------------------------------------------- /test/mailers/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesknelson/memamug-server/fbb639fe2b162ad35a46a0bb5b71a9217aac698c/test/mailers/.keep -------------------------------------------------------------------------------- /test/models/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesknelson/memamug-server/fbb639fe2b162ad35a46a0bb5b71a9217aac698c/test/models/.keep -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | ENV['RAILS_ENV'] ||= 'test' 2 | require File.expand_path('../../config/environment', __FILE__) 3 | require 'rails/test_help' 4 | 5 | class ActiveSupport::TestCase 6 | # Setup all fixtures in test/fixtures/*.yml for all tests in alphabetical order. 7 | fixtures :all 8 | 9 | # Add more helper methods to be used by all tests here... 10 | end 11 | --------------------------------------------------------------------------------