Skip to content
This repository was archived by the owner on Mar 31, 2025. It is now read-only.

Commit 50ee5c0

Browse files
committed
User Twitter handle (#49)
* Add User#handle * General OAuth controller (github/twitter) * Connect/disconnect twitter handle * Mention user on twitter * More specs * SessionsController -> SessionController * Add twitter icon * Add csrf tag & spec improvements
1 parent 842c7cb commit 50ee5c0

File tree

21 files changed

+432
-169
lines changed

21 files changed

+432
-169
lines changed

config/initializers/multiauth.cr

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
require "multi_auth"
22

33
MultiAuth.config("github", ENV.fetch("GITHUB_ID", ""), ENV.fetch("GITHUB_SECRET", ""))
4+
MultiAuth.config("twitter", ENV.fetch("TWITTER_CONSUMER_KEY", ""), ENV.fetch("TWITTER_CONSUMER_SECRET", ""))

config/routes.cr

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,15 @@ Amber::Server.configure do |app|
2626
get "/announcements/random", AnnouncementController, :random
2727
get "/=:hashid", AnnouncementController, :expand
2828
get "/rss", RSSController, :show
29-
get "/sessions/new", SessionsController, :new
30-
delete "/sessions", SessionsController, :destroy
31-
get "/github/auth", SessionsController, :create
29+
30+
get "/oauth/new", OAuthController, :new
31+
get "/oauth/:provider", OAuthController, :authenticate
32+
delete "/sessions", SessionController, :destroy
33+
3234
get "/me", UserController, :me
3335
get "/users/:login", UserController, :show
36+
put "/users/remove_handle", UserController, :remove_handle
37+
3438
get "/", AnnouncementController, :index
3539
end
3640
end
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
-- +micrate Up
2+
ALTER TABLE users ADD COLUMN handle VARCHAR;
3+
4+
-- +micrate Down
5+
ALTER TABLE users DROP COLUMN handle;

db/seed.cr

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ require "./seeds/*"
55

66
puts "Preparing development database:"
77

8-
puts " * Deleting existed records"
8+
puts " * Deleting existing records"
99
Announcement.clear
1010
User.clear
1111

public/stylesheets/main.css

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -527,6 +527,10 @@ input[type="submit"] {
527527
text-transform: uppercase;
528528
}
529529

530+
button .fa {
531+
margin-right: 5px;
532+
}
533+
530534
button:hover,
531535
input[type="button"]:hover,
532536
input[type="reset"]:hover,
@@ -2491,6 +2495,12 @@ p > video {
24912495
padding-right: 5px;
24922496
}
24932497

2498+
div.connect-twitter {
2499+
display: inline-block;
2500+
padding-top: 35px;
2501+
margin-bottom: -20px;
2502+
}
2503+
24942504
/**
24952505
* 16.0 Media Queries
24962506
*/

shard.lock

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ shards:
5858

5959
multi_auth:
6060
github: msa7/multi_auth
61-
commit: 2fc4db9b60e63d535289550c62eab4ece94aaf71
61+
version: 1.0.0
6262

6363
mysql:
6464
github: crystal-lang/crystal-mysql
@@ -78,7 +78,7 @@ shards:
7878

7979
radix:
8080
github: luislavena/radix
81-
commit: 211418416adba540b594af2260e1b5d0c8877c9e
81+
version: 0.3.8
8282

8383
redis:
8484
github: stefanwille/crystal-redis

shard.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ dependencies:
1919

2020
multi_auth:
2121
github: msa7/multi_auth
22-
branch: master
22+
version: 1.0.0
2323

2424
micrate:
2525
github: juanedi/micrate
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
1+
require "./spec_helper"
2+
require "webmock"
3+
4+
describe OAuthController do
5+
describe "GET authenticate" do
6+
context "when provider is not known" do
7+
it "redirects to /" do
8+
get "/oauth/facebook"
9+
expect(response).to redirect_to "/"
10+
end
11+
end
12+
end
13+
14+
context "Github" do
15+
let(:code) { "5ca3797f33399346a01c" }
16+
let(:github_id) { "roCDAM4ShR" }
17+
let(:github_secret) { "J9SYlLlO6XIk5lBqtOpoQlOhcbVEqbm5" }
18+
let(:github_access_token_response) do
19+
{
20+
access_token: "iuxcxlkbwe2342lkasdfjk2klsdj",
21+
token_type: "Bearer",
22+
expires_in: 300,
23+
refresh_token: nil,
24+
scope: "user",
25+
}
26+
end
27+
let(:github_user_response) do
28+
{
29+
login: "amber",
30+
id: 36345345,
31+
name: "Amber Framework",
32+
email: "test@email.com",
33+
bio: "Blah",
34+
}
35+
end
36+
37+
let(:stub_github_authorize_request) do
38+
MultiAuth.config "github", github_id, github_secret
39+
40+
body = "client_id=#{github_id}&client_secret=#{github_secret}&redirect_uri=&grant_type=authorization_code&code=#{code}"
41+
headers = {"Accept" => "application/json", "Content-type" => "application/x-www-form-urlencoded"}
42+
43+
WebMock.stub(:post, "https://github.com/login/oauth/access_token")
44+
.with(body: body, headers: headers)
45+
.to_return(body: github_access_token_response.to_json)
46+
47+
WebMock.stub(:get, "https://api.github.com/user").to_return(body: github_user_response.to_json)
48+
end
49+
50+
describe "GET new" do
51+
it "redirects to github authorize uri" do
52+
get "/oauth/new"
53+
expect(response.headers["Location"].includes? "https://github.com/login/oauth/authorize").to be_true
54+
end
55+
end
56+
57+
describe "GET authenticate" do
58+
before { Announcement.clear; User.clear }
59+
before { stub_github_authorize_request }
60+
61+
it "creates a new user" do
62+
get "/oauth/github", body: "code=#{code}"
63+
u = User.find_by_uid_and_provider(github_user_response[:id], "github")
64+
expect(u).not_to be_nil
65+
expect(u.not_nil!.login).to eq github_user_response[:login]
66+
expect(u.not_nil!.name).to eq github_user_response[:name]
67+
end
68+
69+
it "signs in a user" do
70+
get "/oauth/github", body: "code=#{code}"
71+
u = User.find_by_uid_and_provider(github_user_response[:id], "github")
72+
expect(session["user_id"]).to eq u.not_nil!.id.to_s
73+
end
74+
75+
it "redirects to announcements#new" do
76+
get "/oauth/github", body: "code=#{code}"
77+
expect(response.status_code).to eq 302
78+
expect(response).to redirect_to "/announcements/new"
79+
end
80+
81+
it "can find existing user and update attributes" do
82+
u = user(
83+
name: "Marilyn Manson",
84+
login: github_user_response[:login],
85+
uid: github_user_response[:id].to_s,
86+
provider: "github"
87+
).tap(&.save).not_nil!
88+
89+
get "/oauth/github", body: "code=#{code}"
90+
user = User.find(u.id)
91+
expect(user.not_nil!.name).to eq github_user_response[:name]
92+
end
93+
94+
it "does not create a new user if such user exists" do
95+
u = user(
96+
name: "Marilyn Manson",
97+
login: github_user_response[:login],
98+
uid: github_user_response[:id].to_s,
99+
provider: "github"
100+
).tap(&.save).not_nil!
101+
102+
get "/oauth/github", body: "code=#{code}"
103+
expect(User.all.size).to eq 1
104+
end
105+
end
106+
end
107+
108+
context "Twitter" do
109+
let(:user) { user(login: "JohnDoe").tap &.save }
110+
let(:twitter_consumer_key) { "consumer_key" }
111+
let(:twitter_consumer_secret) { "consumer_secret_key" }
112+
let(:twitter_access_token_response) do
113+
{
114+
oauth_token: "NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0",
115+
oauth_token_secret: "veNRnAWe6inFuo8o2u8SLLZLjolYDmDP7SzL0YfYI",
116+
oauth_callback_confirmed: "true",
117+
}
118+
end
119+
let(:twitter_access_token_response) do
120+
{
121+
oauth_token: "7588892-kagSNqWge8gB1WwE3plnFsJHAZVfxWD7Vb57p0b4",
122+
oauth_token_secret: "PbKfYqSryyeKDWz4ebtY3o5ogNLG11WJuZBc9fQrQo",
123+
}
124+
end
125+
let(:twitter_verify_credentials_response) do
126+
{
127+
id: 38895958,
128+
name: "Sean Cook",
129+
screen_name: "theSeanCook",
130+
location: "San Francisco",
131+
url: "http://twitter.com",
132+
description: "I taught your phone that thing you like. The Mobile Partner Engineer @Twitter.",
133+
profile_image_url: "http://a0.twimg.com/profile_images/1751506047/dead_sexy_normal.JPG",
134+
email: "me@twitter.com",
135+
}
136+
end
137+
let(:twitter_authenticate_request) do
138+
{
139+
oauth_token: "token",
140+
oauth_verifier: "verifier",
141+
}
142+
end
143+
144+
let(:stub_twitter_authorize_request) do
145+
WebMock.stub(:post, "https://api.twitter.com/oauth/request_token")
146+
.to_return(body: HTTP::Params.encode twitter_access_token_response)
147+
end
148+
let(:stub_access_token_request) do
149+
WebMock.stub(:post, "https://api.twitter.com/oauth/access_token")
150+
.to_return(body: HTTP::Params.encode twitter_access_token_response)
151+
end
152+
let(:stub_twitter_verify_credentials_request) do
153+
WebMock.stub(:get, "https://api.twitter.com/1.1/account/verify_credentials.json?include_email=true")
154+
.to_return(body: twitter_verify_credentials_response.to_json)
155+
end
156+
157+
before do
158+
MultiAuth.config "twitter", twitter_consumer_key, twitter_consumer_secret
159+
end
160+
161+
describe "GET new" do
162+
before { stub_twitter_authorize_request }
163+
164+
it "redirects to twitter authorize uri" do
165+
get "/oauth/new?provider=twitter"
166+
location = response.headers["Location"]
167+
expect(location.includes? "https://api.twitter.com/oauth/authorize").to be_true
168+
end
169+
end
170+
171+
describe "GET authenticate" do
172+
before { stub_access_token_request }
173+
before { stub_twitter_verify_credentials_request }
174+
before { Announcement.clear; User.clear }
175+
176+
let(:request_body) { HTTP::Params.encode(twitter_authenticate_request) }
177+
178+
context "when signed in" do
179+
before { login_as user }
180+
181+
it "saves a twitter handle" do
182+
get "/oauth/twitter", body: request_body
183+
u = User.find(user.id)
184+
expect(u.try &.handle).to eq twitter_verify_credentials_response[:screen_name]
185+
end
186+
187+
it "redirects to /me" do
188+
get "/oauth/twitter", body: request_body
189+
expect(response).to redirect_to "/me"
190+
end
191+
end
192+
193+
context "when not signed in" do
194+
it "redirects to /" do
195+
get "/oauth/twitter", body: request_body
196+
expect(response).to redirect_to "/"
197+
end
198+
end
199+
end
200+
end
201+
end
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
require "./spec_helper"
2+
3+
describe SessionController do
4+
describe "DELETE destroy" do
5+
let(:user) { user(login: "Bro").tap &.save }
6+
before { login_as user }
7+
8+
it "signs out user" do
9+
delete "/sessions"
10+
expect(session["user_id"]).to be_nil
11+
end
12+
13+
it "does not sign out user if csrf is invalid" do
14+
delete "/sessions", body: "_csrf=invalid-token"
15+
expect(response.status_code).to eq 403
16+
expect(session["user_id"]).not_to be_nil
17+
end
18+
19+
it "redirects to root url" do
20+
delete "/sessions"
21+
expect(response.status_code).to eq 302
22+
expect(response).to redirect_to "/"
23+
end
24+
end
25+
end

0 commit comments

Comments
 (0)