Maxim Sokhatsky 11 лет назад
Сommit
20a25637cc
10 измененных файлов с 436 добавлено и 0 удалено
  1. 32 0
      README.md
  2. 11 0
      include/avz.hrl
  3. 31 0
      priv/facebook_sdk.dtl
  4. 31 0
      priv/google_sdk.dtl
  5. 8 0
      rebar.config
  6. 27 0
      src/avz.erl
  7. 46 0
      src/facebook.erl
  8. 66 0
      src/github.erl
  9. 38 0
      src/google.erl
  10. 146 0
      src/twitter.erl

+ 32 - 0
README.md

@@ -0,0 +1,32 @@
+AVZ Login System for N2O
+========================
+
+Authentication methods N2O sites. Supports both JavaScript based
+logins (like Google and Facebook) and redirect based OAuth logins (Twitter, Github)
+in a sane and simple manner. Compatible with but not limited to Nitrogen and N2O.
+
+Supported Methods
+-----------------
+
+* Twitter
+* Google
+* Facebook
+* Github
+
+API
+---
+
+    sdk/0                % JavaScript for page embedding for JavaScript based login methods
+    login_button/0       % HTML Button for page embedding
+    event/1              % Page Event for HTTP redirect based login methods
+    api_event/0          % Page Event for JavaScript based login methods
+    callback/0           % Callback part of HTTP redirect based login methods
+    registration_data/3  % Process Parameters
+
+Credits
+-------
+
+* Andrii Zadorozhnii
+* Maxim Sokhatsky
+
+OM A HUM

+ 11 - 0
include/avz.hrl

@@ -0,0 +1,11 @@
+-record(struct, {lst=[]}).
+-define(AFTER_LOGIN, "/account").
+-define(LOGIN_PAGE, "/login").
+-define(METHODS, [facebook,google,github,twitter]).
+-define(API,[sdk/0,               % JavaScript for page embedding for JavaScript based login methods
+             login_button/0,      % HTML Button for page embedding
+             event/1,             % Page Event for HTTP redirect based login methods
+             api_event/3,         % Page Event for JavaScript based login methods
+             callback/0,          % Callback part of HTTP redirect based login methods
+             registration_data/3  % Process Parameters
+            ]).

+ 31 - 0
priv/facebook_sdk.dtl

@@ -0,0 +1,31 @@
+<script>
+window.fbAsyncInit = function() {
+  FB.init({ appId: '{{appid}}', channelUrl: ' {{channelUrl}}', status: true, cookie: true, xfbml: true, oauth: true });
+
+  FB.getLoginStatus(function(response) {
+    if(setFbIframe){
+      var inIframe= top!=self;
+      setFbIframe(inIframe);
+      if(inIframe && response.status == 'connected' && fbLogin)
+        FB.api("/me?fields=id,username,first_name,last_name,email,birthday", function(response){ fbLogin(response);});
+    }
+  });
+};
+
+function fb_login(){
+  FB.getLoginStatus(function(response){
+    if(response.status == 'connected'){
+      if(fbLogin) FB.api("/me?fields=id,username,first_name,last_name,email,birthday", function(response){fbLogin(response);});
+    } else FB.login(function(r){
+        if(r.authResponse && fbLogin) FB.api("/me?fields=id,username,first_name,last_name,email,birthday", function(response){fbLogin(response);});
+      }, {scope: 'email,user_birthday'});
+  });
+}
+
+(function(d){
+  var js, id = 'facebook-jssdk', ref = d.getElementsByTagName('script')[0];
+  if (d.getElementById(id)) {return;}
+  js = d.createElement('script'); js.id = id; js.async = true; js.src = "//connect.facebook.net/en_US/all.js?v=2";
+  ref.parentNode.insertBefore(js, ref);
+}(document));
+</script>

+ 31 - 0
priv/google_sdk.dtl

@@ -0,0 +1,31 @@
+<script type="text/javascript">
+function plusoneCallback(auth){
+  if(!auth['g-oauth-window']) {
+    console.log('skip autologin');
+  } else if(auth['access_token']){
+    gapi.client.load('oauth2', 'v2', function(){
+      gapi.client.oauth2.userinfo.get().execute(function(oauthResp){
+        gapi.client.load('plus', 'v1', function(){
+          gapi.client.plus.people.get({'userId':'me'}).execute(function(profileResp){
+            if(plusLogin)plusLogin($.extend({}, gapi.auth.getToken(), oauthResp, profileResp, {'g-oauth-window': 'defined'})); }); }); }); });
+  }else if(auth['error']){
+        console.log('error');
+  }
+}
+
+function render(){
+  gapi.signin.render('{{loginbtnid}}', {
+    'callback':'plusoneCallback',
+    'clientid':'{{clientid}}',
+    'cookiepolicy': '{{cookiepolicy}}',
+    'requestvisibleactions':'http://schemas.google.com/AddActivity',
+    'scope':'https://www.googleapis.com/auth/plus.login https://www.googleapis.com/auth/userinfo.email '
+  });
+}
+
+(function() {
+  var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
+  po.src = 'https://apis.google.com/js/client:plusone.js?parsetags=explicit&onload=render';
+  var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
+})();
+</script>

+ 8 - 0
rebar.config

@@ -0,0 +1,8 @@
+{deps_dir, ["deps"]}.
+{deps, [n2o,kvs,{erlydtl,".*",{git,"git://github.com/voxoz/erlydtl.git","HEAD"}}]}.
+{erlydtl_opts, [
+    {doc_root,   "priv"},
+    {out_dir,    "ebin"},
+    {source_ext, ".dtl"},
+    {module_ext, "_view"}
+]}.

+ 27 - 0
src/avz.erl

@@ -0,0 +1,27 @@
+-module(avz).
+-author('Maxim Sokhatsky').
+-compile(export_all).
+-include_lib("avz/include/avz.hrl").
+-include_lib("n2o/include/wf.hrl").
+-include_lib("kvs/include/users.hrl").
+
+callback(Methods) -> [ M:callback() || M <- Methods].
+sdk(Methods) -> [ M:sdks() || M <- Methods].
+
+event(init) -> [];
+event(logout) -> wf:user(undefined), wf:redirect(?LOGIN_PAGE);
+event(login) -> wf:redirect(?AFTER_LOGIN);
+event(to_login) -> wf:redirect(?LOGIN_PAGE);
+event({Method,Event}) -> Method:event({Method,Event});
+event(Ev) ->  error_logger:info_msg("Page Event ~p",[Ev]).
+
+api_event(plusLogin, Args, Term) -> google:api_event(plusLogin, Args, Term);
+api_event(fbLogin, Args, Term)   -> facebook:api_event(fbLogin, Args, Term);
+api_event(Name, Args, Term)      -> error_logger:info_msg("Unknown API event: ~p ~p ~p",[Name, Args, Term]).
+
+login_user(User) -> wf:user(User), wf:redirect(?AFTER_LOGIN).
+login(Key, Args)-> case Args of [{error, E}|_Rest] -> error_logger:info_msg("oauth error: ~p", [E]);
+    _ -> case kvs:get(user,Key:email_prop(Args,Key)) of
+              {ok,Existed} -> {Id, RegData} = Key:registration_data(Args, Key, Existed), login_user(RegData);
+              {error,_} -> {Id, RegData} = Key:registration_data(Args, Key, #user{}),
+                  kvs:put(RegData), login_user(RegData) end end.

+ 46 - 0
src/facebook.erl

@@ -0,0 +1,46 @@
+-module(facebook).
+-author('Andrii Zadorozhnii').
+-include_lib("avz/include/avz.hrl").
+-include_lib("n2o/include/wf.hrl").
+-include_lib("kvs/include/users.hrl").
+-export(?API).
+
+-define(HTTP_ADDRESS, case application:get_env(web, http_address) of {ok, A} -> A; _ -> "" end).
+-define(FB_APP_ID, case application:get_env(web, fb_id) of {ok, Id} -> Id; _-> "" end).
+
+event({facebook,_}) -> ok.
+api_event(fbLogin, Args, _Term)-> JSArgs = n2o_json:decode(Args), avz:login(facebook, JSArgs#struct.lst).
+
+registration_data(Props, facebook, Ori)->
+    Id = proplists:get_value(<<"id">>, Props),
+    UserName = binary_to_list(proplists:get_value(<<"username">>, Props)),
+    BirthDay = case proplists:get_value(<<"birthday">>, Props) of
+        undefined -> {1, 1, 1970};
+        BD -> list_to_tuple([list_to_integer(X) || X <- string:tokens(binary_to_list(BD), "/")]) end,
+    error_logger:info_msg("User Ori: ~p",[Ori]),
+    error_logger:info_msg("Props: ~p",[Props]), 
+    { proplists:get_value(<<"id">>, Props), 
+      Ori#user{ display_name = UserName,
+                avatar = "https://graph.facebook.com/" ++ UserName ++ "/picture",
+                email = email_prop(Props, facebook_id),
+                name = proplists:get_value(<<"first_name">>, Props),
+                surname = proplists:get_value(<<"last_name">>, Props),
+                facebook_id = Id,
+                age = {element(3, BirthDay), element(1, BirthDay), element(2, BirthDay)},
+                register_date = erlang:now(),
+                status = ok }}.
+
+email_prop(Props, _) -> binary_to_list(proplists:get_value(<<"email">>, Props)).
+
+login_button() -> #panel{class=["btn-group"], body=
+    #link{id=loginfb, class=[btn, "btn-primary", "btn-large"], 
+        body=[#i{class=["icon-facebook", "icon-large"]}, <<"Facebook">>],
+            actions= "$('#loginfb').on('click', fb_login);" }}.
+
+sdk() ->
+    wf:wire(#api{name=setFbIframe, tag=fb}),
+    wf:wire(#api{name=fbAutoLogin, tag=fb}),
+    wf:wire(#api{name=fbLogin, tag=fb}),
+  [ #panel{id="fb-root"},
+    #dtl{bind_script=false, file="facebook_sdk", ext="dtl", folder="priv/static/js",
+        bindings=[{appid, ?FB_APP_ID},{channelUrl, ?HTTP_ADDRESS ++ "/static/channel.html"} ] } ].

+ 66 - 0
src/github.erl

@@ -0,0 +1,66 @@
+-module(github).
+-author('Andrii Zadorozhnii').
+-include_lib("n2o/include/wf.hrl").
+-include_lib("avz/include/avz.hrl").
+-export(?API).
+
+-define(CLIENT_ID, case application:get_env(web, github_client_id) of {ok, K} -> K;_-> "" end).
+-define(CLIENT_SECRET, case application:get_env(web, github_client_secret) of {ok, S} -> S; _-> "" end).
+-define(OAUTH_URI, "https://github.com/login/oauth").
+-define(AUTHORIZE_URI, ?OAUTH_URI ++ "/authorize").
+-define(ACCESS_TOKEN_URI, ?OAUTH_URI ++ "/access_token").
+-define(API_URI, "https://api.github.com").
+-define(REQ_HEADER, [{"User-Agent", "Erlang Paas"}]).
+
+user(Props) -> api_call("/user", Props).
+
+authorize_url() -> oauth:uri(?AUTHORIZE_URI, [{"client_id", ?CLIENT_ID}, {"state", "state"}]).
+
+get_access_token(Code) ->
+    ReqParams = [{"client_id", ?CLIENT_ID}, {"client_secret", ?CLIENT_SECRET}, {"code", binary_to_list(Code)}],
+    HttpOptions = [{autoredirect, false}],
+    case httpc:request(post, {oauth:uri(?ACCESS_TOKEN_URI, ReqParams), [], "", []}, HttpOptions, []) of
+        {error, _} -> not_authorized;
+        {ok, R = {{"HTTP/1.1",200,"OK"}, _, _}} ->
+              Params = oauth:params_decode(R),
+              case proplists:get_value("error", Params, undefined) of undefined -> Params; _E -> not_authorized end;
+        {ok, _} -> not_authorized end.
+
+api_call(Name, Props) ->
+    Token = [{"access_token", proplists:get_value("access_token", Props)}],
+    case httpc:request(get, {oauth:uri(?API_URI++Name, Token), ?REQ_HEADER}, [], []) of
+         {error, reason} -> api_error;
+         {ok, {HttpResponse, _, Body}} -> 
+                case HttpResponse of {"HTTP/1.1", 200, "OK"} -> n2o_json:decode(Body); _ -> error end;
+         {ok, _} -> api_error end.
+
+sdk() -> [].
+callback() ->
+    Code = wf:q(<<"code">>),
+    State = wf:q(<<"state">>),
+    case wf:user() of
+         undefined when Code =/= undefined andalso State == <<"state">> ->
+            case github:get_access_token(Code) of
+                 not_authorized -> skip;
+                 Props -> UserData = github:user(Props), avz:login(github, UserData#struct.lst) end;
+         _ -> skip end.
+
+registration_data(Props, github, Ori) ->
+    Id = proplists:get_value(<<"id">>, Props),
+    Name = proplists:get_value(<<"name">>, Props),
+    {Id, Ori#user{  username = binary_to_list(proplists:get_value(<<"login">>, Props)),
+                    display_name = Name,
+                    avatar = proplists:get_value(<<"avatar_url">>, Props),
+                    email = email_prop(Props, github_id),
+                    name  = Name,
+                    surname = [],
+                    github_id = Id,
+                    register_date = erlang:now(),
+                    status = ok }}.
+
+login_button() -> #panel{ class=["btn-group"], body=
+    #link{id=github_btn, class=[btn, "btn-large"], 
+        body=[#i{class=["icon-github", "icon-large"]}, <<"Github">>], postback={github,logingithub} }}.
+
+api_event(_,_,_) -> ok.
+event({github,logingithub}) -> wf:redirect(github:authorize_url()).

+ 38 - 0
src/google.erl

@@ -0,0 +1,38 @@
+-module(google).
+-author('Andrii Zadorozhnii').
+-include_lib("avz/include/avz.hrl").
+-include_lib("n2o/include/wf.hrl").
+-include_lib("kvs/include/users.hrl").
+-export(?API).
+-define(GPLUS_CLIENT_ID, case application:get_env(web, gplus_client_id) of {ok, Id} -> Id; _-> "" end).
+-define(GPLUS_COOKIE_POLICY, case application:get_env(web, gplus_cookiepolicy) of {ok, P} -> P; _-> "" end).
+
+api_event(plusLogin, Args, _)-> JSArgs = n2o_json:decode(Args), avz:login(googleplus, JSArgs#struct.lst).
+
+registration_data(Props, googleplus, Ori)->
+    Id = proplists:get_value(<<"id">>, Props),
+    Name = proplists:get_value(<<"name">>, Props),
+    GivenName = proplists:get_value(<<"givenName">>, Name#struct.lst),
+    FamilyName = proplists:get_value(<<"familyName">>, Name#struct.lst),
+    Image = proplists:get_value(<<"image">>, Props),
+    {Id, Ori#user{ display_name = proplists:get_value(<<"displayName">>, Props),
+                   avatar = lists:nth(1,string:tokens(binary_to_list(proplists:get_value(<<"url">>, Image#struct.lst)), "?")),
+                   email = email_prop(Props,googleplus_id),
+                   name = GivenName,
+                   surname = FamilyName,
+                   googleplus_id = Id,
+                   register_date = erlang:now(),
+                   sex = proplists:get_value(gender, Props),
+                   status = ok }};
+
+email_prop(Props, _) -> binary_to_list(proplists:get_value(<<"email">>, Props)).
+
+login_button()-> #panel{id=plusloginbtn, class=["btn-group"], body=
+    #link{class=[btn, "btn-google-plus", "btn-large"], 
+        body=[#i{class=["icon-google-plus", "icon-large"]}, <<"Google">>] }};
+
+sdk() ->
+    wf:wire(#api{name=plusLogin, tag=plus}),
+    #dtl{bind_script=false, file="google_sdk", ext="dtl", folder="priv/static/js",
+        bindings=[{loginbtnid, plusloginbtn},{clientid, ?GPLUS_CLIENT_ID},{cookiepolicy, ?GPLUS_COOKIE_POLICY}]}.
+

+ 146 - 0
src/twitter.erl

@@ -0,0 +1,146 @@
+-module(twitter).
+-author('Andrii Zadorozhnii').
+-include_lib("n2o/include/wf.hrl").
+-include_lib("kvs/include/users.hrl").
+-export(?API).
+-define(CONSUMER_KEY, case application:get_env(web, tw_consumer_key) of {ok, K} -> K;_-> "" end).
+-define(CONSUMER_SECRET, case application:get_env(web, tw_consumer_secret) of {ok, S} -> S; _-> "" end).
+-define(CONSUMER, {?CONSUMER_KEY, ?CONSUMER_SECRET, hmac_sha1}).
+
+registration_data(Props, twitter, Ori)->
+    Id = proplists:get_value(<<"id_str">>, Props),
+    UserName = binary_to_list(proplists:get_value(<<"screen_name">>, Props)),
+    {Id, Ori#user{  username = re:replace(UserName, "\\.", "_", [{return, list}]),
+                    display_name = proplists:get_value(<<"screen_name">>, Props),
+                    avatar = proplists:get_value(<<"profile_image_url">>, Props),
+                    name = proplists:get_value(<<"name">>, Props),
+                    email = email_prop(Props,twitter_id),
+                    surname = [],
+                    twitter_id = Id,
+                    register_date = erlang:now(),
+                    status = ok }}.
+
+callback() ->
+    Token = wf:q(<<"oauth_token">>),
+    Verifier =wf:q(<<"oauth_verifier">>),
+    case wf:user() of
+         undefined ->
+             if (Token /= undefined) andalso ( Verifier/= undefined) ->
+                   case get_access_token(binary_to_list(Token), binary_to_list(Verifier)) of
+                        not_authorized -> skip;
+                        Props -> UserData = show(Props), avz:login(twitter, UserData#struct.lst) end;
+                 true -> skip  end;
+         _ -> skip end.
+
+login_button() -> #panel{class=["btn-group"], body=
+    #link{id=twlogin, class=[btn, "btn-info", "btn-large"], 
+        body=[#i{class=["icon-twitter", "icon-large"]}, <<"Twitter">>], postback={twitter,logintwitter}}}.
+
+sdk() -> [].
+api_event(_,_,_) -> ok.
+event({twitter,logintwitter}) ->
+    case get_request_token() of
+         {RequestToken, _, _} -> wf:redirect(authenticate_url(RequestToken));
+         {error, R} -> error_logger:info_msg("Twitter request failed:", [R]), [] end.
+
+get_request_token()->
+  URL = "https://api.twitter.com/oauth/request_token",
+  case oauth:get(URL, [], ?CONSUMER) of
+    {ok, Response} ->
+      Params = oauth:params_decode(Response),
+      RequestToken = oauth:token(Params),
+      RequestTokenSecret = oauth:token_secret(Params),
+      CallbackConfirmed = proplists:get_value("oauth_callback_confirmed", Params),
+      {RequestToken, RequestTokenSecret, CallbackConfirmed};
+    {error, E}-> {error, E}
+  end.
+
+get_access_token(undefined, undefined)-> not_authorized;
+get_access_token(undefined, _)-> not_authorized;
+get_access_token(_, undefined)-> not_authorized;
+get_access_token(Token, Verifier)->
+  URL = "https://api.twitter.com/oauth/access_token",
+  Signed = oauth:sign("GET", URL, [{"oauth_verifier", Verifier}], ?CONSUMER, Token, ""),
+  {OauthParams, QueryParams} = lists:partition(fun({K, _}) -> lists:prefix("oauth_", K) end, Signed),
+  Request = {oauth:uri(URL, QueryParams), [oauth:header(OauthParams)]},
+  {ok, Response} = httpc:request(get, Request, [{autoredirect, false}], []),
+  case Response of
+    {HttpResponse, _, _}->
+      case HttpResponse of
+        {"HTTP/1.1",200,"OK"}->
+          Params = oauth:params_decode(Response),
+          Params;
+        _ -> not_authorized
+      end;
+    _ -> not_authorized
+  end.
+
+authenticate_url(RequestToken)->
+    oauth:uri("https://api.twitter.com/oauth/authenticate", [{"oauth_token", RequestToken}]).
+authorize_url(RequestToken)->
+    oauth:uri("https://api.twitter.com/oauth/authorize", [{"oauth_token", RequestToken}]).
+
+show(Props)->
+  URI = "https://api.twitter.com/1.1/users/show.json",
+  {ok, Response} = oauth:get(URI, [{"user_id", proplists:get_value("user_id", Props)}, {"include_entities", false}], ?CONSUMER, oauth:token(Props), oauth:token_secret(Props)),
+  case Response of
+    {HttpResponse, _, Body} -> case HttpResponse of {"HTTP/1.1", 200, "OK"} ->  n2o_json:decode(Body); _-> error end;
+    _ -> error
+  end.
+
+service_item()->
+  case nsm_db:get(user, wf:user()) of 
+    {error, notfound} -> wf:redirect("login");
+    {ok, #user{twitter_id=TwitterId}} ->
+      try service_btn(TwitterId) of
+        Btn ->  #li{id=twServiceBtn, class=png, body=Btn}
+      catch
+        _:_ -> []
+      end
+  end.
+
+service_btn(undefined) ->
+  case get_request_token() of
+    {RequestToken, _, _} ->
+      [#image{image="/images/img-52.png"}, #span{body= <<"Twitter">>},
+      #link{class="btn", body=["<span>+</span>", "Add"], url=authorize_url(RequestToken)}];
+    {error, R} -> error_logger:info_msg("Twitter request failed:", [R]), []
+  end;
+service_btn(TwitterId)->
+  case nsm_db:get(twitter_oauth, TwitterId) of
+    {error, notfound}->
+      service_btn(undefined);
+    {ok, #twitter_oauth{token=Token, secret=TokenSecret}} when Token == undefined orelse TokenSecret == undefined ->
+      service_btn(undefined);
+    {ok, #twitter_oauth{}} ->
+      [#image{image="/images/img-52.png"}, #span{body= <<"Twitter">>},
+      #link{class="btn", body=["<span>-</span>", "Del"], postback={delete, twitter}}]
+  end.
+
+delete()->
+  case nsm_db:get(user, wf:user()) of
+    {error, notfound} -> wf:redirect("login");
+    {ok, #user{twitter_id=TwitterId} = User} when TwitterId =/= undefined ->
+      case nsm_db:get(twitter_oauth, TwitterId) of
+        {error, notfound} -> ok;
+        {ok, #twitter_oauth{}} ->
+          nsm_db:put(User#user{twitter_id = undefined}),
+          %nsx_msg:notify(["system", "put"], User#user{twitter_id = undefined}),
+          nsm_sb:delete(twitter_oauth, TwitterId),
+          %nsx_msg:notify(["system", "delete"], {twitter_oauth, TwitterId}),
+          wf:update(twServiceBtn, service_btn(undefined))
+      end;
+    _ -> ok
+  end.
+
+tweet(UserName, Msg)->
+  case nsm_db:get(user, UserName) of
+    {error, notfound} -> fail;
+    {ok, #user{twitter_id=TwitterId}}->
+      case nsm_db:get(twitter_oauth, TwitterId) of
+        {error, notfound} -> fail;
+        {ok, #twitter_oauth{token = AccessToken, secret=AccessTokenSecret}}->
+          URL = "http://api.twitter.com/1.1/statuses/update.json",
+          oauth:post(URL, [{"status", Msg}], ?CONSUMER, AccessToken, AccessTokenSecret)
+    end
+  end.