Skip to content

Instantly share code, notes, and snippets.

@s1037989
Created June 16, 2016 17:11
Show Gist options
  • Save s1037989/066324f5efe2eb83141f332d1ce3d054 to your computer and use it in GitHub Desktop.
Save s1037989/066324f5efe2eb83141f332d1ce3d054 to your computer and use it in GitHub Desktop.
Ama OAuth
package TimeClock::Model::OAuth2;
use Mojo::Base -base;
use UUID::Tiny ':std';
has 'pg';
sub store {
my $self = shift;
if ( $#_ == 1 ) {
#warn "Store Form #1";
my ($id, $provider) = @_;
my $r = $self->pg->db->query('select id from providers where id = ? and provider = ?', $id, $provider)->hash;
return ref $r ? $r->{id} : undef;
} elsif ( $#_ == 0 ) {
#warn "Store Form #2";
my ($provider_id) = @_;
my $r = $self->pg->db->query('select id from providers where provider_id = ?', $provider_id)->hash;
return ref $r ? $r->{id} : uuid_to_string(create_uuid(UUID_V4));
} elsif ( $#_ > 1 ) {
#warn "Store Form #3";
my ($id, $provider, $json, $mapped) = @_;
unless ( $self->pg->db->query('select id from users where id = ?', $id)->rows ) {
$self->pg->db->query('insert into users (id, email, first_name, last_name) values (?, ?, ?, ?)', $id, $mapped->{email}, $mapped->{first_name}, $mapped->{last_name});
}
unless ( $self->pg->db->query('select id from providers where provider_id = ?', $mapped->{id})->rows ) {
$self->pg->db->query('insert into providers (id, provider_id, provider) values (?, ?, ?)', $id, $mapped->{id}, $provider);
}
} else {
#warn "Store Form Unknown";
}
}
sub find { shift->pg->db->query('select * from users where id = ?', shift)->hash }
1;
package Mojolicious::Plugin::OAuth2Accounts;
use Mojo::Base 'Mojolicious::Plugin';
our $VERSION = '0.01';
has providers => sub {
return {
mocked => {
args => {
scope => 'user_about_me email',
},
fetch_user_url => '/mocked/me?access_token=%token%',
map => {
error => '/err/0',
id => '/i',
email => '/e',
first_name => '/f',
last_name => '/l',
},
},
facebook => {
args => {
scope => 'user_about_me email',
},
fetch_user_url => 'https://graph.facebook.com/v2.6/me?access_token=%token%',
map => {
error => '/error/message',
id => '/id',
email => '/email',
name => '/name',
first_name => '/first_name',
last_name => '/last_name',
},
},
google => {
args => {
scope => 'profile email' ,
},
fetch_user_url => 'https://www.googleapis.com/plusDomains/v1/people/me?access_token=%token%',
map => {
error => '/error/message',
id => '/id',
email => '/emails/value',
first_name => '/name/givenName',
last_name => '/name/familyName',
},
},
}
};
sub register {
my ($self, $app, $config) = @_;
my $oauth2_config = {};
my $providers = $self->providers;
foreach my $provider (keys %{$config->{providers}}) {
if (exists $providers->{$provider}) {
foreach my $key (keys %{$config->{providers}->{$provider}}) {
$providers->{$provider}->{$key} = $config->{providers}->{$provider}->{$key};
}
}
else {
$providers->{$provider} = $config->{providers}->{$provider};
}
}
$self->providers($providers);
$app->plugin("OAuth2" => { fix_get_token => 1, %{$config->{providers}} });
$app->routes->get('/logout' => sub {
my $c = shift;
my $token = $c->session('token') || {};
delete $c->session->{$_} foreach keys %{$c->session};
$token->{$_} = {} foreach keys %$token;
$c->session(token => $token);
$c->redirect_to($config->{on_logout});
});
$app->routes->get('/login/:provider' => {provider => ''} => sub {
my $c = shift;
return $c->render($c->session('id') ? 'logout' : 'login') unless $c->param('provider');
return $c->redirect_to('connectprovider', {provider => $c->param('provider')}) unless $c->session('id');
$c->redirect_to($config->{on_success});
})->name('login');
$app->routes->get("/mocked/me" => sub {
my $c = shift;
my $access_token = $c->param('access_token');
return $c->render(json => {err => ['Invalid access token']}) unless $access_token eq 'fake_token';
$c->render(json => { i => 123, f => 'a', l => 'a', e => '[email protected]' });
});
$app->routes->get("/connect/:provider" => sub {
my $c = shift;
$c->session('token' => {}) unless $c->session('token');
my $provider = $c->param('provider');
my $token = $c->session('token');
my ($success, $error, $connect) = ($config->{on_success}, $config->{on_error}, $config->{on_connect});
my ($args, $fetch_user_url, $map) = ($self->providers->{$provider}->{args}, $self->providers->{$provider}->{fetch_user_url}, {%{$self->providers->{$provider}->{map}}});
$c->delay(
sub {
my $delay = shift;
# Only get the token from $provider if the current one isn't expired
if ( $token->{$provider} && $token->{$provider}->{access_token} && $token->{$provider}->{expires_at} && time < $token->{$provider}->{expires_at} ) {
my $cb = $delay->begin;
$c->$cb(undef, $token->{$provider});
} else {
my $args = {redirect_uri => $c->url_for('connectprovider', {provider => $provider})->userinfo(undef)->to_abs, %$args};
$args->{redirect_uri} =~ s/^http/https/;
$c->oauth2->get_token($provider => $args, $delay->begin);
}
},
sub {
my ($delay, $err, $data) = @_;
# If already connected to $provider, no reason to go through this again
# All this does is pull down basic info / email and store locally
return $c->redirect_to($success) if $connect->($c, $c->session('id'), $provider); # on_connect Form #1
unless ( $data->{access_token} ) {
$c->flash(error => "Could not obtain access token: $err");
return $c->redirect_to($error);
}
$token->{$provider} = $data;
$token->{$provider}->{expires_at} = time + ($token->{$provider}->{expires_in}||3600);
$c->session(token => $token);
$c->ua->get($self->_fetch_user_url($fetch_user_url, $token->{$provider}->{access_token}), sub {
my ($ua, $tx) = @_;
return $c->reply->exception("No JSON response") unless $tx->res->json;
my $json = Mojo::JSON::Pointer->new($tx->res->json);
if ( my $error_message = $json->get(delete $map->{error}) ) {
$c->flash(error => $error_message);
return $c->redirect_to($error);
}
$c->session(id => $connect->($c, $json->get($map->{id}))) unless $c->session('id'); # on_connect Form #2
$connect->($c, $c->session('id'), $provider, $tx->res->json, {map { $_ => $json->get($map->{$_}) } keys %$map}); # on_connect Form #3
$c->redirect_to($success);
});
},
);
});
}
sub _fetch_user_url {
my ($self, $fetch_user_url, $token) = @_;
$fetch_user_url =~ s/%token%/$token/g;
return $fetch_user_url;
}
1;
__END__
=encoding utf8
=head1 NAME
Mojolicious::Plugin::OAuth2Accounts - Mojolicious Plugin
=head1 SYNOPSIS
# Mojolicious
$self->plugin('OAuth2Accounts');
# Mojolicious::Lite
plugin 'OAuth2Accounts';
=head1 DESCRIPTION
L<Mojolicious::Plugin::OAuth2Accounts> is a L<Mojolicious> plugin.
=head1 METHODS
L<Mojolicious::Plugin::OAuth2Accounts> inherits all methods from
L<Mojolicious::Plugin> and implements the following new ones.
=head2 register
$plugin->register(Mojolicious->new);
Register plugin in L<Mojolicious> application.
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<http://mojolicio.us>.
=cut
use Mojolicious::Lite;
#use lib '/var/mojo/lib/Mojolicious-Plugin-OAuth2Accounts/lib';
use lib 'lib';
use Mojo::Pg;
use TimeClock::Model::OAuth2;
use TimeClock::Model::Timeclock;
my $config = plugin 'Config';
helper 'pg' => sub { state $pg = Mojo::Pg->new(shift->config('pg')) };
helper 'model.oauth2' => sub { state $users = TimeClock::Model::OAuth2->new(pg => shift->pg) };
helper 'model.timeclock' => sub { state $timeclock = TimeClock::Model::Timeclock->new(pg => shift->pg) };
app->sessions->default_expiration(86400*365*10);
app->pg->migrations->from_data->migrate;
plugin "OAuth2Accounts" => {
on_logout => '/',
on_success => 'timeclock',
on_error => 'login',
on_connect => sub { shift->model->oauth2->store(@_) },
providers => $config->{oauth2},
};
get '/' => sub {
my $c = shift;
return $c->redirect_to('login', {provider => ''}) unless $c->session('id');
$c->stash(user => $c->model->oauth2->find($c->session('id')));
$c->stash(timeclock => $c->model->timeclock);
} => 'timeclock';
post '/' => sub {
my $c = shift;
return $c->reply->not_found unless $c->session('id');
if ( $c->param('clock') eq 'in' ) {
$c->model->timeclock->clock_in($c->session('id'), $c->param('lat'), $c->param('lon'));
} elsif ( $c->param('clock') eq 'out' ) {
$c->model->timeclock->clock_out($c->session('id'), $c->param('lat'), $c->param('lon'));
}
$c->redirect_to('timeclock');
} => 'timeclock';
get '/status' => sub {
my $c = shift;
return $c->redirect_to('timeclock') unless $c->session('id') && $c->model->oauth2->find($c->session('id'))->{admin};
$c->stash(timeclock => $c->model->timeclock);
};
get '/history/:user' => sub {
my $c = shift;
return $c->redirect_to('timeclock') unless $c->session('id');
return $c->reply->not_found unless $c->param('user') eq $c->session('id') || $c->model->oauth2->find($c->session('id'))->{admin};
$c->stash(user => $c->param('user'));
$c->stash(timeclock => $c->model->timeclock);
};
get '/pay/:user/:ids' => sub {
my $c = shift;
return $c->redirect_to('timeclock') unless $c->session('id') && $c->model->oauth2->find($c->session('id'))->{admin};
$c->model->timeclock->pay(split /,/, $c->param('ids'));
$c->stash(user => $c->param('user'));
$c->stash(timeclock => $c->model->timeclock);
$c->redirect_to('historyuser', {user => $c->param('user')});
} => 'payuser';
app->start;
__DATA__
@@ layouts/default.html.ep
<!DOCTYPE html>
<html>
<head>
<style>
* { font-size: 48px }
</style>
<title><%= title %></title>
</head>
<body><%= content %></body>
</html>
@@ historyuser.html.ep
% layout 'default';
% title 'User History';
% my $week;
<%= link_to "All user status" => 'status' %><hr />
% my $user_tc = $timeclock->user($user);
<%= "$user_tc->{first_name} $user_tc->{last_name}" %><br />
<table>
<tr><th>week</th><th>Sunday</th><th>Monday</th><th>Tuesday</th><th>Wednesday</th><th>Thursday</th><th>Friday</th><th>Saturday</th><th>Total</th></tr>
% my $history = $timeclock->history($user);
% foreach my $week ( sort { $b cmp $a } keys %$history ) {
<tr>
<td><%= $week %>
% my $week_time;
% my $unpaid;
% my @unpaid;
% foreach my $dow ( qw/0 1 2 3 4 5 6/ ) {
<td>
% my $day_time;
% foreach my $e ( @{$history->{$week}->{$dow}} ) {
% $day_time = $day_time ? $day_time->add_duration($e->{duration}->clone) : $e->{duration}->clone;
% $week_time = $week_time ? $week_time->add_duration($e->{duration}->clone) : $e->{duration}->clone;
% $unpaid = $unpaid ? $unpaid->add_duration($e->{duration}->clone) : $e->{duration}->clone unless $e->{paid};
% push @unpaid, $e->{id} if !$e->{paid};
<%= link_to($e->{time_in}->hms => "https://www.google.com/maps/place//\@$e->{time_in_lat},$e->{time_in_lon},17z/data=!3m1!4b1!4m2!3m1!1s0x0:0x0") %> - <%= $e->{time_out} ? link_to($e->{time_out}->hms => "https://www.google.com/maps/place//\@$e->{time_in_lat},$e->{time_in_lon},17z/data=!3m1!4b1!4m2!3m1!1s0x0:0x0") : 'Active' %> (<%= $e->{paid} ? $timeclock->duration($e->{duration}) : link_to $timeclock->duration($e->{duration}) => 'payuser', {user => $user, ids => $e->{id}} %>)<br />
% }
% if ( $day_time ) {
<b><%= $timeclock->duration($day_time) %></b>
% }
</td>
% }
<td>
% if ( $unpaid ) {
<b><%= link_to $timeclock->duration($unpaid, 1) => 'payuser', {user => $user, ids => join(',', @unpaid)} %></b><br />
% }
% if ( $week_time ) {
<b><%= $timeclock->duration($week_time, 1) %></b><br />
% }
</td>
</tr>
% }
</table>
@@ login.html.ep
<%= link_to 'Login with facebook' => 'login', {provider => 'facebook'} %><br />
<%= link_to 'Login with google' => 'login', {provider => 'google'} %><br />
@@ logout.html.ep
<%= link_to 'Logout' => 'logout' %><br />
@@ status.html.ep
% layout 'default';
% title 'All users';
<%= link_to 'My timeclock' => 'timeclock' %><hr />
% foreach my $user ( @{$timeclock->users} ) {
% my $status = $timeclock->status($user->{id});
% my $user_tc = $timeclock->user($user->{id});
Name: <%= link_to "$user_tc->{first_name} $user_tc->{last_name}" => 'historyuser', {user => $user->{id}} %><br />
Status: <%== $status ? "Active since ".link_to($status->{time_in}->datetime => "https://www.google.com/maps/place//\@$status->{time_in_lat},$status->{time_in_lon},17z/data=!3m1!4b1!4m2!3m1!1s0x0:0x0")." (".$timeclock->duration($status->{duration}).")" : 'Not active' %><hr />
% }
@@ timeclock.html.ep
% layout 'default';
% title 'My Timeclock';
<script src="http://maps.googleapis.com/maps/api/js?key=AIzaSyDKw1I9ZlI-piCBp2zXSuviBDVRjju-aYI&sensor=true&libraries=adsense"></script>
<script src="http://ctrlq.org/common/js/jquery.min.js"></script>
<script>
if ( "geolocation" in navigator ) {
navigator.geolocation.getCurrentPosition(function(position){
var lat = position.coords.latitude.toFixed(5);
var lon = position.coords.longitude.toFixed(5);
console.log(lat, lon);
$('#lat').attr('value', lat);
$('#lon').attr('value', lon);
$('#div_lat').text(lat);
$('#div_lon').text(lon);
}, null, {enableHighAccuracy: true, timeout: 5000, maximumAge: 1000});
}
</script>
% if ( $user->{admin} ) {
<%= link_to Admin => 'status' %><hr />
% }
Name: <%= link_to $user->{id} => 'historyuser', {user => $user->{id}} %><br />
<%= dumper $user %>
%= form_for timeclock => (method => 'POST') => begin
%= hidden_field 'lat' => '', id => 'lat'
%= hidden_field 'lon' => '', id => 'lon'
% if ( my $status = $timeclock->status(session 'id') ) {
<%= $status->{time_in} %> (<%= $timeclock->duration($status->{duration}) %>)<br />
%= hidden_field clock => 'out'
%= submit_button 'Clock out'
% } else {
Not active <br />
%= hidden_field clock => 'in'
%= submit_button 'Clock in'
% }
% end
@@ migrations
-- 1 up
create table if not exists users (
id text primary key,
email text,
first_name text,
last_name text,
admin integer,
created timestamptz not null default now()
);
create table if not exists providers (
id text,
provider_id text,
provider text,
created timestamptz not null default now(),
PRIMARY KEY (id, provider_id, provider)
);
create table if not exists timeclock (
id serial primary key,
user_id text,
time_in timestamptz,
time_out timestamptz,
time_in_lat text,
time_in_lon text,
time_out_lat text,
time_out_lon text,
paid integer
);
CREATE OR REPLACE FUNCTION round_duration(TIMESTAMPTZ, TIMESTAMPTZ)
RETURNS INTERVAL AS $$
SELECT date_trunc('hour', age(case when $2 is not null then $2 else now() end, $1)) + INTERVAL '15 min' * ROUND(date_part('minute', age(case when $2 is not null then $2 else now() end, $1)) / 15.0)
$$ LANGUAGE SQL;
-- 1 down
drop table if exists providers;
drop table if exists users;
drop table if exists timeclock;
-- 2 up
alter table users add column disable timestamptz;
-- 2 down
alter table users drop column disable;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment