├── .envDist ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .rubocop_todo.yml ├── .ruby-gemset ├── .ruby-version ├── Dockerfile ├── Gemfile ├── Gemfile.lock ├── LICENSE ├── Procfile ├── README.md ├── Rakefile ├── app ├── controllers │ ├── api │ │ └── v1 │ │ │ ├── articles_controller.rb │ │ │ ├── auth_controller.rb │ │ │ ├── sessions_controller.rb │ │ │ ├── spotlight_images_controller.rb │ │ │ ├── tags_controller.rb │ │ │ └── users_controller.rb │ ├── application_controller.rb │ ├── concerns │ │ └── authenticatable.rb │ └── graphql_controller.rb ├── graph │ ├── alumni_api_schema.rb │ ├── mutations │ │ ├── admin_mutations.rb │ │ ├── article_mutations.rb │ │ ├── auth_user_mutations.rb │ │ ├── feedback_mutations.rb │ │ ├── spotlight_image_mutations.rb │ │ ├── tag_mutations.rb │ │ └── user_mutations.rb │ └── types │ │ ├── admin_user_input_type.rb │ │ ├── article_input_type.rb │ │ ├── article_status_enum_type.rb │ │ ├── article_type.rb │ │ ├── auth_user_type.rb │ │ ├── authentication_token_type.rb │ │ ├── feedback_input_type.rb │ │ ├── feedback_type.rb │ │ ├── mutation_type.rb │ │ ├── profile_input_type.rb │ │ ├── query_type.rb │ │ ├── spotlight_image_type.rb │ │ ├── tag_input_type.rb │ │ ├── tag_type.rb │ │ └── user_type.rb ├── mailers │ ├── application_mailer.rb │ └── new_user_signup_mailer.rb ├── models │ ├── application_record.rb │ ├── article.rb │ ├── authentication_token.rb │ ├── concerns │ │ └── user_concerns.rb │ ├── feedback.rb │ ├── spotlight_image.rb │ ├── tag.rb │ └── user.rb ├── policies │ ├── application_policy.rb │ └── article_policy.rb ├── serializers │ ├── article_serializer.rb │ ├── session_serializer.rb │ ├── spotlight_image_serializer.rb │ ├── tag_serializer.rb │ └── user_serializer.rb ├── services │ └── token_issuer.rb └── views │ ├── layouts │ ├── mailer.html.erb │ └── mailer.text.erb │ └── new_user_signup_mailer │ └── welcome_email.html.erb ├── bin ├── bundle ├── rails ├── rake ├── rspec ├── setup ├── spring └── update ├── config.ru ├── config ├── application.rb ├── boot.rb ├── cable.yml ├── database.yml ├── environment.rb ├── environments │ ├── development.rb │ ├── production.rb │ └── test.rb ├── initializers │ ├── application_controller_renderer.rb │ ├── backtrace_silencers.rb │ ├── cors.rb │ ├── devise.rb │ ├── filter_parameter_logging.rb │ ├── inflections.rb │ ├── mime_types.rb │ ├── new_framework_defaults.rb │ └── wrap_parameters.rb ├── locales │ ├── devise.en.yml │ └── en.yml ├── pre_commit.yml ├── puma.rb ├── rack_cors.rb ├── routes.rb ├── secrets.yml └── spring.rb ├── db ├── migrate │ ├── 20160926175003_devise_create_users.rb │ ├── 20160927145825_add_attributes_to_user.rb │ ├── 20160927154706_create_articles.rb │ ├── 20160927183906_add_role_to_users.rb │ ├── 20160928191359_add_status_enum_to_article.rb │ ├── 20160928191930_add_default_non_null_to_article_status.rb │ ├── 20161009215058_add_bio_to_user.rb │ ├── 20161009233632_add_auth_token_to_users.rb │ ├── 20161010033700_create_tags.rb │ ├── 20161010160812_create_join_table_article_tag.rb │ ├── 20161015190453_create_spotlight_images.rb │ ├── 20161016203229_add_user_to_spotlight_images.rb │ ├── 20161017175954_add_json_to_article.rb │ ├── 20161017203917_add_index_to_tags.rb │ ├── 20161022211436_add_public_to_user.rb │ ├── 20161023165045_add_slug_to_article.rb │ ├── 20161118011519_create_feedbacks.rb │ ├── 20161202033652_create_authentication_tokens.rb │ ├── 20161202100913_remove_auth_token_from_user.rb │ └── 20161211192821_add_image_to_feedback.rb ├── schema.rb └── seeds.rb ├── docker-compose.yml ├── lib └── tasks │ ├── graphql.rake │ └── validate.rake ├── log └── .keep ├── public ├── robots.txt └── schema.json ├── spec ├── controllers │ ├── articles_controller_spec.rb │ ├── sessions_controller_spec.rb │ ├── tags_controller_spec.rb │ └── users_controller_spec.rb ├── factories │ ├── article.rb │ ├── feedbacks.rb │ ├── spotlight_images.rb │ ├── tags.rb │ └── users.rb ├── mailers │ ├── new_user_signup_spec.rb │ └── previews │ │ └── new_user_signup_preview.rb ├── models │ ├── article_spec.rb │ ├── feedback_spec.rb │ ├── spotlight_image_spec.rb │ ├── tag_spec.rb │ └── user_spec.rb ├── policies │ └── article_policy_spec.rb ├── rails_helper.rb ├── spec_helper.rb └── support │ ├── factory_girl.rb │ ├── request_helpers.rb │ └── shoulda_matchers.rb └── tmp └── .keep /.envDist: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacityalumni/udacity-alumni-api/3b3e2e883b18ad4633c8a3eae7975d4ce4d2a08f/.envDist -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore bundler config. 2 | /.bundle 3 | dm.sh 4 | .env 5 | 6 | # Ignore all logfiles and tempfiles. 7 | /log/* 8 | /tmp/* 9 | !/log/.keep 10 | !/tmp/.keep 11 | 12 | .idea/ 13 | .env -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | --require spec_helper 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | inherit_from: .rubocop_todo.yml 2 | AllCops: 3 | Include: 4 | - Rakefile 5 | - config.ru 6 | - lib/**/*.rake 7 | Exclude: 8 | - db/schema.rb 9 | - 'spec/**/*' 10 | 11 | Rails: 12 | Enabled: true 13 | -------------------------------------------------------------------------------- /.rubocop_todo.yml: -------------------------------------------------------------------------------- 1 | # This configuration was generated by 2 | # `rubocop --auto-gen-config` 3 | # on 2016-08-01 20:33:14 +0000 using RuboCop version 0.42.0. 4 | # The point is for the user to remove these configuration records 5 | # one by one as the offenses are removed from the code base. 6 | # Note that changes in the inspected code, or installation of new 7 | # versions of RuboCop, may require this file to be generated again. 8 | 9 | # Offense count: 25 10 | # Configuration parameters: AllowHeredoc, AllowURI, URISchemes. 11 | # URISchemes: http, https 12 | Metrics/LineLength: 13 | Max: 130 14 | 15 | # Offense count: 1 16 | # Configuration parameters: EnforcedStyle, SupportedStyles. 17 | # SupportedStyles: nested, compact 18 | Style/ClassAndModuleChildren: 19 | Exclude: 20 | - 'test/test_helper.rb' 21 | 22 | # Offense count: 3 23 | Style/Documentation: 24 | Exclude: 25 | - 'spec/**/*' 26 | - 'test/**/*' 27 | - 'app/mailers/application_mailer.rb' 28 | - 'app/models/application_record.rb' 29 | - 'config/application.rb' 30 | 31 | # Offense count: 2 32 | # Cop supports --auto-correct. 33 | Style/EmptyLines: 34 | Exclude: 35 | - 'config/environments/development.rb' 36 | - 'config/environments/production.rb' 37 | 38 | # Offense count: 1 39 | # Cop supports --auto-correct. 40 | # Configuration parameters: EnforcedStyle, SupportedStyles, UseHashRocketsWithSymbolValues, PreferHashRocketsForNonAlnumEndingSymbols. 41 | # SupportedStyles: ruby19, ruby19_no_mixed_keys, hash_rockets 42 | Style/HashSyntax: 43 | Exclude: 44 | - 'Gemfile' 45 | 46 | # Offense count: 2 47 | # Cop supports --auto-correct. 48 | Style/SpaceInsideBrackets: 49 | Exclude: 50 | - 'config/environments/production.rb' 51 | 52 | # Offense count: 14 53 | # Cop supports --auto-correct. 54 | # Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline. 55 | # SupportedStyles: single_quotes, double_quotes 56 | Style/StringLiterals: 57 | Exclude: 58 | - 'config/application.rb' 59 | - 'config/environments/production.rb' 60 | - 'config/puma.rb' 61 | -------------------------------------------------------------------------------- /.ruby-gemset: -------------------------------------------------------------------------------- 1 | rails5.0 2 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | ruby-2.3.1 2 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ruby:2.3.1 2 | 3 | # Install apt based dependencies required to run Rails as 4 | # well as RubyGems. As the Ruby image itself is based on a 5 | # Debian image, we use apt-get to install those. 6 | RUN apt-get update && apt-get install -y \ 7 | build-essential \ 8 | nodejs 9 | 10 | # Configure the main working directory. This is the base 11 | # directory used in any further RUN, COPY, and ENTRYPOINT 12 | # commands. 13 | RUN mkdir -p /app 14 | WORKDIR /app 15 | 16 | # Copy the Gemfile as well as the Gemfile.lock and install 17 | # the RubyGems. This is a separate step so the dependencies 18 | # will be cached unless changes to one of those two files 19 | # are made. 20 | COPY Gemfile Gemfile.lock ./ 21 | RUN gem install bundler && bundle install --jobs 20 --retry 5 22 | 23 | # Copy the main application. 24 | COPY . ./ 25 | 26 | # Expose port 3000 to the Docker host, so we can access it 27 | # from the outside. 28 | EXPOSE 3000 29 | 30 | CMD ["bundle", "exec", "rails", "server", "-b", "0.0.0.0"] 31 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | ruby '2.3.1' 4 | 5 | gem 'rails', '~> 5.0.0' 6 | gem 'pg' 7 | gem 'puma', '~> 3.0' 8 | gem 'rack-cors' 9 | gem 'ffaker' 10 | gem 'graphql' 11 | gem 'graphiql-rails' 12 | 13 | group :test do 14 | gem 'database_cleaner', '~> 1.5.3' 15 | end 16 | 17 | group :development, :test do 18 | gem 'byebug', platform: :mri 19 | gem 'rspec-rails' 20 | gem 'factory_girl_rails' 21 | gem 'shoulda-matchers', '~> 3.1' 22 | gem 'rails-controller-testing' 23 | gem 'pundit-matchers' 24 | end 25 | 26 | group :development do 27 | gem 'listen', '~> 3.0.5' 28 | gem 'spring' 29 | gem 'spring-watcher-listen', '~> 2.0.0' 30 | gem 'rubocop', '~> 0.42.0', require: false 31 | gem 'rubycritic', :require => false 32 | end 33 | 34 | # Windows does not include zoneinfo files, so bundle the tzinfo-data gem 35 | gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] 36 | 37 | # Authentication and authorization 38 | gem 'devise' 39 | gem 'pundit' 40 | gem 'active_model_serializers' 41 | gem 'has_secure_token' 42 | 43 | gem 'dotenv-rails', :groups => [:development, :test] 44 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | actioncable (5.0.0.1) 5 | actionpack (= 5.0.0.1) 6 | nio4r (~> 1.2) 7 | websocket-driver (~> 0.6.1) 8 | actionmailer (5.0.0.1) 9 | actionpack (= 5.0.0.1) 10 | actionview (= 5.0.0.1) 11 | activejob (= 5.0.0.1) 12 | mail (~> 2.5, >= 2.5.4) 13 | rails-dom-testing (~> 2.0) 14 | actionpack (5.0.0.1) 15 | actionview (= 5.0.0.1) 16 | activesupport (= 5.0.0.1) 17 | rack (~> 2.0) 18 | rack-test (~> 0.6.3) 19 | rails-dom-testing (~> 2.0) 20 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 21 | actionview (5.0.0.1) 22 | activesupport (= 5.0.0.1) 23 | builder (~> 3.1) 24 | erubis (~> 2.7.0) 25 | rails-dom-testing (~> 2.0) 26 | rails-html-sanitizer (~> 1.0, >= 1.0.2) 27 | active_model_serializers (0.10.3) 28 | actionpack (>= 4.1, < 6) 29 | activemodel (>= 4.1, < 6) 30 | jsonapi (= 0.1.1.beta2) 31 | activejob (5.0.0.1) 32 | activesupport (= 5.0.0.1) 33 | globalid (>= 0.3.6) 34 | activemodel (5.0.0.1) 35 | activesupport (= 5.0.0.1) 36 | activerecord (5.0.0.1) 37 | activemodel (= 5.0.0.1) 38 | activesupport (= 5.0.0.1) 39 | arel (~> 7.0) 40 | activesupport (5.0.0.1) 41 | concurrent-ruby (~> 1.0, >= 1.0.2) 42 | i18n (~> 0.7) 43 | minitest (~> 5.1) 44 | tzinfo (~> 1.1) 45 | addressable (2.5.0) 46 | public_suffix (~> 2.0, >= 2.0.2) 47 | arel (7.1.4) 48 | ast (2.3.0) 49 | axiom-types (0.1.1) 50 | descendants_tracker (~> 0.0.4) 51 | ice_nine (~> 0.11.0) 52 | thread_safe (~> 0.3, >= 0.3.1) 53 | bcrypt (3.1.11) 54 | builder (3.2.2) 55 | byebug (9.0.6) 56 | codeclimate-engine-rb (0.4.0) 57 | virtus (~> 1.0) 58 | coercible (1.0.0) 59 | descendants_tracker (~> 0.0.1) 60 | concurrent-ruby (1.0.2) 61 | database_cleaner (1.5.3) 62 | descendants_tracker (0.0.4) 63 | thread_safe (~> 0.3, >= 0.3.1) 64 | devise (4.2.0) 65 | bcrypt (~> 3.0) 66 | orm_adapter (~> 0.1) 67 | railties (>= 4.1.0, < 5.1) 68 | responders 69 | warden (~> 1.2.3) 70 | diff-lcs (1.2.5) 71 | dotenv (2.1.1) 72 | dotenv-rails (2.1.1) 73 | dotenv (= 2.1.1) 74 | railties (>= 4.0, < 5.1) 75 | equalizer (0.0.11) 76 | erubis (2.7.0) 77 | factory_girl (4.7.0) 78 | activesupport (>= 3.0.0) 79 | factory_girl_rails (4.7.0) 80 | factory_girl (~> 4.7.0) 81 | railties (>= 3.0.0) 82 | ffaker (2.3.0) 83 | ffi (1.9.14) 84 | flay (2.8.1) 85 | erubis (~> 2.7.0) 86 | path_expander (~> 1.0) 87 | ruby_parser (~> 3.0) 88 | sexp_processor (~> 4.0) 89 | flog (4.4.0) 90 | path_expander (~> 1.0) 91 | ruby_parser (~> 3.1, > 3.1.0) 92 | sexp_processor (~> 4.4) 93 | globalid (0.3.7) 94 | activesupport (>= 4.1.0) 95 | graphiql-rails (1.4.0) 96 | rails 97 | graphql (1.2.6) 98 | has_secure_token (1.0.0) 99 | activerecord (>= 3.0) 100 | i18n (0.7.0) 101 | ice_nine (0.11.2) 102 | json (1.8.3) 103 | jsonapi (0.1.1.beta2) 104 | json (~> 1.8) 105 | launchy (2.4.3) 106 | addressable (~> 2.3) 107 | listen (3.0.8) 108 | rb-fsevent (~> 0.9, >= 0.9.4) 109 | rb-inotify (~> 0.9, >= 0.9.7) 110 | loofah (2.0.3) 111 | nokogiri (>= 1.5.9) 112 | mail (2.6.4) 113 | mime-types (>= 1.16, < 4) 114 | method_source (0.8.2) 115 | mime-types (3.1) 116 | mime-types-data (~> 3.2015) 117 | mime-types-data (3.2016.0521) 118 | mini_portile2 (2.1.0) 119 | minitest (5.10.1) 120 | nio4r (1.2.1) 121 | nokogiri (1.6.8.1) 122 | mini_portile2 (~> 2.1.0) 123 | orm_adapter (0.5.0) 124 | parser (2.3.1.4) 125 | ast (~> 2.2) 126 | path_expander (1.0.1) 127 | pg (0.19.0) 128 | powerpack (0.1.1) 129 | public_suffix (2.0.4) 130 | puma (3.6.2) 131 | pundit (1.1.0) 132 | activesupport (>= 3.0.0) 133 | pundit-matchers (1.1.0) 134 | pundit (~> 1.1, >= 1.1.0) 135 | rspec-rails (>= 3.0.0) 136 | rack (2.0.1) 137 | rack-cors (0.4.0) 138 | rack-test (0.6.3) 139 | rack (>= 1.0) 140 | rails (5.0.0.1) 141 | actioncable (= 5.0.0.1) 142 | actionmailer (= 5.0.0.1) 143 | actionpack (= 5.0.0.1) 144 | actionview (= 5.0.0.1) 145 | activejob (= 5.0.0.1) 146 | activemodel (= 5.0.0.1) 147 | activerecord (= 5.0.0.1) 148 | activesupport (= 5.0.0.1) 149 | bundler (>= 1.3.0, < 2.0) 150 | railties (= 5.0.0.1) 151 | sprockets-rails (>= 2.0.0) 152 | rails-controller-testing (1.0.1) 153 | actionpack (~> 5.x) 154 | actionview (~> 5.x) 155 | activesupport (~> 5.x) 156 | rails-dom-testing (2.0.1) 157 | activesupport (>= 4.2.0, < 6.0) 158 | nokogiri (~> 1.6.0) 159 | rails-html-sanitizer (1.0.3) 160 | loofah (~> 2.0) 161 | railties (5.0.0.1) 162 | actionpack (= 5.0.0.1) 163 | activesupport (= 5.0.0.1) 164 | method_source 165 | rake (>= 0.8.7) 166 | thor (>= 0.18.1, < 2.0) 167 | rainbow (2.1.0) 168 | rake (11.3.0) 169 | rb-fsevent (0.9.8) 170 | rb-inotify (0.9.7) 171 | ffi (>= 0.5.0) 172 | reek (4.5.2) 173 | codeclimate-engine-rb (~> 0.4.0) 174 | parser (~> 2.3.1, >= 2.3.1.2) 175 | rainbow (~> 2.0) 176 | responders (2.3.0) 177 | railties (>= 4.2.0, < 5.1) 178 | rspec-core (3.5.4) 179 | rspec-support (~> 3.5.0) 180 | rspec-expectations (3.5.0) 181 | diff-lcs (>= 1.2.0, < 2.0) 182 | rspec-support (~> 3.5.0) 183 | rspec-mocks (3.5.0) 184 | diff-lcs (>= 1.2.0, < 2.0) 185 | rspec-support (~> 3.5.0) 186 | rspec-rails (3.5.2) 187 | actionpack (>= 3.0) 188 | activesupport (>= 3.0) 189 | railties (>= 3.0) 190 | rspec-core (~> 3.5.0) 191 | rspec-expectations (~> 3.5.0) 192 | rspec-mocks (~> 3.5.0) 193 | rspec-support (~> 3.5.0) 194 | rspec-support (3.5.0) 195 | rubocop (0.42.0) 196 | parser (>= 2.3.1.1, < 3.0) 197 | powerpack (~> 0.1) 198 | rainbow (>= 1.99.1, < 3.0) 199 | ruby-progressbar (~> 1.7) 200 | unicode-display_width (~> 1.0, >= 1.0.1) 201 | ruby-progressbar (1.8.1) 202 | ruby_parser (3.8.3) 203 | sexp_processor (~> 4.1) 204 | rubycritic (3.1.0) 205 | flay (~> 2.8) 206 | flog (~> 4.4) 207 | launchy (= 2.4.3) 208 | parser (= 2.3.1.4) 209 | rainbow 210 | reek (~> 4.4) 211 | ruby_parser (~> 3.8) 212 | virtus (~> 1.0) 213 | sexp_processor (4.7.0) 214 | shoulda-matchers (3.1.1) 215 | activesupport (>= 4.0.0) 216 | spring (2.0.0) 217 | activesupport (>= 4.2) 218 | spring-watcher-listen (2.0.1) 219 | listen (>= 2.7, < 4.0) 220 | spring (>= 1.2, < 3.0) 221 | sprockets (3.7.0) 222 | concurrent-ruby (~> 1.0) 223 | rack (> 1, < 3) 224 | sprockets-rails (3.2.0) 225 | actionpack (>= 4.0) 226 | activesupport (>= 4.0) 227 | sprockets (>= 3.0.0) 228 | thor (0.19.4) 229 | thread_safe (0.3.5) 230 | tzinfo (1.2.2) 231 | thread_safe (~> 0.1) 232 | unicode-display_width (1.1.1) 233 | virtus (1.0.5) 234 | axiom-types (~> 0.1) 235 | coercible (~> 1.0) 236 | descendants_tracker (~> 0.0, >= 0.0.3) 237 | equalizer (~> 0.0, >= 0.0.9) 238 | warden (1.2.6) 239 | rack (>= 1.0) 240 | websocket-driver (0.6.4) 241 | websocket-extensions (>= 0.1.0) 242 | websocket-extensions (0.1.2) 243 | 244 | PLATFORMS 245 | ruby 246 | 247 | DEPENDENCIES 248 | active_model_serializers 249 | byebug 250 | database_cleaner (~> 1.5.3) 251 | devise 252 | dotenv-rails 253 | factory_girl_rails 254 | ffaker 255 | graphiql-rails 256 | graphql 257 | has_secure_token 258 | listen (~> 3.0.5) 259 | pg 260 | puma (~> 3.0) 261 | pundit 262 | pundit-matchers 263 | rack-cors 264 | rails (~> 5.0.0) 265 | rails-controller-testing 266 | rspec-rails 267 | rubocop (~> 0.42.0) 268 | rubycritic 269 | shoulda-matchers (~> 3.1) 270 | spring 271 | spring-watcher-listen (~> 2.0.0) 272 | tzinfo-data 273 | 274 | RUBY VERSION 275 | ruby 2.3.1p112 276 | 277 | BUNDLED WITH 278 | 1.13.6 279 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: bundle exec puma -C config/puma.rb -p $PORT 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Codeship](https://codeship.com/projects/9c420570-75de-0134-b9f2-66e77c03031d/status?branch=master) 2 | 3 | # Alumni Web App API 4 | 5 | ## Getting Started 6 | 7 | The docker container will do _all_ of the networking between the database and app and will start a rails server. 8 | 9 | To open the correct URL in your web browser, you can run the following 10 | 11 | ``` 12 | open "http://$(docker-machine ip default):3000" 13 | ``` 14 | 15 | Which will print out the IP of the docker-machine and open a web browser to the correct URL where the server is serving from. 16 | 17 | ### Installing 18 | The installation is completely automated by Docker. Please reference the [Docker installation guide](https://docs.docker.com/engine/installation/) if you need any help getting setup. 19 | 20 | To start, clone the repository using git clone: 21 | ``` 22 | git clone https://github.com/udacityalumni/alumni-api.git 23 | ``` 24 | 25 | Running 26 | ``` 27 | docker-compose up 28 | ``` 29 | will do all of the heavy lifting for you in getting your app setup. 30 | 31 | Once installed, to access the docker container internally, please run 32 | ``` 33 | docker exec -it alumniapi_app_1 bash 34 | ``` 35 | 36 | In this example, the app container is named alumniapi_app_1. Please substitute the name of the container if the command fails due to the container name being changed. You can locate the container's name from listing all docker containers with: `docker ps -a`. 37 | 38 | #### Use without Docker 39 | The app can be run without Docker, if you are not favorable to it. You will want to get Rails v.5.0 and Ruby 2.3.1 installed on your machine. You can follow the guide [located here](http://railsapps.github.io/installing-rails.html). 40 | 41 | You must run the database setup scripts separately. 42 | 43 | ``` 44 | bundle install 45 | rake db:setup 46 | ``` 47 | 48 | ## Style and Linting 49 | The app is setup to use rubocop to lint from both the commandline and within text editors. Note that some of the generated code may fail the rubocop tests, but during development we need to ensure that our code is statically analyzed. 50 | 51 | The pipeline for linting has been baked right into the testing rake commands. 52 | The following 53 | ``` 54 | rake validate 55 | ``` 56 | 57 | will run both rubocop and the unit tests. 58 | 59 | On top of rubocop, another tool that we can use to increase the quality of our code is [Ruby Critic](https://github.com/whitesmith/rubycritic#getting-started). The gem is included with the repository and can be implemented through the commandline. 60 | 61 | For help: 62 | ``` 63 | rubycritic --help 64 | ``` 65 | 66 | ## Deployment 67 | TBD 68 | 69 | ## Built With 70 | Rails v5 Api Mode 71 | 72 | ## Authors 73 | 74 | * **Ryan Collins** 75 | 76 | ## License 77 | 78 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details 79 | 80 | ## Acknowledgments 81 | Thanks to everyone of the amazing contributors! 82 | -------------------------------------------------------------------------------- /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_relative 'config/application' 5 | 6 | Rails.application.load_tasks 7 | -------------------------------------------------------------------------------- /app/controllers/api/v1/articles_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::ArticlesController < ApplicationController 2 | before_action :set_article, only: [:show, :update, :destroy] 3 | after_action :verify_authorized 4 | 5 | respond_to :json 6 | 7 | # GET /articles 8 | def index 9 | @articles = Article.all 10 | authorize @articles 11 | 12 | render json: @articles 13 | end 14 | 15 | # GET /articles/1 16 | def show 17 | render json: @article 18 | end 19 | 20 | # POST /articles 21 | def create 22 | @article = Article.new(article_params) 23 | authorize @article 24 | 25 | if @article.save 26 | render json: @article, status: :created, location: :api_v1_articles 27 | else 28 | render json: @article.errors, status: :unprocessable_entity 29 | end 30 | end 31 | 32 | # PATCH/PUT /articles/1 33 | def update 34 | if @article.update(article_params) 35 | render json: @article 36 | else 37 | render json: @article.errors, status: :unprocessable_entity 38 | end 39 | end 40 | 41 | # DELETE /articles/1 42 | def destroy 43 | @article.destroy 44 | end 45 | 46 | private 47 | 48 | # Use callbacks to share common setup or constraints between actions. 49 | def set_article 50 | @article = Article.find(params[:id]) 51 | authorize @article 52 | end 53 | 54 | # Only allow a trusted parameter "white list" through. 55 | def article_params 56 | params.require(:article).permit(:title, :user_id, 57 | :featured, :spotlighted, 58 | :content, :json, 59 | :feature_image, :status, 60 | tags_attributes: [:tag]) 61 | end 62 | end 63 | -------------------------------------------------------------------------------- /app/controllers/api/v1/auth_controller.rb: -------------------------------------------------------------------------------- 1 | class AuthController < ApplicationController 2 | def is_signed_in? 3 | if user_signed_in? 4 | render :json => {"signed_in" => true, "user" => current_user}.to_json() 5 | else 6 | render :json => {"signed_in" => false}.to_json() 7 | end 8 | 9 | end 10 | end -------------------------------------------------------------------------------- /app/controllers/api/v1/sessions_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::SessionsController < ApplicationController 2 | 3 | def create 4 | user_password = params[:session][:password] 5 | user_email = params[:session][:email] 6 | # Find user by email (downcased!) <~~ Important! 7 | user = user_email.present? && User.find_by(email: user_email.downcase) 8 | if user && user.valid_password?(user_password) 9 | token = TokenIssuer.create_and_return_token(user, request) 10 | session = { auth_token: token } 11 | render status: :ok, json: { session: session } 12 | else 13 | render json: { errors: 'Invalid email or password' }, status: :unprocessable_entity 14 | end 15 | end 16 | 17 | def destroy 18 | auth_token = request.headers['Authorization'] 19 | user = User.get_user_from_token(auth_token) 20 | if user 21 | TokenIssuer.expire_token(user, request) 22 | head 204 23 | else 24 | render json: { errors: 'An error occured while deleting the token' }, status: :not_found 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /app/controllers/api/v1/spotlight_images_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::SpotlightImagesController < ApplicationController 2 | 3 | # Return all spotlight_images 4 | def index 5 | all_spotlight_images = SpotlightImage.all 6 | 7 | if all_spotlight_images 8 | render json: { 9 | spotlight_images: ActiveModel::Serializer::CollectionSerializer.new( 10 | all_spotlight_images, 11 | each_serializer: SpotlightImageSerializer, 12 | root: false 13 | ) 14 | } 15 | else 16 | render json: { errors: 'Invalid request' }, status: :unprocessable_entity 17 | end 18 | end 19 | 20 | # Create a new spotlight image 21 | def create 22 | auth_token = request.headers['Authorization'] 23 | user = User.get_user_from_token(auth_token) 24 | spotlight_image = user.spotlight_images.build(spotlight_image_params) 25 | if spotlight_image.save 26 | render json: spotlight_image, 27 | status: 201, 28 | location: :api_v1_spotlight_images 29 | else 30 | render json: { errors: 'Invalid Request' }, status: :unprocessable_entity 31 | end 32 | end 33 | 34 | def delete 35 | auth_token = request.headers['Authorization'] 36 | if auth_token.valid? 37 | spotlight_image = SpotlightImage.find_by_id(params[:id]) 38 | if spotlight_image 39 | render json: spotlight_image, 40 | status: 201, 41 | location: :api_v1_spotlight_images 42 | else 43 | render json: { errors: 'Invalid Request' }, status: :unprocessable_entity 44 | end 45 | else 46 | render json: { errors: 'Invalid Request' }, status: :unprocessable_entity 47 | end 48 | end 49 | 50 | private 51 | 52 | # Spotlight_Image params accepted at this point for creating 53 | # a spotlight_image are as shown: 54 | def spotlight_image_params 55 | params.require(:spotlight_image).permit(:url, :user) 56 | end 57 | end 58 | -------------------------------------------------------------------------------- /app/controllers/api/v1/tags_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::TagsController < ApplicationController 2 | before_action :set_tag, only: [:show, :update, :destroy] 3 | respond_to :json 4 | 5 | # GET /tags 6 | def index 7 | @tags = Tag.all 8 | render json: @tags 9 | end 10 | 11 | # GET /tag/# 12 | def show 13 | render json: @tag 14 | end 15 | 16 | # POST /tags 17 | def create 18 | @tag = Tag.new(tag_params) 19 | if @tag.save 20 | render json: @tag, status: :created, location: :api_v1_tags 21 | else 22 | render json: @tag.errors, status: :unprocessable_entity 23 | end 24 | end 25 | 26 | # PATCH/PUT /tags/# 27 | def update 28 | if @tag.update(tag_params) 29 | render json: @tag 30 | else 31 | render json: @tag.errors, status: :unprocessable_entity 32 | end 33 | end 34 | 35 | # DELETE /tags/# 36 | def destroy 37 | @tag.destroy 38 | end 39 | 40 | private 41 | 42 | # Use callbacks to share common setup or constraints between actions. 43 | def set_tag 44 | @tag = Tag.find(params[:id]) 45 | end 46 | 47 | # Only allow a trusted parameter "white list" through. 48 | def tag_params 49 | params.require(:tag).permit(:tag, :articles) 50 | end 51 | 52 | end 53 | -------------------------------------------------------------------------------- /app/controllers/api/v1/users_controller.rb: -------------------------------------------------------------------------------- 1 | class Api::V1::UsersController < ApplicationController 2 | before_action :authenticate_with_token!, only: [:update, :destroy] 3 | 4 | def show 5 | auth_token = request.headers['Authorization'] 6 | current_user = User.get_user_from_token(auth_token) 7 | if current_user 8 | respond_with current_user, serializer: UserSerializer 9 | else 10 | render json: { errors: 'Invalid request' }, status: :unprocessable_entity 11 | end 12 | end 13 | 14 | def create 15 | user = User.new(user_params) 16 | if user.save 17 | render json: user, status: 201, location: :api_v1_users 18 | else 19 | render json: { errors: user.errors }, status: :unprocessable_entity 20 | end 21 | end 22 | 23 | def update 24 | if current_user.update(user_params) 25 | render json: current_user, 26 | serializer: UserSerializer, 27 | status: 200, 28 | location: :api_v1_users 29 | else 30 | render json: { errors: current_user.errors }, status: :unprocessable_entity 31 | end 32 | end 33 | 34 | private 35 | 36 | # User params accepted at this point for creating a user are as shown: 37 | def user_params 38 | params.require(:user).permit(:password, :password_confirmation, :bio, 39 | :email, :name, :avatar, :public) 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /app/controllers/application_controller.rb: -------------------------------------------------------------------------------- 1 | class ApplicationController < ActionController::API 2 | respond_to :json 3 | include Pundit 4 | include Authenticatable 5 | # prepare Devise to accept json requests 6 | # protect_from_forgery with: :null_session 7 | before_action :configure_permitted_parameters, if: :devise_controller? 8 | end 9 | -------------------------------------------------------------------------------- /app/controllers/concerns/authenticatable.rb: -------------------------------------------------------------------------------- 1 | module Authenticatable 2 | def current_user 3 | @current_user ||= User.get_user_from_token(request.headers['Authorization']) 4 | end 5 | 6 | def authenticate_with_token! 7 | render json: { errors: 'Not authenticated' }, status: :unauthorized unless current_user.present? 8 | end 9 | 10 | def user_signed_in? 11 | current_user.present? 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /app/controllers/graphql_controller.rb: -------------------------------------------------------------------------------- 1 | class GraphqlController < ActionController::API 2 | 3 | def new 4 | end 5 | 6 | def create 7 | query_string = params[:query] 8 | query_variables = ensure_hash(params[:variables]) 9 | result = AlumniApiSchema.execute( 10 | query_string, 11 | variables: query_variables, 12 | context: { } 13 | ) 14 | render json: result 15 | end 16 | 17 | private 18 | 19 | def ensure_hash(query_variables) 20 | if query_variables.blank? 21 | {} 22 | elsif query_variables.is_a?(String) 23 | JSON.parse(query_variables) 24 | else 25 | query_variables 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/graph/alumni_api_schema.rb: -------------------------------------------------------------------------------- 1 | AlumniApiSchema = GraphQL::Schema.define( 2 | query: QueryType, 3 | mutation: MutationType 4 | ) 5 | -------------------------------------------------------------------------------- /app/graph/mutations/admin_mutations.rb: -------------------------------------------------------------------------------- 1 | module AdminMutations 2 | UpdateUser = GraphQL::Relay::Mutation.define do 3 | name 'UpdateUser' 4 | input_field :auth_token, !types.String, 'The admin auth token' 5 | input_field :user_id, !types.ID, 'The user ID' 6 | input_field :user, AdminUserInputType 7 | return_field :user, AuthUserType 8 | 9 | resolve -> (_obj, inputs, _ctx) do 10 | auth_token = inputs[:auth_token] 11 | admin = User.get_user_from_token(auth_token) 12 | if admin && admin.role == "admin" 13 | user = User.find_by(id: inputs[:user_id]) 14 | user.name = inputs[:user][:name] if inputs[:user][:name] 15 | user.bio = inputs[:user][:bio] if inputs[:user][:bio] 16 | user.avatar = inputs[:user][:avatar] if inputs[:user][:avatar] 17 | user.email = inputs[:user][:email] if inputs[:user][:email] 18 | user.role = inputs[:user][:role] if inputs[:user][:role] 19 | public_input = inputs[:user][:public] 20 | user.public = public_input unless inputs[:user][:public].nil? 21 | user.save! 22 | { 23 | user: user 24 | } 25 | end 26 | end 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /app/graph/mutations/article_mutations.rb: -------------------------------------------------------------------------------- 1 | module ArticleMutations 2 | Create = GraphQL::Relay::Mutation.define do 3 | name 'CreateArticle' 4 | input_field :auth_token, !types.String 5 | input_field :article, ArticleInputType 6 | 7 | return_field :article, ArticleType 8 | 9 | resolve -> (_obj, inputs, _ctx) do 10 | auth_token = inputs[:auth_token] 11 | user = User.get_user_from_token(auth_token) 12 | article_inputs = inputs[:article] 13 | if user 14 | article = Article.create( 15 | title: article_inputs[:title], 16 | content: article_inputs[:content], 17 | json: article_inputs[:json], 18 | status: article_inputs[:status], 19 | spotlighted: article_inputs[:spotlighted] 20 | ) 21 | article.user = user 22 | article.feature_image = article_inputs[:feature_image] if article_inputs[:feature_image] 23 | if article_inputs[:tags] 24 | article_inputs[:tags].to_a.each do |tag| 25 | tag = Tag.find_or_create_by(tag: tag.to_h["tag"]) 26 | article.tags << tag 27 | end 28 | end 29 | # Make sure tags are unique 30 | article.tags = article.tags.uniq{ |art| art.tag } 31 | article.save! 32 | { 33 | article: article 34 | } 35 | end 36 | end 37 | end 38 | Update = GraphQL::Relay::Mutation.define do 39 | name 'UpdateArticle' 40 | input_field :auth_token, !types.String 41 | input_field :id, !types.ID 42 | input_field :article, ArticleInputType 43 | 44 | return_field :article, ArticleType 45 | resolve -> (_obj, inputs, _ctx) do 46 | auth_token = inputs[:auth_token] 47 | user = User.get_user_from_token(auth_token) 48 | article_inputs = inputs[:article] 49 | if user 50 | article = Article.find_by_id(inputs[:id]) 51 | article.update( 52 | title: article_inputs[:title], 53 | content: article_inputs[:content], 54 | json: article_inputs[:json], 55 | status: article_inputs[:status], 56 | spotlighted: article_inputs[:spotlighted] 57 | ) 58 | article.feature_image = article_inputs[:feature_image] if article_inputs[:feature_image] 59 | if article_inputs[:tags] 60 | article_inputs[:tags].to_a.uniq.each do |tag| 61 | the_tag = Tag.find_or_create_by(tag: tag.to_h["tag"]) 62 | article.tags << the_tag 63 | end 64 | end 65 | # Make sure tags are unique 66 | article.save! 67 | { 68 | article: article 69 | } 70 | end 71 | end 72 | end 73 | Delete = GraphQL::Relay::Mutation.define do 74 | name 'DeleteArticle' 75 | input_field :auth_token, !types.String 76 | input_field :id, !types.ID 77 | return_field :deleted_id, types.ID 78 | resolve -> (_obj, inputs, _ctx) do 79 | article = Article.find_by_id(inputs[:id]) 80 | id = article.id 81 | if article.destroy 82 | { 83 | deleted_id: id 84 | } 85 | end 86 | end 87 | end 88 | end 89 | -------------------------------------------------------------------------------- /app/graph/mutations/auth_user_mutations.rb: -------------------------------------------------------------------------------- 1 | module AuthUserMutations 2 | UpdateProfile = GraphQL::Relay::Mutation.define do 3 | name 'UpdateProfile' 4 | description 'Update the user profile' 5 | input_field :auth_token, !types.String 6 | input_field :profile, ProfileInputType 7 | 8 | return_field :user, AuthUserType 9 | resolve -> (_obj, inputs, _ctx) do 10 | auth_token = inputs[:auth_token] 11 | user = User.get_user_from_token(auth_token) 12 | user.name = inputs[:profile][:name] if inputs[:profile][:name] 13 | user.bio = inputs[:profile][:bio] if inputs[:profile][:bio] 14 | user.avatar = inputs[:profile][:avatar] if inputs[:profile][:avatar] 15 | user.email = inputs[:profile][:email] if inputs[:profile][:email] 16 | public_input = inputs[:profile][:public] 17 | user.public = public_input unless inputs[:profile][:public].nil? 18 | user.save! 19 | { 20 | user: user 21 | } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/graph/mutations/feedback_mutations.rb: -------------------------------------------------------------------------------- 1 | module FeedbackMutations 2 | Create = GraphQL::Relay::Mutation.define do 3 | name 'CreateFeedback' 4 | description 'User submitted feedback for the site' 5 | input_field :feedback, FeedbackInputType 6 | input_field :auth_token, !types.String 7 | return_field :feedback, FeedbackType 8 | 9 | resolve -> (_obj, inputs, _ctx) do 10 | auth_token = inputs[:auth_token] 11 | @user = User.get_user_from_token(auth_token) 12 | feedback = Feedback.create( 13 | description: inputs[:feedback][:description], 14 | url: inputs[:feedback][:url], 15 | image: inputs[:feedback][:image].read 16 | ) 17 | @user.feedback << feedback 18 | @user.save! 19 | { 20 | feedback: feedback 21 | } 22 | end 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /app/graph/mutations/spotlight_image_mutations.rb: -------------------------------------------------------------------------------- 1 | module SpotlightImageMutations 2 | Create = GraphQL::Relay::Mutation.define do 3 | name 'CreateSpotlightImage' 4 | input_field :auth_token, !types.String 5 | input_field :url, !types.String 6 | 7 | return_field :spotlight_image, SpotlightImageType 8 | resolve -> (_obj, inputs, _ctx) do 9 | auth_token = inputs[:auth_token] 10 | user = User.get_user_from_token(auth_token) 11 | image = SpotlightImage.new( 12 | url: inputs[:url] 13 | ) 14 | image.user = user 15 | image.save! 16 | { 17 | spotlight_image: image 18 | } 19 | end 20 | end 21 | Update = GraphQL::Relay::Mutation.define do 22 | name 'UpdateSpotlightImage' 23 | input_field :auth_token, !types.String 24 | input_field :id, !types.ID 25 | input_field :url, !types.String 26 | 27 | return_field :spotlight_image, SpotlightImageType 28 | resolve -> (_obj, inputs, _ctx) do 29 | auth_token = inputs[:auth_token] 30 | user = User.get_user_from_token(auth_token) 31 | image = SpotlightImage.find_by(id: inputs[:id]) 32 | image.update(url: inputs[:url], user: user) 33 | image.save! 34 | { 35 | spotlight_image: image 36 | } 37 | end 38 | end 39 | Delete = GraphQL::Relay::Mutation.define do 40 | name 'DeleteSpotlightImage' 41 | input_field :auth_token, !types.String 42 | input_field :id, !types.ID 43 | 44 | return_field :deleted_id, !types.ID 45 | 46 | resolve -> (_obj, inputs, _ctx) do 47 | image = SpotlightImage.find_by_id(inputs[:id]) 48 | deleted_id = image.id 49 | image.destroy 50 | { 51 | deleted_id: deleted_id 52 | } 53 | end 54 | end 55 | end 56 | -------------------------------------------------------------------------------- /app/graph/mutations/tag_mutations.rb: -------------------------------------------------------------------------------- 1 | module TagMutations 2 | Create = GraphQL::Relay::Mutation.define do 3 | name 'CreateTag' 4 | input_field :tag, TagInputType 5 | input_field :auth_token, !types.String 6 | 7 | return_field :tags, types[TagType] 8 | 9 | resolve -> (_obj, inputs, _ctx) do 10 | auth_token = inputs[:auth_token] 11 | user = User.get_user_from_token(auth_token) 12 | Tag.create( 13 | tag: inputs[:tag].tag, 14 | user: user 15 | ) 16 | tags = Tag.all 17 | tags 18 | end 19 | end 20 | SaveAll = GraphQL::Relay::Mutation.define do 21 | name 'SaveAll' 22 | input_field :tags, types[TagInputType] 23 | input_field :auth_token, !types.String 24 | return_field :tags, types[TagType] 25 | 26 | resolve -> (_obj, inputs, _ctx) do 27 | tags = inputs[:tags].to_a 28 | auth_token = inputs[:auth_token] 29 | user = User.get_user_from_token(auth_token) 30 | tags.each do |tag| 31 | Tag.find_or_create_by(tag: tag.tag, user: user) 32 | end 33 | Tag.all 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /app/graph/mutations/user_mutations.rb: -------------------------------------------------------------------------------- 1 | module UserMutations 2 | RequestPasswordInstructions = GraphQL::Relay::Mutation.define do 3 | name 'RequestPasswordInstructions' 4 | description 'When a user requests that their password be reset, send them an email' 5 | input_field :email, !types.String 6 | 7 | return_field :success, types.Boolean 8 | resolve -> (_obj, inputs, _ctx) do 9 | user = User.find_by(email: inputs[:email]) 10 | if user 11 | user.send_reset_password_instructions 12 | { 13 | success: true 14 | } 15 | else 16 | { 17 | success: false 18 | } 19 | end 20 | end 21 | end 22 | ResetPassword = GraphQL::Relay::Mutation.define do 23 | name 'ResetPassword' 24 | description 'When a user returns to the password reset route with a token to reset password' 25 | input_field :token, !types.String 26 | input_field :password, !types.String 27 | input_field :password_confirmation, !types.String 28 | 29 | return_field :user, AuthUserType 30 | resolve -> (_obj, inputs, _ctx) do 31 | user = User.with_reset_password_token(inputs[:token]) 32 | if user 33 | User.reset_password_by_token( 34 | reset_password_token: inputs[:token], 35 | password: inputs[:password], 36 | password_confirmation: inputs[:password_confirmation] 37 | ) 38 | user.reload 39 | { 40 | user: user 41 | } 42 | else 43 | { 44 | user: nil 45 | } 46 | end 47 | end 48 | end 49 | end 50 | -------------------------------------------------------------------------------- /app/graph/types/admin_user_input_type.rb: -------------------------------------------------------------------------------- 1 | AdminUserInputType = GraphQL::InputObjectType.define do 2 | name 'AdminUserInput' 3 | description 'Admin user interactions via content dashboard' 4 | input_field :name, types.String, 'The name of the user' 5 | input_field :email, types.String, 'The email of the user' 6 | input_field :bio, types.String, 'The bio of the user' 7 | input_field :avatar, types.String, 'The user avatar' 8 | input_field :role, types.String, 'The user role' 9 | input_field :public, types.Boolean, 'Whether the user is public or not' 10 | end 11 | -------------------------------------------------------------------------------- /app/graph/types/article_input_type.rb: -------------------------------------------------------------------------------- 1 | ArticleInputType = GraphQL::InputObjectType.define do 2 | name 'ArticleInput' 3 | description 'The input for articles for the site' 4 | input_field :status, ArticleStatusEnumType, 'The article status' 5 | input_field :content, types.String, 'The content of the article' 6 | input_field :json, types.String, 'The json content of the article' 7 | input_field :title, types.String, 'The title of the article' 8 | input_field :feature_image, types.String, 'The featured image for the article' 9 | input_field :spotlighted, types.Boolean, 'Whether the article is spotlighted' 10 | input_field :featured, types.Boolean, 'Whether the article is featured' 11 | input_field :tags, -> { types[TagInputType] }, 'Tags associated with the article' 12 | end 13 | -------------------------------------------------------------------------------- /app/graph/types/article_status_enum_type.rb: -------------------------------------------------------------------------------- 1 | ArticleStatusEnumType = GraphQL::EnumType.define do 2 | name 'ArticleStatusEnum' 3 | description 'The status of the event' 4 | value 'draft' 5 | value 'published' 6 | value 'archived' 7 | end 8 | -------------------------------------------------------------------------------- /app/graph/types/article_type.rb: -------------------------------------------------------------------------------- 1 | ArticleType = GraphQL::ObjectType.define do 2 | name 'Article' 3 | description 'The articles for the site' 4 | field :id, types.ID, 'The id of the article' 5 | field :status, ArticleStatusEnumType, 'The article status' 6 | field :slug, types.String, 'The article slug' 7 | field :content, types.String, 'The content of the article' 8 | field :json, types.String, 'The json content of the article' 9 | field :title, types.String, 'The title of the article' 10 | field :user, UserType, 'The user who posted the article' 11 | field :feature_image, types.String, 'The featured image for the article' 12 | field :created_at, types.String, 'The date the article was created' 13 | field :updated_at, types.String, 'The date the article was updated' 14 | field :spotlighted, types.Boolean, 'Whether the article is spotlighted' 15 | field :featured, types.Boolean, 'Whether the article is featured' 16 | field :tags, -> { types[TagType] }, 'Tags associated with the article' 17 | end 18 | -------------------------------------------------------------------------------- /app/graph/types/auth_user_type.rb: -------------------------------------------------------------------------------- 1 | AuthUserType = GraphQL::ObjectType.define do 2 | name 'AuthUser' 3 | description 'The authenticated user model type' 4 | field :id, types.ID, 'The id of this user' 5 | field :name, !types.String, 'The name of the user' 6 | field :email, !types.String, 'The email of the user' 7 | field :bio, types.String, 'The bio of the user' 8 | field :avatar, types.String, 'The user avatar' 9 | field :authentication_tokens, types[AuthenticationTokenType], 'The user auth tokens' 10 | field :role, !types.String, 'The user role' 11 | field :public, !types.Boolean, 'Whether the user is public or not' 12 | end 13 | -------------------------------------------------------------------------------- /app/graph/types/authentication_token_type.rb: -------------------------------------------------------------------------------- 1 | AuthenticationTokenType = GraphQL::ObjectType.define do 2 | name 'AuthenticatonToken' 3 | description 'The authentication tokens' 4 | field :body, types.String, 'The body of the token' 5 | end 6 | -------------------------------------------------------------------------------- /app/graph/types/feedback_input_type.rb: -------------------------------------------------------------------------------- 1 | FeedbackInputType = GraphQL::InputObjectType.define do 2 | name 'FeedbackInput' 3 | description 'Feedback submitted by a user, input type' 4 | input_field :description, !types.String, 'Description of the feedback' 5 | input_field :url, types.String, 'The url where the feedback came from' 6 | input_field :image, types.String, 'Image uploaded by the user' 7 | end -------------------------------------------------------------------------------- /app/graph/types/feedback_type.rb: -------------------------------------------------------------------------------- 1 | FeedbackType = GraphQL::ObjectType.define do 2 | name 'Feedback' 3 | description 'Feedback submitted by a user' 4 | field :id, !types.ID, 'The tag id' 5 | field :description, !types.String, 'Description of the feedback' 6 | field :url, types.String, 'The url where the feedback came from' 7 | field :image, types.String, 'The image that the user uploaded' 8 | field :user, AuthUserType, 'The user that submitted the feedback' 9 | field :created_at, !types.String, 'When the feedback was created' 10 | field :updated_at, !types.String, 'When the feedback was updated' 11 | end 12 | -------------------------------------------------------------------------------- /app/graph/types/mutation_type.rb: -------------------------------------------------------------------------------- 1 | MutationType = GraphQL::ObjectType.define do 2 | name 'Mutation' 3 | 4 | field :CreateTag, field: TagMutations::Create.field 5 | field :SaveAll, field: TagMutations::SaveAll.field 6 | 7 | field :CreateArticle, field: ArticleMutations::Create.field 8 | field :UpdateArticle, field: ArticleMutations::Update.field 9 | field :DeleteArticle, field: ArticleMutations::Delete.field 10 | 11 | field :CreateSpotlightImage, field: SpotlightImageMutations::Create.field 12 | field :UpdateSpotlightImage, field: SpotlightImageMutations::Update.field 13 | field :DeleteSpotlightImage, field: SpotlightImageMutations::Delete.field 14 | 15 | field :UpdateProfile, field: AuthUserMutations::UpdateProfile.field 16 | 17 | field :RequestPasswordInstructions, field: UserMutations::RequestPasswordInstructions.field 18 | field :ResetPassword, field: UserMutations::ResetPassword.field 19 | 20 | field :CreateFeedback, field: FeedbackMutations::Create.field 21 | 22 | field :AdminUpdateUser, field: AdminMutations::UpdateUser.field 23 | end 24 | -------------------------------------------------------------------------------- /app/graph/types/profile_input_type.rb: -------------------------------------------------------------------------------- 1 | ProfileInputType = GraphQL::InputObjectType.define do 2 | name 'ProfileInput' 3 | description 'The user profile type' 4 | input_field :name, types.String, 'The name of the user' 5 | input_field :email, types.String, 'The email of the user' 6 | input_field :bio, types.String, 'The bio of the user' 7 | input_field :avatar, types.String, 'The user avatar' 8 | input_field :public, types.Boolean, 'Whether the user is public or not' 9 | end 10 | -------------------------------------------------------------------------------- /app/graph/types/query_type.rb: -------------------------------------------------------------------------------- 1 | QueryType = GraphQL::ObjectType.define do 2 | name 'Query' 3 | description 'The root level query type' 4 | 5 | field :allUsers, types[AuthUserType] do 6 | argument :auth_token, !types.String 7 | 8 | resolve -> (_obj, args, _ctx) do 9 | auth_token = args[:auth_token] 10 | user = User.get_user_from_token(auth_token) 11 | if user && user.role == "admin" 12 | users = User.all.sort { |a, b| a.name.downcase <=> b.name.downcase } 13 | users 14 | end 15 | end 16 | end 17 | field :allArticles, types[ArticleType] do 18 | argument :auth_token, !types.String 19 | 20 | resolve -> (_obj, args, _ctx) do 21 | auth_token = args[:auth_token] 22 | user = User.get_user_from_token(auth_token) 23 | if user && user.role == "admin" 24 | articles = Article.all.sort_by(&:created_at) 25 | articles 26 | elsif user && user.role == "author" 27 | articles = Article.where(user: user).sort_by(&:created_at) 28 | articles 29 | end 30 | end 31 | end 32 | field :userRoles, types[types.String] do 33 | resolve -> (_obj, args, _ctx) do 34 | User.roles.map { |a| a[0] } 35 | end 36 | end 37 | field :articles, types[ArticleType] do 38 | argument :tag, types.String 39 | argument :first, types.Int 40 | 41 | resolve -> (_obj, args, _ctx) do 42 | articles = Article.where(status: 'published') 43 | if args[:tag] 44 | tag = Tag.where(tag: args[:tag]).first 45 | tag.articles.where(status: 'published') 46 | elsif args[:first] 47 | articles.first(args[:first]) 48 | else 49 | articles 50 | end 51 | end 52 | end 53 | field :articleFeedCount, types.Int do 54 | resolve -> (_obj, args, _ctx) { 55 | Article.where(status: 'published', spotlighted: false).count 56 | } 57 | end 58 | field :articleFeed, types[ArticleType] do 59 | argument :first, types.Int 60 | resolve -> (_obj, args, _ctx) do 61 | articles = Article.where(spotlighted: false, status: 'published') 62 | if args[:first] 63 | articles.first(args[:first]) 64 | else 65 | articles 66 | end 67 | end 68 | end 69 | field :article, ArticleType do 70 | argument :id, types.ID 71 | argument :slug, types.String 72 | resolve -> (_obj, args, _ctx) do 73 | if args[:slug] 74 | Article.find_by(slug: args[:slug]) 75 | else 76 | Article.find_by(id: args[:id]) 77 | end 78 | end 79 | end 80 | field :tags, types[TagType] do 81 | resolve -> (_obj, args, _ctx) do 82 | Tag.all 83 | end 84 | end 85 | field :tag, TagType do 86 | argument :id, types.ID 87 | resolve -> (_obj, args, _ctx) do 88 | tag = Tag.find_by(id: args[:id]) 89 | tag 90 | end 91 | end 92 | field :spotlightImages, types[SpotlightImageType] do 93 | resolve -> (_obj, args, _ctx) do 94 | SpotlightImage.all 95 | end 96 | end 97 | field :spotlightImage, SpotlightImageType do 98 | argument :id, types.ID 99 | resolve -> (_obj, args, _ctx) do 100 | image = SpotlightImage.find_by(id: args[:id]) 101 | image 102 | end 103 | end 104 | field :authUser, AuthUserType do 105 | argument :auth_token, !types.String 106 | resolve -> (_obj, args, _ctx) do 107 | auth_token = args[:auth_token] 108 | User.get_user_from_token(auth_token) 109 | end 110 | end 111 | field :publicUsers, types[UserType] do 112 | resolve -> (_obj, args, _ctx) do 113 | User.where(public: true) 114 | end 115 | end 116 | field :user, UserType do 117 | argument :id, !types.ID 118 | resolve -> (_obj, args, _ctx) do 119 | User.where(public: true, id: args[:id]).first 120 | end 121 | end 122 | field :feedback, types[FeedbackType] do 123 | argument :auth_token, !types.String 124 | resolve -> (_obj, args, _ctx) do 125 | auth_token = args[:auth_token] 126 | user = User.get_user_from_token(auth_token) 127 | if user && user.role == 'admin' 128 | Feedback.all 129 | end 130 | end 131 | end 132 | end 133 | -------------------------------------------------------------------------------- /app/graph/types/spotlight_image_type.rb: -------------------------------------------------------------------------------- 1 | SpotlightImageType = GraphQL::ObjectType.define do 2 | name 'SpotlightImage' 3 | description 'The type for the carousel spotlight images' 4 | field :id, !types.ID, 'The id of the image' 5 | field :url, !types.String, 'The url of the image' 6 | end 7 | -------------------------------------------------------------------------------- /app/graph/types/tag_input_type.rb: -------------------------------------------------------------------------------- 1 | TagInputType = GraphQL::InputObjectType.define do 2 | name 'TagInput' 3 | description 'Input type for a tag' 4 | input_field :tag, !types.String 5 | end 6 | -------------------------------------------------------------------------------- /app/graph/types/tag_type.rb: -------------------------------------------------------------------------------- 1 | TagType = GraphQL::ObjectType.define do 2 | name 'Tag' 3 | description 'The article tags model' 4 | field :id, !types.ID, 'The tag id' 5 | field :tag, !types.String, 'The tag title' 6 | field :slug, !types.String, 'The tag slug' 7 | field :articles, types[ArticleType], 'The articles associated with the tag' 8 | end 9 | -------------------------------------------------------------------------------- /app/graph/types/user_type.rb: -------------------------------------------------------------------------------- 1 | UserType = GraphQL::ObjectType.define do 2 | name 'User' 3 | description 'The application, non auth user type' 4 | field :id, !types.ID, 'The id of the user' 5 | field :public, !types.Boolean, 'Whether the user is public' 6 | field :name, !types.String, 'The name of the user' 7 | field :bio, types.String, 'The bio of the user' 8 | field :avatar, types.String, 'The avatar url of the user' 9 | field :created_at, types.String, 'When the user was created' 10 | end 11 | -------------------------------------------------------------------------------- /app/mailers/application_mailer.rb: -------------------------------------------------------------------------------- 1 | class ApplicationMailer < ActionMailer::Base 2 | default from: 'udacity-alumni@udacity.com' 3 | layout 'mailer' 4 | end 5 | -------------------------------------------------------------------------------- /app/mailers/new_user_signup_mailer.rb: -------------------------------------------------------------------------------- 1 | class NewUserSignupMailer < ApplicationMailer 2 | 3 | def welcome_email(user) 4 | @user = user 5 | mail(to: @user.email, subject: "Thank you for joining Udacity alumni network!" ) 6 | end 7 | 8 | end -------------------------------------------------------------------------------- /app/models/application_record.rb: -------------------------------------------------------------------------------- 1 | class ApplicationRecord < ActiveRecord::Base 2 | self.abstract_class = true 3 | end 4 | -------------------------------------------------------------------------------- /app/models/article.rb: -------------------------------------------------------------------------------- 1 | class Article < ApplicationRecord 2 | belongs_to :user 3 | before_create :create_slug 4 | has_and_belongs_to_many :tags 5 | accepts_nested_attributes_for :tags 6 | # The default status is Draft, so the order needs 7 | # to stay as it is for now. 8 | enum status: [:draft, :published, :archived] 9 | 10 | def create_slug 11 | self.slug = self.title.parameterize 12 | end 13 | 14 | def autosave_associated_records_for_tag 15 | new_tag = Tag.find_or_create_by(tag: tag.tag) 16 | self.tags << new_tag 17 | self.tags.save! 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /app/models/authentication_token.rb: -------------------------------------------------------------------------------- 1 | class AuthenticationToken < ApplicationRecord 2 | belongs_to :user 3 | has_secure_token :body 4 | end 5 | -------------------------------------------------------------------------------- /app/models/concerns/user_concerns.rb: -------------------------------------------------------------------------------- 1 | module UserConcerns 2 | extend ActiveSupport::Concern 3 | 4 | module ClassMethods 5 | def get_user_from_token(token) 6 | auth_token = AuthenticationToken.find_by(body: token) 7 | if auth_token && auth_token.user 8 | auth_token.user 9 | end 10 | end 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /app/models/feedback.rb: -------------------------------------------------------------------------------- 1 | class Feedback < ApplicationRecord 2 | belongs_to :user 3 | end 4 | -------------------------------------------------------------------------------- /app/models/spotlight_image.rb: -------------------------------------------------------------------------------- 1 | class SpotlightImage < ApplicationRecord 2 | validates :url, presence: true 3 | # Each spotlight_image will belong to a user 4 | belongs_to :user 5 | end 6 | -------------------------------------------------------------------------------- /app/models/tag.rb: -------------------------------------------------------------------------------- 1 | class Tag < ApplicationRecord 2 | has_and_belongs_to_many :articles 3 | validates :tag, presence: true, uniqueness: { case_sensitive: false } 4 | 5 | def slug 6 | tag.parameterize 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /app/models/user.rb: -------------------------------------------------------------------------------- 1 | class User < ApplicationRecord 2 | before_create :generate_auth_token! 3 | include UserConcerns 4 | 5 | # Include default devise modules. Others available are: 6 | # :confirmable, :lockable, :timeoutable and :omniauthable 7 | devise :database_authenticatable, :registerable, 8 | :recoverable, :rememberable, :trackable, :validatable 9 | 10 | has_many :articles 11 | enum role: [:user, :author, :admin] 12 | 13 | before_save { self.email = email.downcase } 14 | after_initialize :set_default_role 15 | after_create :send_welcome_email 16 | 17 | validates :name, presence: true, length: { maximum: 50 } 18 | VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i 19 | validates :email, presence: true, length: { maximum: 255 }, 20 | format: { with: VALID_EMAIL_REGEX }, 21 | uniqueness: { case_sensitive: false } 22 | # Admin User will have => many spotlight images 23 | has_many :spotlight_images 24 | has_many :feedbacks 25 | alias_attribute :feedback, :feedbacks 26 | 27 | has_many :authentication_tokens 28 | 29 | def generate_auth_token! 30 | self.authentication_tokens << AuthenticationToken.create! 31 | end 32 | 33 | private 34 | 35 | def set_default_role 36 | self.role ||= :user if self.new_record? 37 | end 38 | 39 | def send_welcome_email 40 | NewUserSignupMailer.welcome_email(self).deliver_now 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /app/policies/application_policy.rb: -------------------------------------------------------------------------------- 1 | class ApplicationPolicy 2 | attr_reader :user, :record 3 | 4 | def initialize(user, record) 5 | @user = user 6 | @record = record 7 | end 8 | 9 | def index? 10 | false 11 | end 12 | 13 | def show? 14 | scope.where(:id => record.id).exists? 15 | end 16 | 17 | def create? 18 | false 19 | end 20 | 21 | def new? 22 | create? 23 | end 24 | 25 | def update? 26 | false 27 | end 28 | 29 | def edit? 30 | update? 31 | end 32 | 33 | def destroy? 34 | false 35 | end 36 | 37 | def scope 38 | Pundit.policy_scope!(user, record.class) 39 | end 40 | 41 | class Scope 42 | attr_reader :user, :scope 43 | 44 | def initialize(user, scope) 45 | @user = user 46 | @scope = scope 47 | end 48 | 49 | def resolve 50 | scope 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /app/policies/article_policy.rb: -------------------------------------------------------------------------------- 1 | class ArticlePolicy < ApplicationPolicy 2 | attr_reader :user, :article 3 | 4 | def initialize(current_user, article) 5 | if current_user 6 | @user = current_user 7 | elsif article.class == Article 8 | @user = article.user 9 | end 10 | @article = article 11 | end 12 | 13 | def index? 14 | true 15 | end 16 | 17 | def show? 18 | true 19 | end 20 | 21 | def create? 22 | user.admin? || user.author? if user 23 | end 24 | 25 | def update? 26 | user.admin? || user.author? if user 27 | end 28 | 29 | def destroy? 30 | user.admin? || user.author? if user 31 | end 32 | 33 | end -------------------------------------------------------------------------------- /app/serializers/article_serializer.rb: -------------------------------------------------------------------------------- 1 | class ArticleSerializer < ActiveModel::Serializer 2 | attributes :id, :title, :featured, 3 | :spotlighted, :content, :feature_image, 4 | :status, :slug, :user, 5 | :created_at, :updated_at 6 | has_many :tags 7 | end 8 | -------------------------------------------------------------------------------- /app/serializers/session_serializer.rb: -------------------------------------------------------------------------------- 1 | class SessionSerializer < ActiveModel::Serializer 2 | attributes :auth_token 3 | type 'session' 4 | end 5 | -------------------------------------------------------------------------------- /app/serializers/spotlight_image_serializer.rb: -------------------------------------------------------------------------------- 1 | class SpotlightImageSerializer < ActiveModel::Serializer 2 | attributes :id, :url 3 | type 'spotlight_images' 4 | end 5 | -------------------------------------------------------------------------------- /app/serializers/tag_serializer.rb: -------------------------------------------------------------------------------- 1 | class TagSerializer < ActiveModel::Serializer 2 | attributes :id, :tag 3 | has_many :articles 4 | end 5 | -------------------------------------------------------------------------------- /app/serializers/user_serializer.rb: -------------------------------------------------------------------------------- 1 | class UserSerializer < ActiveModel::Serializer 2 | attributes :id, :name, :bio, :email, :avatar, :role, :public 3 | type 'user' 4 | end 5 | -------------------------------------------------------------------------------- /app/services/token_issuer.rb: -------------------------------------------------------------------------------- 1 | class TokenIssuer 2 | MAXIMUM_TOKENS_PER_USER = 20 3 | 4 | def self.build 5 | new(MAXIMUM_TOKENS_PER_USER) 6 | end 7 | 8 | def self.create_and_return_token(resource, request) 9 | build.create_and_return_token(resource, request) 10 | end 11 | 12 | def self.expire_token(resource, request) 13 | build.expire_token(resource, request) 14 | end 15 | 16 | def self.purge_old_tokens(resource) 17 | build.purge_old_tokens(resource) 18 | end 19 | 20 | def initialize(maximum_tokens_per_user) 21 | self.maximum_tokens_per_user = maximum_tokens_per_user 22 | end 23 | 24 | def create_and_return_token(resource, request) 25 | token = resource.authentication_tokens.create!( 26 | last_used_at: DateTime.current, 27 | ip_address: request.remote_ip, 28 | user_agent: request.user_agent) 29 | 30 | token.body 31 | end 32 | 33 | def expire_token(resource, request) 34 | find_token(resource, request.headers["Authorization"]).try(:destroy) 35 | end 36 | 37 | def find_token(resource, token_from_headers) 38 | resource.authentication_tokens.detect do |token| 39 | token.body == token_from_headers 40 | end 41 | end 42 | 43 | def purge_old_tokens(resource) 44 | resource.authentication_tokens 45 | .order(last_used_at: :desc) 46 | .offset(maximum_tokens_per_user) 47 | .destroy_all 48 | end 49 | 50 | private 51 | 52 | attr_accessor :maximum_tokens_per_user 53 | 54 | end -------------------------------------------------------------------------------- /app/views/layouts/mailer.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 11 | <%= yield %> 12 | 13 | 14 | -------------------------------------------------------------------------------- /app/views/layouts/mailer.text.erb: -------------------------------------------------------------------------------- 1 | <%= yield %> 2 | -------------------------------------------------------------------------------- /app/views/new_user_signup_mailer/welcome_email.html.erb: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |

Welcome to Udacity Alumni network, <%= @user.name %>!

9 |

Thanks for joining and have a great day! Now sign in and do 10 | awesome things!

11 | 12 | -------------------------------------------------------------------------------- /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 => e 5 | raise unless e.message.include?('spring') 6 | end 7 | APP_PATH = File.expand_path('../config/application', __dir__) 8 | require_relative '../config/boot' 9 | require 'rails/commands' 10 | -------------------------------------------------------------------------------- /bin/rake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | begin 3 | load File.expand_path('../spring', __FILE__) 4 | rescue LoadError => e 5 | raise unless e.message.include?('spring') 6 | end 7 | require_relative '../config/boot' 8 | require 'rake' 9 | Rake.application.run 10 | -------------------------------------------------------------------------------- /bin/rspec: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | # frozen_string_literal: true 3 | # 4 | # This file was generated by Bundler. 5 | # 6 | # The application 'rspec' is installed as part of a gem, and 7 | # this file is here to facilitate running it. 8 | # 9 | 10 | require "pathname" 11 | ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../../Gemfile", 12 | Pathname.new(__FILE__).realpath) 13 | 14 | require "rubygems" 15 | require "bundler/setup" 16 | 17 | load Gem.bin_path("rspec-core", "rspec") 18 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a starting point to setup your application. 15 | # Add necessary setup steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | # puts "\n== Copying sample files ==" 22 | # unless File.exist?('config/database.yml') 23 | # cp 'config/database.yml.sample', 'config/database.yml' 24 | # end 25 | 26 | puts "\n== Preparing database ==" 27 | system! 'bin/rails db:setup' 28 | 29 | puts "\n== Removing old logs and tempfiles ==" 30 | system! 'bin/rails log:clear tmp:clear' 31 | 32 | puts "\n== Restarting application server ==" 33 | system! 'bin/rails restart' 34 | end 35 | -------------------------------------------------------------------------------- /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 | Gem.paths = { 'GEM_PATH' => [Bundler.bundle_path.to_s, *Gem.path].uniq.join(Gem.path_separator) } 12 | gem 'spring', match[1] 13 | require 'spring/binstub' 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /bin/update: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pathname' 3 | require 'fileutils' 4 | include FileUtils 5 | 6 | # path to your application root. 7 | APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) 8 | 9 | def system!(*args) 10 | system(*args) || abort("\n== Command #{args} failed ==") 11 | end 12 | 13 | chdir APP_ROOT do 14 | # This script is a way to update your development environment automatically. 15 | # Add necessary update steps to this file. 16 | 17 | puts '== Installing dependencies ==' 18 | system! 'gem install bundler --conservative' 19 | system('bundle check') || system!('bundle install') 20 | 21 | puts "\n== Updating database ==" 22 | system! 'bin/rails db:migrate' 23 | 24 | puts "\n== Removing old logs and tempfiles ==" 25 | system! 'bin/rails log:clear tmp:clear' 26 | 27 | puts "\n== Restarting application server ==" 28 | system! 'bin/rails restart' 29 | end 30 | -------------------------------------------------------------------------------- /config.ru: -------------------------------------------------------------------------------- 1 | # This file is used by Rack-based servers to start the application. 2 | 3 | require_relative 'config/environment' 4 | 5 | run Rails.application 6 | -------------------------------------------------------------------------------- /config/application.rb: -------------------------------------------------------------------------------- 1 | require_relative 'boot' 2 | 3 | require "rails" 4 | # Pick the frameworks you want: 5 | require "active_model/railtie" 6 | require "active_job/railtie" 7 | require "active_record/railtie" 8 | require "action_controller/railtie" 9 | require "action_mailer/railtie" 10 | require "action_view/railtie" 11 | require "action_cable/engine" 12 | require "sprockets/railtie" 13 | require "rails/test_unit/railtie" 14 | 15 | # Require the gems listed in Gemfile, including any gems 16 | # you've limited to :test, :development, or :production. 17 | Bundler.require(*Rails.groups) 18 | 19 | module Alumniapi 20 | class Application < Rails::Application 21 | config.autoload_paths << Rails.root.join('app/graph') 22 | config.autoload_paths << Rails.root.join('app/graph/types') 23 | config.autoload_paths << Rails.root.join('app/graph/mutations') 24 | config.autoload_paths << Rails.root.join('app/graph/queries') 25 | config.autoload_paths += %W(#{config.root}/app/services/**/) 26 | config.api_only = false 27 | config.middleware.insert_before 0, Rack::Cors do 28 | allow do 29 | origins '*' 30 | resource '*', headers: :any, methods: [:get, :post, :options] 31 | end 32 | end 33 | ActiveModelSerializers.config.adapter = :json 34 | 35 | # settings to generate RSpec files in spec and factories (spec/factories) 36 | config.generators do |g| 37 | g.test_framework :rspec, 38 | fixtures: true, 39 | view_specs: false, 40 | helper_specs: false, 41 | routing_specs: false, 42 | controller_specs: true, 43 | request_specs: false 44 | g.fixture_replacement :factory_girl, dir: "spec/factories" 45 | end 46 | 47 | # prepare Devise to accept json requests 48 | config.to_prepare do 49 | DeviseController.respond_to :html, :json 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /config/boot.rb: -------------------------------------------------------------------------------- 1 | ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) 2 | 3 | require 'bundler/setup' # Set up gems listed in the Gemfile. 4 | -------------------------------------------------------------------------------- /config/cable.yml: -------------------------------------------------------------------------------- 1 | development: 2 | adapter: async 3 | 4 | test: 5 | adapter: async 6 | 7 | production: 8 | adapter: redis 9 | url: redis://localhost:6379/1 10 | -------------------------------------------------------------------------------- /config/database.yml: -------------------------------------------------------------------------------- 1 | default: &default 2 | adapter: postgresql 3 | encoding: unicode 4 | pool: 5 5 | timeout: 5000 6 | username: postgres 7 | # please see the update below about using hostnames to 8 | # access linked services via docker-compose 9 | host: <%= ENV['POSTGRES_PORT_5432_TCP_ADDR'] %> 10 | port: <%= ENV['POSTGRES_PORT_5432_TCP_PORT'] %> 11 | 12 | development: 13 | <<: *default 14 | database: app_development 15 | 16 | # Warning: The database defined as "test" will be erased and 17 | # re-generated from your development database when you run 18 | # "rake". Do not set this db to the same as development or 19 | # production. 20 | test: 21 | <<: *default 22 | database: app_test 23 | -------------------------------------------------------------------------------- /config/environment.rb: -------------------------------------------------------------------------------- 1 | # Load the Rails application. 2 | require_relative 'application' 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. 13 | config.consider_all_requests_local = true 14 | 15 | # Enable/disable caching. By default caching is disabled. 16 | if Rails.root.join('tmp/caching-dev.txt').exist? 17 | config.action_controller.perform_caching = true 18 | 19 | config.cache_store = :memory_store 20 | config.public_file_server.headers = { 21 | 'Cache-Control' => 'public, max-age=172800' 22 | } 23 | else 24 | config.action_controller.perform_caching = false 25 | 26 | config.cache_store = :null_store 27 | end 28 | 29 | config.action_mailer.perform_caching = false 30 | 31 | # Print deprecation notices to the Rails logger. 32 | config.active_support.deprecation = :log 33 | 34 | # Raise an error on page load if there are pending migrations. 35 | config.active_record.migration_error = :page_load 36 | 37 | # Raises error for missing translations 38 | # config.action_view.raise_on_missing_translations = true 39 | 40 | # Use an evented file watcher to asynchronously detect changes in source code, 41 | # routes, locales, etc. This feature depends on the listen gem. 42 | config.file_watcher = ActiveSupport::EventedFileUpdateChecker 43 | 44 | # ActionMailer config 45 | config.action_mailer.raise_delivery_errors = false 46 | config.action_mailer.default_url_options = { host: 'localhost:3000' } 47 | config.action_mailer.delivery_method = :smtp 48 | ActionMailer::Base.smtp_settings = { 49 | user_name: ENV['SENDGRID_USERNAME'], 50 | password: ENV['SENDGRID_PASSWORD'], 51 | domain: 'heroku.com', 52 | address: 'smtp.sendgrid.net', 53 | port: 587, 54 | authentication: :plain, 55 | enable_starttls_auto: true 56 | } 57 | 58 | end 59 | -------------------------------------------------------------------------------- /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 | # Disable serving static files from the `/public` folder by default since 18 | # Apache or NGINX already handles this. 19 | config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? 20 | 21 | 22 | # Enable serving of images, stylesheets, and JavaScripts from an asset server. 23 | # config.action_controller.asset_host = 'http://assets.example.com' 24 | 25 | # Specifies the header that your server uses for sending files. 26 | # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache 27 | # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX 28 | 29 | # Mount Action Cable outside main process or domain 30 | # config.action_cable.mount_path = nil 31 | # config.action_cable.url = 'wss://example.com/cable' 32 | # config.action_cable.allowed_request_origins = [ 'http://example.com', /http:\/\/example.*/ ] 33 | 34 | # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. 35 | # config.force_ssl = true 36 | 37 | # Use the lowest log level to ensure availability of diagnostic information 38 | # when problems arise. 39 | config.log_level = :debug 40 | 41 | # Prepend all log lines with the following tags. 42 | config.log_tags = [ :request_id ] 43 | 44 | # Use a different cache store in production. 45 | # config.cache_store = :mem_cache_store 46 | 47 | # Use a real queuing backend for Active Job (and separate queues per environment) 48 | # config.active_job.queue_adapter = :resque 49 | # config.active_job.queue_name_prefix = "alumniapi_#{Rails.env}" 50 | config.action_mailer.perform_caching = false 51 | 52 | # Ignore bad email addresses and do not raise email delivery errors. 53 | # Set this to true and configure the email server for immediate delivery to raise delivery errors. 54 | # config.action_mailer.raise_delivery_errors = false 55 | 56 | # Enable locale fallbacks for I18n (makes lookups for any locale fall back to 57 | # the I18n.default_locale when a translation cannot be found). 58 | config.i18n.fallbacks = true 59 | 60 | # Send deprecation notices to registered listeners. 61 | config.active_support.deprecation = :notify 62 | 63 | # Use default logging formatter so that PID and timestamp are not suppressed. 64 | config.log_formatter = ::Logger::Formatter.new 65 | 66 | # Use a different logger for distributed setups. 67 | # require 'syslog/logger' 68 | # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') 69 | 70 | if ENV["RAILS_LOG_TO_STDOUT"].present? 71 | logger = ActiveSupport::Logger.new(STDOUT) 72 | logger.formatter = config.log_formatter 73 | config.logger = ActiveSupport::TaggedLogging.new(logger) 74 | end 75 | 76 | # Do not dump schema after migrations. 77 | config.active_record.dump_schema_after_migration = false 78 | 79 | # ActionMailer config 80 | config.action_mailer.default_url_options = { host: 'http://udacity-client.herokuapp.com' } 81 | config.action_mailer.delivery_method = :smtp 82 | ActionMailer::Base.smtp_settings = { 83 | user_name: ENV['SENDGRID_USERNAME'], 84 | password: ENV['SENDGRID_PASSWORD'], 85 | domain: 'heroku.com', 86 | address: 'smtp.sendgrid.net', 87 | port: 587, 88 | authentication: :plain, 89 | enable_starttls_auto: true 90 | } 91 | 92 | end 93 | -------------------------------------------------------------------------------- /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 public file server for tests with Cache-Control for performance. 16 | config.public_file_server.enabled = true 17 | config.public_file_server.headers = { 18 | 'Cache-Control' => 'public, max-age=3600' 19 | } 20 | 21 | # Show full error reports and disable caching. 22 | config.consider_all_requests_local = true 23 | config.action_controller.perform_caching = false 24 | 25 | # Raise exceptions instead of rendering exception templates. 26 | config.action_dispatch.show_exceptions = false 27 | 28 | # Disable request forgery protection in test environment. 29 | config.action_controller.allow_forgery_protection = false 30 | config.action_mailer.perform_caching = false 31 | 32 | # Tell Action Mailer not to deliver emails to the real world. 33 | # The :test delivery method accumulates sent emails in the 34 | # ActionMailer::Base.deliveries array. 35 | config.action_mailer.delivery_method = :test 36 | 37 | # Print deprecation notices to the stderr. 38 | config.active_support.deprecation = :stderr 39 | 40 | # Raises error for missing translations 41 | # config.action_view.raise_on_missing_translations = true 42 | end 43 | -------------------------------------------------------------------------------- /config/initializers/application_controller_renderer.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # ApplicationController.renderer.defaults.merge!( 4 | # http_host: 'example.org', 5 | # https: false 6 | # ) 7 | -------------------------------------------------------------------------------- /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/cors.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | 3 | # Avoid CORS issues when API is called from the frontend app. 4 | # Handle Cross-Origin Resource Sharing (CORS) in order to accept cross-origin AJAX requests. 5 | 6 | # Read more: https://github.com/cyu/rack-cors 7 | 8 | # Rails.application.config.middleware.insert_before 0, Rack::Cors do 9 | # allow do 10 | # origins 'example.com' 11 | # 12 | # resource '*', 13 | # headers: :any, 14 | # methods: [:get, :post, :put, :patch, :delete, :options, :head] 15 | # end 16 | # end 17 | -------------------------------------------------------------------------------- /config/initializers/devise.rb: -------------------------------------------------------------------------------- 1 | # Use this hook to configure devise mailer, warden hooks and so forth. 2 | # Many of these configuration options can be set straight in your model. 3 | Devise.setup do |config| 4 | # The secret key used by Devise. Devise uses this key to generate 5 | # random tokens. Changing this key will render invalid all existing 6 | # confirmation, reset password and unlock tokens in the database. 7 | # Devise will use the `secret_key_base` as its `secret_key` 8 | # by default. You can change it below and use your own secret key. 9 | # config.secret_key = 'f5ed794c47cf9388ec7fa97b0147f6eaa51097f0c8dc35fc2d3627032c058187706c148d1138fbb4855d1bb0fd9917a79428e46120a3465b23d3c61ce3c25c4b' 10 | 11 | # ==> Mailer Configuration 12 | # Configure the e-mail address which will be shown in Devise::Mailer, 13 | # note that it will be overwritten if you use your own mailer class 14 | # with default "from" parameter. 15 | config.mailer_sender = 'please-change-me-at-config-initializers-devise@example.com' 16 | 17 | # Configure the class responsible to send e-mails. 18 | # config.mailer = 'Devise::Mailer' 19 | 20 | # Configure the parent class responsible to send e-mails. 21 | # config.parent_mailer = 'ActionMailer::Base' 22 | 23 | # ==> ORM configuration 24 | # Load and configure the ORM. Supports :active_record (default) and 25 | # :mongoid (bson_ext recommended) by default. Other ORMs may be 26 | # available as additional gems. 27 | require 'devise/orm/active_record' 28 | 29 | # ==> Configuration for any authentication mechanism 30 | # Configure which keys are used when authenticating a user. The default is 31 | # just :email. You can configure it to use [:username, :subdomain], so for 32 | # authenticating a user, both parameters are required. Remember that those 33 | # parameters are used only when authenticating and not when retrieving from 34 | # session. If you need permissions, you should implement that in a before filter. 35 | # You can also supply a hash where the value is a boolean determining whether 36 | # or not authentication should be aborted when the value is not present. 37 | # config.authentication_keys = [:email] 38 | 39 | # Configure parameters from the request object used for authentication. Each entry 40 | # given should be a request method and it will automatically be passed to the 41 | # find_for_authentication method and considered in your model lookup. For instance, 42 | # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. 43 | # The same considerations mentioned for authentication_keys also apply to request_keys. 44 | # config.request_keys = [] 45 | 46 | # Configure which authentication keys should be case-insensitive. 47 | # These keys will be downcased upon creating or modifying a user and when used 48 | # to authenticate or find a user. Default is :email. 49 | config.case_insensitive_keys = [:email] 50 | 51 | # Configure which authentication keys should have whitespace stripped. 52 | # These keys will have whitespace before and after removed upon creating or 53 | # modifying a user and when used to authenticate or find a user. Default is :email. 54 | config.strip_whitespace_keys = [:email] 55 | 56 | # Tell if authentication through request.params is enabled. True by default. 57 | # It can be set to an array that will enable params authentication only for the 58 | # given strategies, for example, `config.params_authenticatable = [:database]` will 59 | # enable it only for database (email + password) authentication. 60 | # config.params_authenticatable = true 61 | 62 | # Tell if authentication through HTTP Auth is enabled. False by default. 63 | # It can be set to an array that will enable http authentication only for the 64 | # given strategies, for example, `config.http_authenticatable = [:database]` will 65 | # enable it only for database authentication. The supported strategies are: 66 | # :database = Support basic authentication with authentication key + password 67 | # config.http_authenticatable = false 68 | 69 | # If 401 status code should be returned for AJAX requests. True by default. 70 | # config.http_authenticatable_on_xhr = true 71 | 72 | # The realm used in Http Basic Authentication. 'Application' by default. 73 | # config.http_authentication_realm = 'Application' 74 | 75 | # It will change confirmation, password recovery and other workflows 76 | # to behave the same regardless if the e-mail provided was right or wrong. 77 | # Does not affect registerable. 78 | # config.paranoid = true 79 | 80 | # By default Devise will store the user in session. You can skip storage for 81 | # particular strategies by setting this option. 82 | # Notice that if you are skipping storage for all authentication paths, you 83 | # may want to disable generating routes to Devise's sessions controller by 84 | # passing skip: :sessions to `devise_for` in your config/routes.rb 85 | config.skip_session_storage = [:http_auth] 86 | 87 | # By default, Devise cleans up the CSRF token on authentication to 88 | # avoid CSRF token fixation attacks. This means that, when using AJAX 89 | # requests for sign in and sign up, you need to get a new CSRF token 90 | # from the server. You can disable this option at your own risk. 91 | # config.clean_up_csrf_token_on_authentication = true 92 | 93 | # When false, Devise will not attempt to reload routes on eager load. 94 | # This can reduce the time taken to boot the app but if your application 95 | # requires the Devise mappings to be loaded during boot time the application 96 | # won't boot properly. 97 | # config.reload_routes = true 98 | 99 | # ==> Configuration for :database_authenticatable 100 | # For bcrypt, this is the cost for hashing the password and defaults to 11. If 101 | # using other algorithms, it sets how many times you want the password to be hashed. 102 | # 103 | # Limiting the stretches to just one in testing will increase the performance of 104 | # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use 105 | # a value less than 10 in other environments. Note that, for bcrypt (the default 106 | # algorithm), the cost increases exponentially with the number of stretches (e.g. 107 | # a value of 20 is already extremely slow: approx. 60 seconds for 1 calculation). 108 | config.stretches = Rails.env.test? ? 1 : 11 109 | 110 | # Set up a pepper to generate the hashed password. 111 | # config.pepper = 'cbe6c6d2d44bb7b319127df4002eb2c3d09be5a0025c26ce87bcc1a0e95b9e6840bddf8605409a1ffbad054343e0cb04bab1b002a6d13f3fa46f7c119520bacb' 112 | 113 | # Send a notification email when the user's password is changed 114 | # config.send_password_change_notification = false 115 | 116 | # ==> Configuration for :confirmable 117 | # A period that the user is allowed to access the website even without 118 | # confirming their account. For instance, if set to 2.days, the user will be 119 | # able to access the website for two days without confirming their account, 120 | # access will be blocked just in the third day. Default is 0.days, meaning 121 | # the user cannot access the website without confirming their account. 122 | # config.allow_unconfirmed_access_for = 2.days 123 | 124 | # A period that the user is allowed to confirm their account before their 125 | # token becomes invalid. For example, if set to 3.days, the user can confirm 126 | # their account within 3 days after the mail was sent, but on the fourth day 127 | # their account can't be confirmed with the token any more. 128 | # Default is nil, meaning there is no restriction on how long a user can take 129 | # before confirming their account. 130 | # config.confirm_within = 3.days 131 | 132 | # If true, requires any email changes to be confirmed (exactly the same way as 133 | # initial account confirmation) to be applied. Requires additional unconfirmed_email 134 | # db field (see migrations). Until confirmed, new email is stored in 135 | # unconfirmed_email column, and copied to email column on successful confirmation. 136 | config.reconfirmable = true 137 | 138 | # Defines which key will be used when confirming an account 139 | # config.confirmation_keys = [:email] 140 | 141 | # ==> Configuration for :rememberable 142 | # The time the user will be remembered without asking for credentials again. 143 | # config.remember_for = 2.weeks 144 | 145 | # Invalidates all the remember me tokens when the user signs out. 146 | config.expire_all_remember_me_on_sign_out = true 147 | 148 | # If true, extends the user's remember period when remembered via cookie. 149 | # config.extend_remember_period = false 150 | 151 | # Options to be passed to the created cookie. For instance, you can set 152 | # secure: true in order to force SSL only cookies. 153 | # config.rememberable_options = {} 154 | 155 | # ==> Configuration for :validatable 156 | # Range for password length. 157 | config.password_length = 6..128 158 | 159 | # Email regex used to validate email formats. It simply asserts that 160 | # one (and only one) @ exists in the given string. This is mainly 161 | # to give user feedback and not to assert the e-mail validity. 162 | config.email_regexp = /\A[^@\s]+@[^@\s]+\z/ 163 | 164 | # ==> Configuration for :timeoutable 165 | # The time you want to timeout the user session without activity. After this 166 | # time the user will be asked for credentials again. Default is 30 minutes. 167 | # config.timeout_in = 30.minutes 168 | 169 | # ==> Configuration for :lockable 170 | # Defines which strategy will be used to lock an account. 171 | # :failed_attempts = Locks an account after a number of failed attempts to sign in. 172 | # :none = No lock strategy. You should handle locking by yourself. 173 | # config.lock_strategy = :failed_attempts 174 | 175 | # Defines which key will be used when locking and unlocking an account 176 | # config.unlock_keys = [:email] 177 | 178 | # Defines which strategy will be used to unlock an account. 179 | # :email = Sends an unlock link to the user email 180 | # :time = Re-enables login after a certain amount of time (see :unlock_in below) 181 | # :both = Enables both strategies 182 | # :none = No unlock strategy. You should handle unlocking by yourself. 183 | # config.unlock_strategy = :both 184 | 185 | # Number of authentication tries before locking an account if lock_strategy 186 | # is failed attempts. 187 | # config.maximum_attempts = 20 188 | 189 | # Time interval to unlock the account if :time is enabled as unlock_strategy. 190 | # config.unlock_in = 1.hour 191 | 192 | # Warn on the last attempt before the account is locked. 193 | # config.last_attempt_warning = true 194 | 195 | # ==> Configuration for :recoverable 196 | # 197 | # Defines which key will be used when recovering the password for an account 198 | # config.reset_password_keys = [:email] 199 | 200 | # Time interval you can reset your password with a reset password key. 201 | # Don't put a too small interval or your users won't have the time to 202 | # change their passwords. 203 | config.reset_password_within = 6.hours 204 | 205 | # When set to false, does not sign a user in automatically after their password is 206 | # reset. Defaults to true, so a user is signed in automatically after a reset. 207 | # config.sign_in_after_reset_password = true 208 | 209 | # ==> Configuration for :encryptable 210 | # Allow you to use another hashing or encryption algorithm besides bcrypt (default). 211 | # You can use :sha1, :sha512 or algorithms from others authentication tools as 212 | # :clearance_sha1, :authlogic_sha512 (then you should set stretches above to 20 213 | # for default behavior) and :restful_authentication_sha1 (then you should set 214 | # stretches to 10, and copy REST_AUTH_SITE_KEY to pepper). 215 | # 216 | # Require the `devise-encryptable` gem when using anything other than bcrypt 217 | # config.encryptor = :sha512 218 | 219 | # ==> Scopes configuration 220 | # Turn scoped views on. Before rendering "sessions/new", it will first check for 221 | # "users/sessions/new". It's turned off by default because it's slower if you 222 | # are using only default views. 223 | # config.scoped_views = false 224 | 225 | # Configure the default scope given to Warden. By default it's the first 226 | # devise role declared in your routes (usually :user). 227 | # config.default_scope = :user 228 | 229 | # Set this configuration to false if you want /users/sign_out to sign out 230 | # only the current scope. By default, Devise signs out all scopes. 231 | # config.sign_out_all_scopes = true 232 | 233 | # ==> Navigation configuration 234 | # Lists the formats that should be treated as navigational. Formats like 235 | # :html, should redirect to the sign in page when the user does not have 236 | # access, but formats like :xml or :json, should return 401. 237 | # 238 | # If you have any extra navigational formats, like :iphone or :mobile, you 239 | # should add them to the navigational formats lists. 240 | # 241 | # The "*/*" below is required to match Internet Explorer requests. 242 | # config.navigational_formats = ['*/*', :html] 243 | 244 | # The default HTTP method used to sign out a resource. Default is :delete. 245 | config.sign_out_via = :delete 246 | 247 | # ==> OmniAuth 248 | # Add a new OmniAuth provider. Check the wiki for more information on setting 249 | # up on your models and hooks. 250 | # config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo' 251 | 252 | # ==> Warden configuration 253 | # If you want to use other strategies, that are not supported by Devise, or 254 | # change the failure app, you can configure them inside the config.warden block. 255 | # 256 | # config.warden do |manager| 257 | # manager.intercept_401 = false 258 | # manager.default_strategies(scope: :user).unshift :some_external_strategy 259 | # end 260 | 261 | # ==> Mountable engine configurations 262 | # When using Devise inside an engine, let's call it `MyEngine`, and this engine 263 | # is mountable, there are some extra configurations to be taken into account. 264 | # The following options are available, assuming the engine is mounted as: 265 | # 266 | # mount MyEngine, at: '/my_engine' 267 | # 268 | # The router that invoked `devise_for`, in the example above, would be: 269 | # config.router_name = :my_engine 270 | # 271 | # When using OmniAuth, Devise cannot automatically set OmniAuth path, 272 | # so you need to do it manually. For the users scope, it would be: 273 | # config.omniauth_path_prefix = '/my_engine/users/auth' 274 | end 275 | -------------------------------------------------------------------------------- /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/new_framework_defaults.rb: -------------------------------------------------------------------------------- 1 | # Be sure to restart your server when you modify this file. 2 | # 3 | # This file contains migration options to ease your Rails 5.0 upgrade. 4 | # 5 | # Read the Rails 5.0 release notes for more info on each option. 6 | 7 | # Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. 8 | # Previous versions had false. 9 | ActiveSupport.to_time_preserves_timezone = true 10 | 11 | # Require `belongs_to` associations by default. Previous versions had false. 12 | Rails.application.config.active_record.belongs_to_required_by_default = true 13 | 14 | # Do not halt callback chains when a callback returns false. Previous versions had true. 15 | ActiveSupport.halt_callback_chains_on_return_false = false 16 | 17 | # Configure SSL options to enable HSTS with subdomains. Previous versions had false. 18 | Rails.application.config.ssl_options = { hsts: { subdomains: true } } 19 | -------------------------------------------------------------------------------- /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] 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/devise.en.yml: -------------------------------------------------------------------------------- 1 | # Additional translations at https://github.com/plataformatec/devise/wiki/I18n 2 | 3 | en: 4 | devise: 5 | confirmations: 6 | confirmed: "Your email address has been successfully confirmed." 7 | send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes." 8 | send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes." 9 | failure: 10 | already_authenticated: "You are already signed in." 11 | inactive: "Your account is not activated yet." 12 | invalid: "Invalid %{authentication_keys} or password." 13 | locked: "Your account is locked." 14 | last_attempt: "You have one more attempt before your account is locked." 15 | not_found_in_database: "Invalid %{authentication_keys} or password." 16 | timeout: "Your session expired. Please sign in again to continue." 17 | unauthenticated: "You need to sign in or sign up before continuing." 18 | unconfirmed: "You have to confirm your email address before continuing." 19 | mailer: 20 | confirmation_instructions: 21 | subject: "Confirmation instructions" 22 | reset_password_instructions: 23 | subject: "Reset password instructions" 24 | unlock_instructions: 25 | subject: "Unlock instructions" 26 | password_change: 27 | subject: "Password Changed" 28 | omniauth_callbacks: 29 | failure: "Could not authenticate you from %{kind} because \"%{reason}\"." 30 | success: "Successfully authenticated from %{kind} account." 31 | passwords: 32 | no_token: "You can't access this page without coming from a password reset email. If you do come from a password reset email, please make sure you used the full URL provided." 33 | send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes." 34 | send_paranoid_instructions: "If your email address exists in our database, you will receive a password recovery link at your email address in a few minutes." 35 | updated: "Your password has been changed successfully. You are now signed in." 36 | updated_not_active: "Your password has been changed successfully." 37 | registrations: 38 | destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon." 39 | signed_up: "Welcome! You have signed up successfully." 40 | signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated." 41 | signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked." 42 | signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account." 43 | update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address." 44 | updated: "Your account has been updated successfully." 45 | sessions: 46 | signed_in: "Signed in successfully." 47 | signed_out: "Signed out successfully." 48 | already_signed_out: "Signed out successfully." 49 | unlocks: 50 | send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes." 51 | send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes." 52 | unlocked: "Your account has been unlocked successfully. Please sign in to continue." 53 | errors: 54 | messages: 55 | already_confirmed: "was already confirmed, please try signing in" 56 | confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one" 57 | expired: "has expired, please request a new one" 58 | not_found: "not found" 59 | not_locked: "was not locked" 60 | not_saved: 61 | one: "1 error prohibited this %{resource} from being saved:" 62 | other: "%{count} errors prohibited this %{resource} from being saved:" 63 | -------------------------------------------------------------------------------- /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/pre_commit.yml: -------------------------------------------------------------------------------- 1 | --- 2 | :warnings_remove: 3 | - :jshint 4 | :warnings_add: 5 | - :rubocop 6 | -------------------------------------------------------------------------------- /config/puma.rb: -------------------------------------------------------------------------------- 1 | # Puma can serve each request in a thread from an internal thread pool. 2 | # The `threads` method setting takes two numbers a minimum and maximum. 3 | # Any libraries that use thread pools should be configured to match 4 | # the maximum value specified for Puma. Default is set to 5 threads for minimum 5 | # and maximum, this matches the default thread size of Active Record. 6 | # 7 | threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i 8 | threads threads_count, threads_count 9 | 10 | # Specifies the `port` that Puma will listen on to receive requests, default is 3000. 11 | # 12 | port ENV.fetch("PORT") { 3000 } 13 | 14 | # Specifies the `environment` that Puma will run in. 15 | # 16 | environment ENV.fetch("RAILS_ENV") { "development" } 17 | 18 | # Specifies the number of `workers` to boot in clustered mode. 19 | # Workers are forked webserver processes. If using threads and workers together 20 | # the concurrency of the application would be max `threads` * `workers`. 21 | # Workers do not work on JRuby or Windows (both of which do not support 22 | # processes). 23 | # 24 | # workers ENV.fetch("WEB_CONCURRENCY") { 2 } 25 | 26 | # Use the `preload_app!` method when specifying a `workers` number. 27 | # This directive tells Puma to first boot the application and load code 28 | # before forking the application. This takes advantage of Copy On Write 29 | # process behavior so workers use less memory. If you use this option 30 | # you need to make sure to reconnect any threads in the `on_worker_boot` 31 | # block. 32 | # 33 | # preload_app! 34 | 35 | # The code in the `on_worker_boot` will be called if you are using 36 | # clustered mode by specifying a number of `workers`. After each worker 37 | # process is booted this block will be run, if you are using `preload_app!` 38 | # option you will want to use this block to reconnect to any threads 39 | # or connections that may have been created at application boot, Ruby 40 | # cannot share connections between processes. 41 | # 42 | # on_worker_boot do 43 | # ActiveRecord::Base.establish_connection if defined?(ActiveRecord) 44 | # end 45 | 46 | # Allow puma to be restarted by `rails restart` command. 47 | plugin :tmp_restart 48 | -------------------------------------------------------------------------------- /config/rack_cors.rb: -------------------------------------------------------------------------------- 1 | Rails.application.config.middleware.insert_before 0, Rack::Cors do 2 | allow do 3 | origins '*' 4 | resource '*', headers: :any, methods: [:get, :post, :put, :delete, :options] 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | Rails.application.routes.draw do 2 | devise_for :users 3 | # Bootstrap graphql and the graphiql editor 4 | mount GraphiQL::Rails::Engine, at: '/graphiql', graphql_path: '/graphql' 5 | root to: redirect('/graphiql') 6 | resources :graphql 7 | 8 | namespace :api, defaults: { format: :json } do 9 | namespace :v1 do 10 | resources :articles, only: [:index, :show, :create, :update, :destroy] 11 | resources :tags, only: [:index, :show, :create, :update, :destroy] 12 | resource :users, only: [:show, :create, :update] 13 | resource :sessions, only: [:create, :destroy] 14 | resources :spotlight_images, only: [:index, :create, :destroy] 15 | 16 | scope :auth do 17 | get 'is_signed_in', to: 'auth#is_signed_in?' 18 | end 19 | end 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /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 `rails 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: 26ecb4220a12bc7236e6b8fff4184f56cfe272640042413b0b5f1091974af69bfef9627905e3ffa9a54ea62bad7d665fe081590371c54a71688af87f5260ee50 15 | 16 | test: 17 | secret_key_base: 81c2fd167da29b0cbd77a3218a05737b9cfa5dc589260e11e25ff933b42c72d7a277f1d30e1caccb265b505523f9f8576b81635c5f3efbac6cee11d62d9e1e8d 18 | 19 | # Do not keep production secrets in the repository, 20 | # instead read values from the environment. 21 | production: 22 | secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> 23 | -------------------------------------------------------------------------------- /config/spring.rb: -------------------------------------------------------------------------------- 1 | %w( 2 | .ruby-version 3 | .rbenv-vars 4 | tmp/restart.txt 5 | tmp/caching-dev.txt 6 | ).each { |path| Spring.watch(path) } 7 | -------------------------------------------------------------------------------- /db/migrate/20160926175003_devise_create_users.rb: -------------------------------------------------------------------------------- 1 | class DeviseCreateUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :users do |t| 4 | ## Database authenticatable 5 | t.string :email, null: false, default: "" 6 | t.string :encrypted_password, null: false, default: "" 7 | 8 | ## Recoverable 9 | t.string :reset_password_token 10 | t.datetime :reset_password_sent_at 11 | 12 | ## Rememberable 13 | t.datetime :remember_created_at 14 | 15 | ## Trackable 16 | t.integer :sign_in_count, default: 0, null: false 17 | t.datetime :current_sign_in_at 18 | t.datetime :last_sign_in_at 19 | t.inet :current_sign_in_ip 20 | t.inet :last_sign_in_ip 21 | 22 | ## Confirmable 23 | # t.string :confirmation_token 24 | # t.datetime :confirmed_at 25 | # t.datetime :confirmation_sent_at 26 | # t.string :unconfirmed_email # Only if using reconfirmable 27 | 28 | ## Lockable 29 | # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts 30 | # t.string :unlock_token # Only if unlock strategy is :email or :both 31 | # t.datetime :locked_at 32 | 33 | 34 | t.timestamps null: false 35 | end 36 | 37 | add_index :users, :email, unique: true 38 | add_index :users, :reset_password_token, unique: true 39 | # add_index :users, :confirmation_token, unique: true 40 | # add_index :users, :unlock_token, unique: true 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /db/migrate/20160927145825_add_attributes_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddAttributesToUser < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :name, :string, default: '', null: false 4 | add_column :users, :avatar, :string 5 | end 6 | end 7 | -------------------------------------------------------------------------------- /db/migrate/20160927154706_create_articles.rb: -------------------------------------------------------------------------------- 1 | class CreateArticles < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :articles do |t| 4 | t.string :title, null: false 5 | t.references :user 6 | t.boolean :featured, default: false 7 | t.boolean :spotlighted, default: false 8 | t.text :content, null: false 9 | t.string :feature_image 10 | 11 | t.timestamps 12 | end 13 | end 14 | end 15 | -------------------------------------------------------------------------------- /db/migrate/20160927183906_add_role_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddRoleToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :role, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160928191359_add_status_enum_to_article.rb: -------------------------------------------------------------------------------- 1 | class AddStatusEnumToArticle < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :articles, :status, :integer 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20160928191930_add_default_non_null_to_article_status.rb: -------------------------------------------------------------------------------- 1 | class AddDefaultNonNullToArticleStatus < ActiveRecord::Migration[5.0] 2 | def change 3 | change_column :articles, :status, :integer, null: false, default: 0 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161009215058_add_bio_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddBioToUser < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :bio, :text, default: '' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161009233632_add_auth_token_to_users.rb: -------------------------------------------------------------------------------- 1 | class AddAuthTokenToUsers < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :auth_token, :string, default: '' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161010033700_create_tags.rb: -------------------------------------------------------------------------------- 1 | class CreateTags < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :tags do |t| 4 | t.string :tag, null: false 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20161010160812_create_join_table_article_tag.rb: -------------------------------------------------------------------------------- 1 | class CreateJoinTableArticleTag < ActiveRecord::Migration[5.0] 2 | def change 3 | create_join_table :articles, :tags do |t| 4 | t.index [:article_id, :tag_id] 5 | t.index [:tag_id, :article_id] 6 | end 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /db/migrate/20161015190453_create_spotlight_images.rb: -------------------------------------------------------------------------------- 1 | class CreateSpotlightImages < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :spotlight_images do |t| 4 | t.string :url 5 | 6 | t.timestamps 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/20161016203229_add_user_to_spotlight_images.rb: -------------------------------------------------------------------------------- 1 | class AddUserToSpotlightImages < ActiveRecord::Migration[5.0] 2 | def change 3 | add_reference :spotlight_images, :user, foreign_key: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161017175954_add_json_to_article.rb: -------------------------------------------------------------------------------- 1 | class AddJsonToArticle < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :articles, :json, :jsonb 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161017203917_add_index_to_tags.rb: -------------------------------------------------------------------------------- 1 | class AddIndexToTags < ActiveRecord::Migration[5.0] 2 | def change 3 | add_index :tags, :tag, unique: true 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161022211436_add_public_to_user.rb: -------------------------------------------------------------------------------- 1 | class AddPublicToUser < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :users, :public, :boolean, default: false 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161023165045_add_slug_to_article.rb: -------------------------------------------------------------------------------- 1 | class AddSlugToArticle < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :articles, :slug, :string, default: '' 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161118011519_create_feedbacks.rb: -------------------------------------------------------------------------------- 1 | class CreateFeedbacks < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :feedbacks do |t| 4 | t.text :description 5 | t.string :url 6 | t.references :user, foreign_key: true 7 | 8 | t.timestamps 9 | end 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /db/migrate/20161202033652_create_authentication_tokens.rb: -------------------------------------------------------------------------------- 1 | class CreateAuthenticationTokens < ActiveRecord::Migration[5.0] 2 | def change 3 | create_table :authentication_tokens do |t| 4 | t.string :body 5 | t.references :user, foreign_key: true 6 | t.datetime :last_used_at 7 | t.string :ip_address 8 | t.string :user_agent 9 | 10 | t.timestamps 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /db/migrate/20161202100913_remove_auth_token_from_user.rb: -------------------------------------------------------------------------------- 1 | class RemoveAuthTokenFromUser < ActiveRecord::Migration[5.0] 2 | def change 3 | remove_column :users, :auth_token 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/migrate/20161211192821_add_image_to_feedback.rb: -------------------------------------------------------------------------------- 1 | class AddImageToFeedback < ActiveRecord::Migration[5.0] 2 | def change 3 | add_column :feedbacks, :image, :binary 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /db/schema.rb: -------------------------------------------------------------------------------- 1 | # This file is auto-generated from the current state of the database. Instead 2 | # of editing this file, please use the migrations feature of Active Record to 3 | # incrementally modify your database, and then regenerate this schema definition. 4 | # 5 | # Note that this schema.rb definition is the authoritative source for your 6 | # database schema. If you need to create the application database on another 7 | # system, you should be using db:schema:load, not running all the migrations 8 | # from scratch. The latter is a flawed and unsustainable approach (the more migrations 9 | # you'll amass, the slower it'll run and the greater likelihood for issues). 10 | # 11 | # It's strongly recommended that you check this file into your version control system. 12 | 13 | ActiveRecord::Schema.define(version: 20161211192821) do 14 | 15 | # These are extensions that must be enabled in order to support this database 16 | enable_extension "plpgsql" 17 | 18 | create_table "articles", force: :cascade do |t| 19 | t.string "title", null: false 20 | t.integer "user_id" 21 | t.boolean "featured", default: false 22 | t.boolean "spotlighted", default: false 23 | t.text "content", null: false 24 | t.string "feature_image" 25 | t.datetime "created_at", null: false 26 | t.datetime "updated_at", null: false 27 | t.integer "status", default: 0, null: false 28 | t.jsonb "json" 29 | t.string "slug", default: "" 30 | t.index ["user_id"], name: "index_articles_on_user_id", using: :btree 31 | end 32 | 33 | create_table "articles_tags", id: false, force: :cascade do |t| 34 | t.integer "article_id", null: false 35 | t.integer "tag_id", null: false 36 | t.index ["article_id", "tag_id"], name: "index_articles_tags_on_article_id_and_tag_id", using: :btree 37 | t.index ["tag_id", "article_id"], name: "index_articles_tags_on_tag_id_and_article_id", using: :btree 38 | end 39 | 40 | create_table "authentication_tokens", force: :cascade do |t| 41 | t.string "body" 42 | t.integer "user_id" 43 | t.datetime "last_used_at" 44 | t.string "ip_address" 45 | t.string "user_agent" 46 | t.datetime "created_at", null: false 47 | t.datetime "updated_at", null: false 48 | t.index ["user_id"], name: "index_authentication_tokens_on_user_id", using: :btree 49 | end 50 | 51 | create_table "feedbacks", force: :cascade do |t| 52 | t.text "description" 53 | t.string "url" 54 | t.integer "user_id" 55 | t.datetime "created_at", null: false 56 | t.datetime "updated_at", null: false 57 | t.binary "image" 58 | t.index ["user_id"], name: "index_feedbacks_on_user_id", using: :btree 59 | end 60 | 61 | create_table "spotlight_images", force: :cascade do |t| 62 | t.string "url" 63 | t.datetime "created_at", null: false 64 | t.datetime "updated_at", null: false 65 | t.integer "user_id" 66 | t.index ["user_id"], name: "index_spotlight_images_on_user_id", using: :btree 67 | end 68 | 69 | create_table "tags", force: :cascade do |t| 70 | t.string "tag", null: false 71 | t.datetime "created_at", null: false 72 | t.datetime "updated_at", null: false 73 | t.index ["tag"], name: "index_tags_on_tag", unique: true, using: :btree 74 | end 75 | 76 | create_table "users", force: :cascade do |t| 77 | t.string "email", default: "", null: false 78 | t.string "encrypted_password", default: "", null: false 79 | t.string "reset_password_token" 80 | t.datetime "reset_password_sent_at" 81 | t.datetime "remember_created_at" 82 | t.integer "sign_in_count", default: 0, null: false 83 | t.datetime "current_sign_in_at" 84 | t.datetime "last_sign_in_at" 85 | t.inet "current_sign_in_ip" 86 | t.inet "last_sign_in_ip" 87 | t.datetime "created_at", null: false 88 | t.datetime "updated_at", null: false 89 | t.string "name", default: "", null: false 90 | t.string "avatar" 91 | t.integer "role" 92 | t.text "bio", default: "" 93 | t.boolean "public", default: false 94 | t.index ["email"], name: "index_users_on_email", unique: true, using: :btree 95 | t.index ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true, using: :btree 96 | end 97 | 98 | add_foreign_key "authentication_tokens", "users" 99 | add_foreign_key "feedbacks", "users" 100 | add_foreign_key "spotlight_images", "users" 101 | end 102 | -------------------------------------------------------------------------------- /db/seeds.rb: -------------------------------------------------------------------------------- 1 | puts 'Creating Users' 2 | User.create!( 3 | name: 'David Harris', 4 | email: 'forbiddenvoid@gmail.com', 5 | password: 'Password', 6 | role: 'admin' 7 | ) 8 | 9 | User.create!( 10 | name: 'Ryan Collins', 11 | email: 'admin@ryancollins.io', 12 | password: 'Password', 13 | bio: 'Experienced Software Engineer specializing in implementing cutting-edge technologies in a multitude of domains, focusing on Front End Web Development and UI / UX.', 14 | avatar: 'https://media.licdn.com/mpr/mpr/shrinknp_400_400/AAEAAQAAAAAAAAQyAAAAJDU3YWY4Nzk1LWQ0YzEtNGIyMy1iOWI3LTBmMTllMmI1Y2Q5NQ.jpg', 15 | role: 'admin' 16 | ) 17 | 18 | User.create!( 19 | name: 'Andreas Daimainger', 20 | email: 'andreas@gmail.com', 21 | password: 'Password', 22 | bio: 'I started to code about 2 years ago and have been very focused on developing cutting edge UI components. I have a passion for functional programming, and I love creating environments that make it easier for developers to write consistent, testable code.', 23 | avatar: 'https://media.licdn.com/mpr/mpr/shrinknp_400_400/AAEAAQAAAAAAAAcWAAAAJDM2ZjBiYmMwLTk4ZmUtNGVkOC05MjYyLTMxMGI1ZmU1NTMyZQ.jpg', 24 | role: 'admin' 25 | ) 26 | 27 | User.create!( 28 | name: 'Demo User', 29 | email: 'demo@udacity.com', 30 | password: 'Password123!', 31 | bio: 'An amazing, but sadly fake user', 32 | avatar: 'http://data.whicdn.com/images/88776105/superthumb.jpg', 33 | role: 'admin' 34 | ) 35 | 36 | User.create!( 37 | name: 'Author User', 38 | email: 'author@udacity.com', 39 | password: 'Password123!', 40 | bio: 'An amazing, but sadly fake author user', 41 | avatar: 'http://data.whicdn.com/images/88776105/superthumb.jpg', 42 | role: 'author' 43 | ) 44 | 45 | User.create!( 46 | name: 'Abhishek Ghosh', 47 | email: 'abhighosh18@gmail.com', 48 | password: 'Password123!', 49 | bio: "Hi my name is Abhi and I am a 🕸 developer working with 🐍 and 🛤'", 50 | avatar: 'https://avatars1.githubusercontent.com/u/19292575?v=3&s=460', 51 | role: 'admin' 52 | ) 53 | 54 | User.create!( 55 | name: 'Chashmeet Singh', 56 | email: 'chashmeetsingh@gmail.com', 57 | password: 'Password123!', 58 | bio: 'Hi, I’m Chashmeet. I am a freelancer and specialize in both web as well as mobile application development. I’m passionate about what I do, and I love to help people out.', 59 | avatar: 'https://files.slack.com/files-tmb/T056SDA1K-F3BKPEYF9-812ce53e6c/cash_360.png', 60 | role: 'admin' 61 | ) 62 | 63 | User.create!( 64 | name: 'James Gallagher', 65 | email: 'me@jamesthedev.com', 66 | password: 'Password123!', 67 | bio: 'Devops Team Lead at Udacity Alumni', 68 | avatar: 'https://avatars3.githubusercontent.com/u/15236109?v=3&s=460', 69 | role: 'admin' 70 | ) 71 | 72 | puts 'Creating tags' 73 | tags = [ 74 | 'Software Engineering', 75 | 'Open Source', 76 | 'Mobile', 77 | 'Web', 78 | 'Machine Learning', 79 | 'Data Analysis', 80 | 'JavaScript', 81 | 'React', 82 | 'GraphQL', 83 | 'Ruby on Rails' 84 | ] 85 | 10.times.each_with_index do |index| 86 | Tag.create!( 87 | tag: tags[index] 88 | ) 89 | end 90 | 91 | puts 'Creating Spotlight Images' 92 | SpotlightImage.create!( 93 | url: 'https://github.com/RyanCCollins/cdn/blob/master/alumni-webapp/Udacity-Self-Driving-Car-Engineer-Nanodegree-800x333.jpg?raw=true', 94 | user: User.first 95 | ) 96 | 97 | SpotlightImage.create!( 98 | url: 'https://github.com/RyanCCollins/cdn/blob/master/alumni-webapp/VRDeveloperNanodegreeProgram.png?raw=true', 99 | user: User.first 100 | ) 101 | 102 | SpotlightImage.create!( 103 | url: 'https://github.com/RyanCCollins/cdn/blob/master/alumni-webapp/deeplearning.jpg?raw=true', 104 | user: User.first 105 | ) 106 | 107 | SpotlightImage.create!( 108 | url: 'https://github.com/RyanCCollins/cdn/blob/master/alumni-webapp/seniorweb.gif?raw=true', 109 | user: User.first 110 | ) 111 | 112 | puts 'Creating articles' 113 | statuses = *(0..2) 114 | spotlighted_count = 0 115 | 10.times do 116 | title = FFaker::HealthcareIpsum.words.map(&:capitalize).join(' ') 117 | featured = [true, false, false].sample 118 | spotlighted_options = [true, false].sample 119 | if spotlighted_options && spotlighted_count <= 3 120 | spotlighted_count += 1 121 | spotlighted = true 122 | else 123 | spotlighted = false 124 | end 125 | Article.create!( 126 | user: User.all.sample, 127 | title: title, 128 | featured: featured, 129 | spotlighted: spotlighted, 130 | status: statuses.sample, 131 | content: FFaker::HealthcareIpsum.paragraph, 132 | feature_image: FFaker::Avatar.image, 133 | tags: Tag.all.sample(4).uniq(&:tag) 134 | ) 135 | end 136 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | app: 2 | build: . 3 | command: rails server -p 3000 -b '0.0.0.0' 4 | volumes: 5 | - .:/app 6 | ports: 7 | - "3000:3000" 8 | links: 9 | - postgres 10 | postgres: 11 | image: postgres:9.4 12 | ports: 13 | - "5432" 14 | -------------------------------------------------------------------------------- /lib/tasks/graphql.rake: -------------------------------------------------------------------------------- 1 | namespace :graphql do 2 | desc "Generate the graphql ruby introspection schema.json file" 3 | task all: [:generate_schema, :move_schema] 4 | 5 | task generate_schema: :environment do 6 | query = GraphQL::Introspection::INTROSPECTION_QUERY 7 | 8 | File.open("./schema.json", "w") do |f| 9 | f.write( 10 | JSON.pretty_generate( 11 | AlumniApiSchema.execute( 12 | query 13 | ) 14 | ) 15 | ) 16 | end 17 | end 18 | task move_schema: :environment do 19 | FileUtils.mv("./schema.json", "../client/config/linting") 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /lib/tasks/validate.rake: -------------------------------------------------------------------------------- 1 | desc 'Run tests and rubocop' 2 | task :validate do 3 | Rake::Task['rubocop'].invoke 4 | Rake::Task['test'].invoke 5 | end 6 | 7 | task :rubocop do 8 | require 'rubocop' 9 | cli = RuboCop::CLI.new 10 | cli.run(%w(--rails --auto-correct)) 11 | end 12 | -------------------------------------------------------------------------------- /log/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacityalumni/udacity-alumni-api/3b3e2e883b18ad4633c8a3eae7975d4ce4d2a08f/log/.keep -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # See http://www.robotstxt.org/robotstxt.html for documentation on how to use the robots.txt file 2 | # 3 | # To ban all spiders from the entire site uncomment the next two lines: 4 | # User-agent: * 5 | # Disallow: / 6 | -------------------------------------------------------------------------------- /spec/controllers/articles_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Api::V1::ArticlesController, type: :controller do 4 | 5 | let(:valid_attributes) { 6 | build(:article).attributes.symbolize_keys 7 | } 8 | 9 | let(:invalid_attributes) { 10 | { test: 1 } 11 | } 12 | 13 | let(:valid_session) { {} } 14 | 15 | describe "GET #index" do 16 | it "assigns all articles as @articles" do 17 | article = Article.create! valid_attributes 18 | get :index, params: {}, session: valid_session 19 | expect(assigns(:articles)).to eq([article]) 20 | end 21 | end 22 | 23 | describe "GET #show" do 24 | it "assigns the requested article as @article" do 25 | article = Article.create! valid_attributes 26 | get :show, params: {id: article.to_param}, session: valid_session 27 | expect(assigns(:article)).to eq(article) 28 | end 29 | end 30 | 31 | describe "POST #create" do 32 | context "with valid params" do 33 | it "creates a new Article" do 34 | expect { 35 | post :create, params: {article: valid_attributes}, session: valid_session 36 | }.to change(Article, :count).by(1) 37 | end 38 | 39 | it "assigns a newly created article as @article" do 40 | post :create, params: {article: valid_attributes}, session: valid_session 41 | expect(assigns(:article)).to be_a(Article) 42 | expect(assigns(:article)).to be_persisted 43 | end 44 | end 45 | end 46 | 47 | describe "PUT #update" do 48 | context "with valid params" do 49 | let(:new_attributes) { 50 | { title: "testing"} 51 | } 52 | 53 | it "updates the requested article" do 54 | article = Article.create! valid_attributes 55 | put :update, params: {id: article.to_param, article: new_attributes}, session: valid_session 56 | article.reload 57 | # skip("Add assertions for updated state") 58 | expect(assigns(:article)).to eq(article) 59 | end 60 | 61 | it "assigns the requested article as @article" do 62 | article = Article.create! valid_attributes 63 | put :update, params: {id: article.to_param, article: valid_attributes}, session: valid_session 64 | expect(assigns(:article)).to eq(article) 65 | end 66 | end 67 | 68 | context "with invalid params" do 69 | it "assigns the article as @article" do 70 | article = Article.create! valid_attributes 71 | put :update, params: {id: article.to_param, article: invalid_attributes}, session: valid_session 72 | expect(assigns(:article)).to eq(article) 73 | end 74 | end 75 | end 76 | 77 | describe "DELETE #destroy" do 78 | it "destroys the requested article" do 79 | article = Article.create! valid_attributes 80 | expect { 81 | delete :destroy, params: {id: article.to_param}, session: valid_session 82 | }.to change(Article, :count).by(-1) 83 | end 84 | end 85 | 86 | end 87 | -------------------------------------------------------------------------------- /spec/controllers/sessions_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Api::V1::SessionsController, type: :controller do 4 | describe 'when POST #create' do 5 | before(:each) do 6 | @user = FactoryGirl.create :user 7 | end 8 | context 'when the credentials are correct' do 9 | before(:each) do 10 | credentials = { email: @user.email, password: @user.password } 11 | post :create, params: { session: credentials }, format: :json 12 | end 13 | it 'returns a response with a session and auth token' do 14 | @user.reload 15 | expect(json_response[:session][:auth_token]).to eql @user.auth_token 16 | end 17 | end 18 | context 'when the credentials are incorrect' do 19 | before(:each) do 20 | credentials = { email: @user.email, password: 'wrongpassword' } 21 | post :create, params: { session: credentials }, format: :json 22 | end 23 | it 'returns a json object with an error' do 24 | expect(json_response[:errors]).to eql 'Invalid email or password' 25 | end 26 | it { should respond_with 422 } 27 | end 28 | end 29 | describe "when DELETE #destroy" do 30 | context "when valid auth token" do 31 | before(:each) do 32 | @user = FactoryGirl.create :user 33 | sign_in @user 34 | @request.headers['Authorization'] = @user.auth_token 35 | delete :destroy 36 | end 37 | it { should respond_with 204 } 38 | end 39 | context "when invalid auth token" do 40 | before(:each) do 41 | @user = FactoryGirl.create :user 42 | sign_in @user 43 | @request.headers['Authorization'] = 'foobared😖' 44 | delete :destroy 45 | end 46 | it 'returns a json object with an error' do 47 | expect(json_response[:errors]).to eql 'An error occured while deleting the token' 48 | end 49 | it { should respond_with 404 } 50 | end 51 | end 52 | end 53 | -------------------------------------------------------------------------------- /spec/controllers/tags_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Api::V1::TagsController, type: :controller do 4 | 5 | describe 'when GET #index' do 6 | before(:each) do 7 | 4.times { 8 | FactoryGirl.create :tag 9 | } 10 | get :index 11 | end 12 | 13 | it 'returns 4 tags from the database' do 14 | expect(json_response[:tags].length).to eq(4) 15 | end 16 | 17 | it { should respond_with 200 } 18 | end 19 | 20 | describe 'when GET #show' do 21 | before(:each) do 22 | @tag = FactoryGirl.create :tag 23 | get :show, id: @tag.id 24 | end 25 | 26 | it 'returns the selected tag as json' do 27 | expect(json_response[:tag][:tag]).to eql @tag.tag 28 | end 29 | it { should respond_with 200 } 30 | end 31 | 32 | describe "when PATCH/PUT #update" do 33 | before(:each) do 34 | @tag = FactoryGirl.create :tag 35 | @user = FactoryGirl.create :user 36 | request.headers['Authorization'] = @user.auth_token 37 | patch :update, params: { id: @tag.id, tag: { tag: 'Hello world' } } 38 | end 39 | it 'successfully updates the tag and responds with json' do 40 | expect(json_response[:tag][:tag]).to eql 'Hello world' 41 | end 42 | it { should respond_with 200 } 43 | end 44 | 45 | describe 'when DELETE #destroy' do 46 | before(:each) do 47 | @tag = FactoryGirl.create :tag 48 | @user = FactoryGirl.create :user 49 | request.headers['Authorization'] = @user.auth_token 50 | delete :destroy, id: @tag.id 51 | end 52 | 53 | it { should respond_with 204 } 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /spec/controllers/users_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | RSpec.describe Api::V1::UsersController, type: :controller do 4 | describe 'GET #show' do 5 | 6 | context "when valid auth token" do 7 | before(:each) do 8 | @user = FactoryGirl.create :user 9 | request.headers['Authorization'] = @user.auth_token 10 | get :show, format: :json 11 | end 12 | it 'returns a user object' do 13 | expect(json_response[:user][:email]).to eql @user.email 14 | end 15 | it { should respond_with 200 } 16 | end 17 | 18 | context "when invalid auth token" do 19 | before(:each) do 20 | @user = FactoryGirl.create :user 21 | request.headers['Authorization'] = 'foobared😖' 22 | get :show, format: :json 23 | end 24 | it 'returns a user object' do 25 | @error = 'Invalid request' 26 | expect(json_response[:errors]).to eql @error 27 | end 28 | it { should respond_with 422 } 29 | end 30 | 31 | end 32 | 33 | describe 'POST #create' do 34 | 35 | context "when user is created successfully" do 36 | before(:each) do 37 | @user_attributes = FactoryGirl.attributes_for :user 38 | post :create, { user: @user_attributes }, format: :json 39 | end 40 | it "renders the json representation for the user" do 41 | expect(json_response[:user][:email]).to eql @user_attributes[:email] 42 | end 43 | it { should respond_with 201 } 44 | end 45 | 46 | context "when user is not created successfully" do 47 | before(:each) do 48 | @invalid_user_attributes = { password: '12345678', 49 | password_confirmation: '87654321' } 50 | post :create, { user: @invalid_user_attributes }, format: :json 51 | end 52 | it 'renders errors as json' do 53 | expect(json_response).to have_key(:errors) 54 | end 55 | it { should respond_with 422 } 56 | end 57 | 58 | end 59 | describe 'PUT/PATCH #updated' do 60 | 61 | before(:each) do 62 | @user = FactoryGirl.create :user 63 | end 64 | 65 | context "when user is successfully updated" do 66 | before(:each) do 67 | request.headers['Authorization'] = @user.auth_token 68 | @new_email = 'hello@world.com' 69 | patch :update, { user: { email: @new_email } }, format: :json 70 | end 71 | it 'renders json for the updated user' do 72 | expect(json_response[:user][:email]).to eql @new_email 73 | end 74 | it { should respond_with 200 } 75 | end 76 | 77 | context "when user is not updated successfully" do 78 | before(:each) do 79 | request.headers['Authorization'] = 'foobared😖' 80 | patch :update, { user: { email: 'who@cares.org' }}, format: :json 81 | end 82 | it 'renders json errors' do 83 | expect(json_response).to have_key(:errors) 84 | end 85 | it { should respond_with 401 } 86 | end 87 | 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/factories/article.rb: -------------------------------------------------------------------------------- 1 | require 'ffaker' 2 | 3 | FactoryGirl.define do 4 | factory :article do 5 | user 6 | title FFaker::HealthcareIpsum.words.map(&:capitalize).join(' ') 7 | featured true 8 | spotlighted false 9 | content FFaker::HealthcareIpsum.paragraph 10 | feature_image FFaker::Avatar.image 11 | status 'draft' 12 | end 13 | end -------------------------------------------------------------------------------- /spec/factories/feedbacks.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :feedback do 3 | description "MyText" 4 | url "MyString" 5 | user nil 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/factories/spotlight_images.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :spotlight_image do 3 | url "MyString" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/factories/tags.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :tag do 3 | tag "MyString" 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /spec/factories/users.rb: -------------------------------------------------------------------------------- 1 | FactoryGirl.define do 2 | factory :user do 3 | name FFaker::Name.name 4 | sequence(:email) { |n| "user#{n}@example.com"} 5 | password 'password' 6 | password_confirmation 'password' 7 | role 'admin' 8 | bio 'Hello world' 9 | avatar 'http://github.com/RyanCCollins.png' 10 | end 11 | end 12 | -------------------------------------------------------------------------------- /spec/mailers/new_user_signup_spec.rb: -------------------------------------------------------------------------------- 1 | require "rails_helper" 2 | 3 | RSpec.describe NewUserSignupMailer, type: :mailer do 4 | before(:each) do 5 | ActionMailer::Base.delivery_method = :test 6 | ActionMailer::Base.perform_deliveries = true 7 | ActionMailer::Base.deliveries = [] 8 | @user = create(:user, email: "user@example.com") 9 | NewUserSignupMailer.welcome_email(@user).deliver_now 10 | end 11 | 12 | after(:each) do 13 | ActionMailer::Base.deliveries.clear 14 | end 15 | 16 | it 'renders the receiver email' do 17 | expect(ActionMailer::Base.deliveries.first.to).to eq(["user@example.com"]) 18 | end 19 | 20 | it 'should set the subject to the correct subject' do 21 | expect(ActionMailer::Base.deliveries.first.subject).to eq('Thank you for joining Udacity alumni network!') 22 | end 23 | 24 | it 'renders the sender email' do 25 | expect(ActionMailer::Base.deliveries.first.from).to eq(["udacity-alumni@udacity.com"]) 26 | end 27 | 28 | end 29 | -------------------------------------------------------------------------------- /spec/mailers/previews/new_user_signup_preview.rb: -------------------------------------------------------------------------------- 1 | # Preview all emails at http://localhost:3000/rails/mailers/new_user_signup 2 | class NewUserSignupPreview < ActionMailer::Preview 3 | 4 | end 5 | -------------------------------------------------------------------------------- /spec/models/article_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Article, type: :model do 4 | describe 'Associations' do 5 | it { should belong_to(:user) } 6 | it { should have_and_belong_to_many(:tags) } 7 | end 8 | it 'is not valid without a title' do 9 | expect( 10 | Article.new(content: 'Yahoo!') 11 | ).to_not be_valid 12 | end 13 | it 'is not valid without content' do 14 | expect( 15 | Article.new(title: 'Hello World') 16 | ).to_not be_valid 17 | end 18 | it 'sets the status to draft by default' do 19 | expect( 20 | Article.new.status 21 | ).to eq('draft') 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/models/feedback_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Feedback, type: :model do 4 | describe 'Associations' do 5 | it { should belong_to(:user) } 6 | end 7 | it 'is not valid without a description' do 8 | expect( 9 | Feedback.new(url: 'Yahoo.com') 10 | ).to_not be_valid 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /spec/models/spotlight_image_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe SpotlightImage, type: :model do 4 | pending "add some examples to (or delete) #{__FILE__}" 5 | end 6 | -------------------------------------------------------------------------------- /spec/models/tag_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe Tag, type: :model do 4 | 5 | describe 'has expected columns with expected options' do 6 | it { should have_db_column(:tag).of_type(:string).with_options(null: false) } 7 | end 8 | 9 | it { should validate_presence_of :tag } 10 | 11 | it { should have_and_belong_to_many(:articles) } 12 | 13 | it 'is not valid without a tag' do 14 | expect( 15 | Tag.new() 16 | ).to_not be_valid 17 | end 18 | 19 | end 20 | -------------------------------------------------------------------------------- /spec/models/user_spec.rb: -------------------------------------------------------------------------------- 1 | require 'rails_helper' 2 | 3 | RSpec.describe User, type: :model do 4 | describe 'has expected columns with expected options' do 5 | it { should have_db_column(:name).of_type(:string).with_options(null: false) } 6 | it { should have_db_column(:email).of_type(:string).with_options(null: false) } 7 | it { should have_db_column(:avatar).of_type(:string) } 8 | it { should have_db_column(:encrypted_password).of_type(:string).with_options(null: false, default: '') } 9 | it { should have_db_column(:reset_password_token).of_type(:string).with_options(null: true, default: '') } 10 | it { should have_db_column(:reset_password_sent_at).of_type(:datetime).with_options(null: true) } 11 | it { should have_db_column(:remember_created_at).of_type(:datetime).with_options(null: true) } 12 | it { should have_db_column(:sign_in_count).of_type(:integer).with_options(null: false, default: 0) } 13 | it { should have_db_column(:current_sign_in_at).of_type(:datetime).with_options(null: true) } 14 | it { should have_db_column(:last_sign_in_at).of_type(:datetime).with_options(null: true) } 15 | it { should have_db_column(:current_sign_in_ip).of_type(:inet).with_options(null: true) } 16 | it { should have_db_column(:last_sign_in_ip).of_type(:inet).with_options(null: true) } 17 | it { should have_db_column(:created_at).of_type(:datetime).with_options(null: false) } 18 | it { should have_db_column(:updated_at).of_type(:datetime).with_options(null: false) } 19 | end 20 | # TODO: Add tests for behavior and also for the authorization policies 21 | end 22 | -------------------------------------------------------------------------------- /spec/policies/article_policy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe ArticlePolicy do 4 | subject { ArticlePolicy.new(user, article) } 5 | 6 | let(:article) { Article.create } 7 | 8 | context "for a user" do 9 | let(:user) { nil } 10 | 11 | it { is_expected.to permit_action(:show) } 12 | it { is_expected.to forbid_action(:create) } 13 | it { is_expected.to forbid_action(:new) } 14 | it { is_expected.to forbid_action(:update) } 15 | it { is_expected.to forbid_action(:edit) } 16 | it { is_expected.to forbid_action(:destroy) } 17 | end 18 | 19 | context "for an author" do 20 | let(:user) { User.create(role: 'author') } 21 | 22 | it { is_expected.to permit_action(:show) } 23 | it { is_expected.to permit_action(:create) } 24 | it { is_expected.to permit_action(:new) } 25 | it { is_expected.to permit_action(:update) } 26 | it { is_expected.to permit_action(:edit) } 27 | it { is_expected.to permit_action(:destroy) } 28 | end 29 | 30 | context "for an admin" do 31 | let(:user) { User.create(role: 'admin') } 32 | 33 | it { is_expected.to permit_action(:show) } 34 | it { is_expected.to permit_action(:create) } 35 | it { is_expected.to permit_action(:new) } 36 | it { is_expected.to permit_action(:update) } 37 | it { is_expected.to permit_action(:edit) } 38 | it { is_expected.to permit_action(:destroy) } 39 | end 40 | 41 | end -------------------------------------------------------------------------------- /spec/rails_helper.rb: -------------------------------------------------------------------------------- 1 | # This file is copied to spec/ when you run 'rails generate rspec:install' 2 | ENV['RAILS_ENV'] ||= 'test' 3 | require File.expand_path('../../config/environment', __FILE__) 4 | # Prevent database truncation if the environment is production 5 | abort("The Rails environment is running in production mode!") if Rails.env.production? 6 | require 'spec_helper' 7 | require 'rspec/rails' 8 | require 'shoulda/matchers' 9 | require 'ffaker' 10 | require 'pundit/rspec' 11 | require 'pundit/matchers' 12 | require 'devise' 13 | 14 | ActiveRecord::Migration.maintain_test_schema! 15 | 16 | # Load up the support files 17 | Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } 18 | 19 | RSpec.configure do |config| 20 | # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures 21 | config.fixture_path = "#{::Rails.root}/spec/fixtures" 22 | 23 | # Include Factory Girl syntax to simplify calls to factories 24 | config.include FactoryGirl::Syntax::Methods 25 | 26 | config.include Devise::Test::ControllerHelpers, type: :controller 27 | 28 | # If you're not using ActiveRecord, or you'd prefer not to run each of your 29 | # examples within a transaction, remove the following line or assign false 30 | # instead of true. 31 | config.use_transactional_fixtures = true 32 | 33 | # RSpec Rails can automatically mix in different behaviours to your tests 34 | # based on their file location, for example enabling you to call `get` and 35 | # `post` in specs under `spec/controllers`. 36 | # 37 | # You can disable this behaviour by removing the line below, and instead 38 | # explicitly tag your specs with their type, e.g.: 39 | # 40 | # RSpec.describe UsersController, :type => :controller do 41 | # # ... 42 | # end 43 | # 44 | # The different available types are documented in the features, such as in 45 | # https://relishapp.com/rspec/rspec-rails/docs 46 | config.infer_spec_type_from_file_location! 47 | 48 | # Filter lines from Rails gems in backtraces. 49 | config.filter_rails_from_backtrace! 50 | # arbitrary gems may also be filtered via: 51 | # config.filter_gems_from_backtrace("gem name") 52 | 53 | # test database cleaner 54 | config.before(:suite) do 55 | DatabaseCleaner.strategy = :transaction 56 | DatabaseCleaner.clean_with(:truncation) 57 | end 58 | 59 | config.around(:each) do |example| 60 | DatabaseCleaner.cleaning do 61 | example.run 62 | end 63 | end 64 | 65 | config.after(:each) do 66 | DatabaseCleaner.clean 67 | end 68 | 69 | end 70 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # This file was generated by the `rails generate rspec:install` command. Conventionally, all 2 | # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`. 3 | # The generated `.rspec` file contains `--require spec_helper` which will cause 4 | # this file to always be loaded, without a need to explicitly require it in any 5 | # files. 6 | # 7 | # Given that it is always loaded, you are encouraged to keep this file as 8 | # light-weight as possible. Requiring heavyweight dependencies from this file 9 | # will add to the boot time of your test suite on EVERY test run, even for an 10 | # individual file that may not need all of that loaded. Instead, consider making 11 | # a separate helper file that requires the additional dependencies and performs 12 | # the additional setup, and require it from the spec files that actually need 13 | # it. 14 | # 15 | # The `.rspec` file also contains a few flags that are not defaults but that 16 | # users commonly want. 17 | # 18 | # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration 19 | ENV["RAILS_ENV"] ||= 'test' 20 | require File.expand_path("../../config/environment", __FILE__) 21 | require 'rspec/rails' 22 | require 'rails_helper' 23 | 24 | RSpec.configure do |config| 25 | # rspec-expectations config goes here. You can use an alternate 26 | # assertion/expectation library such as wrong or the stdlib/minitest 27 | # assertions if you prefer. 28 | config.expect_with :rspec do |expectations| 29 | # This option will default to `true` in RSpec 4. It makes the `description` 30 | # and `failure_message` of custom matchers include text for helper methods 31 | # defined using `chain`, e.g.: 32 | # be_bigger_than(2).and_smaller_than(4).description 33 | # # => "be bigger than 2 and smaller than 4" 34 | # ...rather than: 35 | # # => "be bigger than 2" 36 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 37 | end 38 | 39 | # rspec-mocks config goes here. You can use an alternate test double 40 | # library (such as bogus or mocha) by changing the `mock_with` option here. 41 | config.mock_with :rspec do |mocks| 42 | # Prevents you from mocking or stubbing a method that does not exist on 43 | # a real object. This is generally recommended, and will default to 44 | # `true` in RSpec 4. 45 | mocks.verify_partial_doubles = true 46 | end 47 | 48 | # This option will default to `:apply_to_host_groups` in RSpec 4 (and will 49 | # have no way to turn it off -- the option exists only for backwards 50 | # compatibility in RSpec 3). It causes shared context metadata to be 51 | # inherited by the metadata hash of host groups and examples, rather than 52 | # triggering implicit auto-inclusion in groups with matching metadata. 53 | config.shared_context_metadata_behavior = :apply_to_host_groups 54 | config.include Request::JsonHelpers, type: :controller 55 | end 56 | -------------------------------------------------------------------------------- /spec/support/factory_girl.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.include FactoryGirl::Syntax::Methods 3 | end 4 | -------------------------------------------------------------------------------- /spec/support/request_helpers.rb: -------------------------------------------------------------------------------- 1 | module Request 2 | module JsonHelpers 3 | def json_response 4 | @json_response ||= JSON.parse(response.body, symbolize_names: true) 5 | end 6 | end 7 | end 8 | -------------------------------------------------------------------------------- /spec/support/shoulda_matchers.rb: -------------------------------------------------------------------------------- 1 | Shoulda::Matchers.configure do |config| 2 | config.integrate do |with| 3 | # Choose a test framework: 4 | with.test_framework :rspec 5 | 6 | # Or, choose the following (which implies all of the above): 7 | with.library :rails 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /tmp/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/udacityalumni/udacity-alumni-api/3b3e2e883b18ad4633c8a3eae7975d4ce4d2a08f/tmp/.keep --------------------------------------------------------------------------------