diff --git a/.gitignore b/.gitignore index 5b80ff5..f732b7c 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,4 @@ # Ignore master key for decrypting credentials and more. /config/master.key -/.env \ No newline at end of file +.env \ No newline at end of file diff --git a/Gemfile b/Gemfile index d49723c..c9d6552 100644 --- a/Gemfile +++ b/Gemfile @@ -41,4 +41,10 @@ end # Windows does not include zoneinfo files, so bundle the tzinfo-data gem gem 'tzinfo-data', platforms: [:mingw, :mswin, :x64_mingw, :jruby] -gem 'dotenv-rails' \ No newline at end of file +gem 'dotenv-rails' + +gem 'faraday', '~> 0.11' + +gem 'jwt', '~> 1.5' + +gem 'pry' \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index cb1b28d..2634f98 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -60,6 +60,7 @@ GEM msgpack (~> 1.0) builder (3.2.3) byebug (11.0.1) + coderay (1.1.2) concurrent-ruby (1.1.5) crass (1.0.4) dotenv (2.7.5) @@ -67,11 +68,14 @@ GEM dotenv (= 2.7.5) railties (>= 3.2, < 6.1) erubi (1.8.0) + faraday (0.15.4) + multipart-post (>= 1.2, < 3) ffi (1.11.1) globalid (0.4.2) activesupport (>= 4.2.0) i18n (1.6.0) concurrent-ruby (~> 1.0) + jwt (1.5.6) listen (3.1.5) rb-fsevent (~> 0.9, >= 0.9.4) rb-inotify (~> 0.9, >= 0.9.7) @@ -89,9 +93,13 @@ GEM mini_portile2 (2.4.0) minitest (5.11.3) msgpack (1.3.1) + multipart-post (2.1.1) nio4r (2.5.1) nokogiri (1.10.4) mini_portile2 (~> 2.4.0) + pry (0.12.2) + coderay (~> 1.1.0) + method_source (~> 0.9.0) puma (3.12.1) rack (2.0.7) rack-test (1.1.0) @@ -155,7 +163,10 @@ DEPENDENCIES bootsnap (>= 1.4.2) byebug dotenv-rails + faraday (~> 0.11) + jwt (~> 1.5) listen (>= 3.0.5, < 3.2) + pry puma (~> 3.11) rails (~> 6.0.0) spring diff --git a/Rakefile b/Rakefile index fb4148c..f0e29a5 100644 --- a/Rakefile +++ b/Rakefile @@ -3,6 +3,8 @@ require_relative 'config/application' require 'fileutils' +require 'dotenv' + namespace :start do task :development do diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 4ac8823..f6f3667 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,2 +1,16 @@ class ApplicationController < ActionController::API -end + def current_user + token = params[:token] + payload = TokenEncoder.decode(token) + puts payload + @current_user ||= User.find_by_login(payload[0]['sub']) + end + + def logged_in? + current_user != nil + end + + def authenticate_user! + head :unauthorized unless logged_in? + end + end \ No newline at end of file diff --git a/app/controllers/authentication_controller.rb b/app/controllers/authentication_controller.rb new file mode 100644 index 0000000..9beacd3 --- /dev/null +++ b/app/controllers/authentication_controller.rb @@ -0,0 +1,35 @@ + +require 'pry' + +class AuthenticationController < ApplicationController + def github + + #binding.pry + + authenticator = Authenticator.new + user_info = authenticator.github(params[:code]) + + login = user_info[:login] + name = user_info[:name] + puts login + + # Generate token... + token = TokenEncoder.encode(login) + # ... create user if it doesn't exist... + User.where(login: login).first_or_create!( + name: name, + #avatar_url: avatar_url + ) + puts "here: "+token + # ... and redirect to client app. + redirect_to "#{issuer}?token=#{token}" + rescue StandardError => error + redirect_to "#{issuer}?error=#{error.message}" + end + + private + + def issuer + ENV['CLIENT_URL'] + end + end \ No newline at end of file diff --git a/app/controllers/dashboards_controller.rb b/app/controllers/dashboards_controller.rb new file mode 100644 index 0000000..0c523c2 --- /dev/null +++ b/app/controllers/dashboards_controller.rb @@ -0,0 +1,52 @@ +class DashboardsController < ApplicationController + before_action :authenticate_user! + before_action :set_dashboard, only: [:show, :update, :destroy] + + # GET /dashboards + def index + @dashboards = Dashboard.all + + render json: @dashboards + end + + # GET /dashboards/1 + def show + render json: @dashboard + end + + # POST /dashboards + def create + @dashboard = Dashboard.new(dashboard_params) + + if @dashboard.save + render json: @dashboard, status: :created, location: @dashboard + else + render json: @dashboard.errors, status: :unprocessable_entity + end + end + + # PATCH/PUT /dashboards/1 + def update + if @dashboard.update(dashboard_params) + render json: @dashboard + else + render json: @dashboard.errors, status: :unprocessable_entity + end + end + + # DELETE /dashboards/1 + def destroy + @dashboard.destroy + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_dashboard + @dashboard = Dashboard.find(params[:id]) + end + + # Only allow a trusted parameter "white list" through. + def dashboard_params + params.require(:dashboard).permit(:user_id) + end +end diff --git a/app/controllers/test_controller.rb b/app/controllers/test_controller.rb index 122e420..fb9fdc7 100644 --- a/app/controllers/test_controller.rb +++ b/app/controllers/test_controller.rb @@ -1,7 +1,8 @@ class TestController < ApplicationController + before_action :authenticate_user! def new - foo = {foo:"bar"} + foo = {foo:current_user} render :json => foo end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb new file mode 100644 index 0000000..82adb9c --- /dev/null +++ b/app/controllers/users_controller.rb @@ -0,0 +1,51 @@ +class UsersController < ApplicationController + before_action :set_user, only: [:show, :update, :destroy] + + # GET /users + def index + @users = User.all + + render json: @users + end + + # GET /users/1 + def show + render json: @user + end + + # POST /users + def create + @user = User.new(user_params) + + if @user.save + render json: @user, status: :created, location: @user + else + render json: @user.errors, status: :unprocessable_entity + end + end + + # PATCH/PUT /users/1 + def update + if @user.update(user_params) + render json: @user + else + render json: @user.errors, status: :unprocessable_entity + end + end + + # DELETE /users/1 + def destroy + @user.destroy + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_user + @user = User.find(params[:id]) + end + + # Only allow a trusted parameter "white list" through. + def user_params + params.require(:user).permit(:login, :name) + end +end diff --git a/app/models/dashboard.rb b/app/models/dashboard.rb new file mode 100644 index 0000000..d8c4b22 --- /dev/null +++ b/app/models/dashboard.rb @@ -0,0 +1,3 @@ +class Dashboard < ApplicationRecord + belongs_to :user +end diff --git a/app/models/user.rb b/app/models/user.rb new file mode 100644 index 0000000..379658a --- /dev/null +++ b/app/models/user.rb @@ -0,0 +1,2 @@ +class User < ApplicationRecord +end diff --git a/client/src/App.js b/client/src/App.js index 397b706..6d8ddbd 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -1,15 +1,22 @@ -import React from 'react'; - +import React, { Component } from 'react'; +import { getQueryParams } from "./utility/urlUtility" import './App.css'; - -function App() { +import Login from'./component/login' +class App extends Component { + + constructor(){ + super() + this.state = { token: getQueryParams().token } + } + render(){ return (
- Yo! + + {!this.state.token ? : "" }
- ); + );} } export default App; diff --git a/client/src/component/login/index.js b/client/src/component/login/index.js new file mode 100644 index 0000000..e1a26ca --- /dev/null +++ b/client/src/component/login/index.js @@ -0,0 +1,6 @@ +import React from 'react' + + +export default ()=>{ + return ( log in w/ github) +} \ No newline at end of file diff --git a/client/src/index.js b/client/src/index.js index 47a7de0..e27b6be 100644 --- a/client/src/index.js +++ b/client/src/index.js @@ -2,13 +2,13 @@ import React from 'react'; import ReactDOM from 'react-dom'; import './index.css'; import App from './App'; -import { BrowserRouter as Router, Route } from 'react-router-dom'; +import { BrowserRouter, Route } from 'react-router-dom'; import * as serviceWorker from './serviceWorker'; -ReactDOM.render(( - - - ), document.getElementById('root')); +ReactDOM.render( + + + , document.getElementById('root')); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. diff --git a/client/src/utility/urlUtility.js b/client/src/utility/urlUtility.js new file mode 100644 index 0000000..efc23d8 --- /dev/null +++ b/client/src/utility/urlUtility.js @@ -0,0 +1,8 @@ +export function getQueryParams() { + const query = window.location.search.substring(1); + const pairs = query.split('&').map((str) => str.split('=')); + return pairs.reduce((memo, pair) => { + memo[pair[0]] = pair[1]; + return memo; + }, {}); + } \ No newline at end of file diff --git a/config/application.rb b/config/application.rb index f9ba45c..60ac42a 100644 --- a/config/application.rb +++ b/config/application.rb @@ -19,11 +19,16 @@ # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) + + module CustomDash class Application < Rails::Application # Initialize configuration defaults for originally generated Rails version. config.load_defaults 6.0 + config.autoload_paths += %W(#{config.root}/lib) + config.autoload_paths += Dir["#{config.root}/lib/**/"] + # Settings in config/environments/* take precedence over those specified here. # Application configuration can go into files in config/initializers # -- all .rb files in that directory are automatically loaded after loading diff --git a/config/routes.rb b/config/routes.rb index 9e86edf..f728dcd 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,8 @@ Rails.application.routes.draw do + resources :dashboards + resources :users # For details on the DSL available within this file, see https://guides.rubyonrails.org/routing.html get "/test" => "test#new" - get "/auth/google/callback" => "session#googleAuth" + #get "/auth/google/callback" => "session#googleAuth" + get '/auth/github', to: 'authentication#github', format: false end diff --git a/db/migrate/20190831164054_create_users.rb b/db/migrate/20190831164054_create_users.rb new file mode 100644 index 0000000..4f42ace --- /dev/null +++ b/db/migrate/20190831164054_create_users.rb @@ -0,0 +1,10 @@ +class CreateUsers < ActiveRecord::Migration[6.0] + def change + create_table :users do |t| + t.string :login + t.string :name + + t.timestamps + end + end +end diff --git a/db/migrate/20190831165244_create_dashboards.rb b/db/migrate/20190831165244_create_dashboards.rb new file mode 100644 index 0000000..b4523f8 --- /dev/null +++ b/db/migrate/20190831165244_create_dashboards.rb @@ -0,0 +1,9 @@ +class CreateDashboards < ActiveRecord::Migration[6.0] + def change + create_table :dashboards do |t| + t.references :user, null: false, foreign_key: true + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb new file mode 100644 index 0000000..ca8281c --- /dev/null +++ b/db/schema.rb @@ -0,0 +1,30 @@ +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `rails +# db:schema:load`. When creating a new database, `rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema.define(version: 2019_08_31_165244) do + + create_table "dashboards", force: :cascade do |t| + t.integer "user_id", null: false + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["user_id"], name: "index_dashboards_on_user_id" + end + + create_table "users", force: :cascade do |t| + t.string "login" + t.string "name" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + end + + add_foreign_key "dashboards", "users" +end diff --git a/lib/authenticator.rb b/lib/authenticator.rb new file mode 100644 index 0000000..3189e11 --- /dev/null +++ b/lib/authenticator.rb @@ -0,0 +1,43 @@ +class Authenticator + def initialize(connection = Faraday.new) + @connection = connection + end + + def github(code) + access_token_resp = fetch_github_access_token(code) + access_token = access_token_resp['access_token'] + user_info_resp = fetch_github_user_info(access_token) + puts user_info_resp + { + issuer: ENV['CLIENT_URL'], + login: user_info_resp['login'], + name: user_info_resp['name'], + #avatar_url: user_info_resp['avatar_url'] + } + end + + private + + def fetch_github_access_token(code) + resp = @connection.post ENV['GITHUB_ACCESS_TOKEN_URL'], { + code: code, + client_id: ENV['CLIENT_ID'], + client_secret: ENV['CLIENT_SECRET'] + } + #binding.pry + raise IOError, 'FETCH_ACCESS_TOKEN' unless resp.success? + URI.decode_www_form(resp.body).to_h + end + + def fetch_github_user_info(access_token) + #binding.pry + resp = @connection.get ENV['GITHUB_USER_INFO_URL'], { + access_token: access_token + } + + + + raise IOError, 'FETCH_USER_INFO' unless resp.success? + JSON.parse(resp.body) + end + end \ No newline at end of file diff --git a/lib/token_encoder.rb b/lib/token_encoder.rb new file mode 100644 index 0000000..5f41e7e --- /dev/null +++ b/lib/token_encoder.rb @@ -0,0 +1,22 @@ +module TokenEncoder + def self.encode(sub) + payload = { + iss: ENV['CLIENT_URL'], + sub: sub, + exp: 4.hours.from_now.to_i, + iat: Time.now.to_i + } + JWT.encode payload, ENV['JWT_SECRET'], 'HS256' + end + + def self.decode(token) + options = { + iss: ENV['CLIENT_URL'], + verify_iss: true, + verify_iat: true, + leeway: 30, + algorithm: 'HS256' + } + JWT.decode token, ENV['JWT_SECRET'], true, options + end + end \ No newline at end of file diff --git a/test/controllers/authentication_controller_test.rb b/test/controllers/authentication_controller_test.rb new file mode 100644 index 0000000..ab5e221 --- /dev/null +++ b/test/controllers/authentication_controller_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class AuthenticationControllerTest < ActionDispatch::IntegrationTest + # test "the truth" do + # assert true + # end +end diff --git a/test/controllers/dashboards_controller_test.rb b/test/controllers/dashboards_controller_test.rb new file mode 100644 index 0000000..f8371eb --- /dev/null +++ b/test/controllers/dashboards_controller_test.rb @@ -0,0 +1,38 @@ +require 'test_helper' + +class DashboardsControllerTest < ActionDispatch::IntegrationTest + setup do + @dashboard = dashboards(:one) + end + + test "should get index" do + get dashboards_url, as: :json + assert_response :success + end + + test "should create dashboard" do + assert_difference('Dashboard.count') do + post dashboards_url, params: { dashboard: { user_id: @dashboard.user_id } }, as: :json + end + + assert_response 201 + end + + test "should show dashboard" do + get dashboard_url(@dashboard), as: :json + assert_response :success + end + + test "should update dashboard" do + patch dashboard_url(@dashboard), params: { dashboard: { user_id: @dashboard.user_id } }, as: :json + assert_response 200 + end + + test "should destroy dashboard" do + assert_difference('Dashboard.count', -1) do + delete dashboard_url(@dashboard), as: :json + end + + assert_response 204 + end +end diff --git a/test/controllers/users_controller_test.rb b/test/controllers/users_controller_test.rb new file mode 100644 index 0000000..2a2e246 --- /dev/null +++ b/test/controllers/users_controller_test.rb @@ -0,0 +1,38 @@ +require 'test_helper' + +class UsersControllerTest < ActionDispatch::IntegrationTest + setup do + @user = users(:one) + end + + test "should get index" do + get users_url, as: :json + assert_response :success + end + + test "should create user" do + assert_difference('User.count') do + post users_url, params: { user: { login: @user.login, name: @user.name } }, as: :json + end + + assert_response 201 + end + + test "should show user" do + get user_url(@user), as: :json + assert_response :success + end + + test "should update user" do + patch user_url(@user), params: { user: { login: @user.login, name: @user.name } }, as: :json + assert_response 200 + end + + test "should destroy user" do + assert_difference('User.count', -1) do + delete user_url(@user), as: :json + end + + assert_response 204 + end +end diff --git a/test/fixtures/dashboards.yml b/test/fixtures/dashboards.yml new file mode 100644 index 0000000..6dc1ab3 --- /dev/null +++ b/test/fixtures/dashboards.yml @@ -0,0 +1,7 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + user: one + +two: + user: two diff --git a/test/fixtures/users.yml b/test/fixtures/users.yml new file mode 100644 index 0000000..3b34319 --- /dev/null +++ b/test/fixtures/users.yml @@ -0,0 +1,9 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +one: + login: MyString + name: MyString + +two: + login: MyString + name: MyString diff --git a/test/models/dashboard_test.rb b/test/models/dashboard_test.rb new file mode 100644 index 0000000..48d77a9 --- /dev/null +++ b/test/models/dashboard_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class DashboardTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end diff --git a/test/models/user_test.rb b/test/models/user_test.rb new file mode 100644 index 0000000..82f61e0 --- /dev/null +++ b/test/models/user_test.rb @@ -0,0 +1,7 @@ +require 'test_helper' + +class UserTest < ActiveSupport::TestCase + # test "the truth" do + # assert true + # end +end