The catalyst-action-rest branch from shlevy/hydra was an exploration of using Catalyst::Action::REST to create a JSON API for hydra. This commit merges in the best bits from that experiment, with the goal that further API endpoints can be added incrementally. In addition to migrating more endpoints, there is potential for improvement in what's already been done: * The web interface can be updated to use the same non-GET endpoints as the JSON interface (using x-tunneled-method) instead of having a separate endpoint * The web rendering should use the $c->stash->{resource} data structure where applicable rather than putting the same data in two places in the stash * Which columns to render for each endpoint is a completely debatable question * Hydra::Component::ToJSON should turn has_many relations that have strings as their primary keys into objects instead of arrays Fixes NixOS/hydra#98 Signed-off-by: Shea Levy <shea@shealevy.com>
273 lines
7.3 KiB
Perl
273 lines
7.3 KiB
Perl
package Hydra::Controller::User;
|
|
|
|
use utf8;
|
|
use strict;
|
|
use warnings;
|
|
use base 'Hydra::Base::Controller::REST';
|
|
use Crypt::RandPasswd;
|
|
use Digest::SHA1 qw(sha1_hex);
|
|
use Hydra::Helper::Nix;
|
|
use Hydra::Helper::CatalystUtils;
|
|
|
|
|
|
__PACKAGE__->config->{namespace} = '';
|
|
|
|
|
|
sub login :Local :Args(0) :ActionClass('REST::ForBrowsers') { }
|
|
|
|
sub login_GET {
|
|
my ($self, $c) = @_;
|
|
|
|
my $baseurl = $c->uri_for('/');
|
|
my $referer = $c->request->referer;
|
|
$c->session->{referer} = $referer if defined $referer && $referer =~ m/^($baseurl)/;
|
|
|
|
$c->stash->{template} = 'login.tt';
|
|
}
|
|
|
|
sub login_POST {
|
|
my ($self, $c) = @_;
|
|
|
|
my $username;
|
|
my $password;
|
|
|
|
$username = $c->stash->{params}->{username};
|
|
$password = $c->stash->{params}->{password};
|
|
|
|
if ($username && $password) {
|
|
if ($c->authenticate({username => $username, password => $password})) {
|
|
if ($c->request->looks_like_browser) {
|
|
backToReferer($c);
|
|
} else {
|
|
currentUser_GET($self, $c);
|
|
}
|
|
} else {
|
|
$self->status_forbidden($c, message => "Bad username or password.");
|
|
if ($c->request->looks_like_browser) {
|
|
login_GET($self, $c);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
sub logout :Local :Args(0) :ActionClass('REST::ForBrowsers') { }
|
|
|
|
sub logout_POST {
|
|
my ($self, $c) = @_;
|
|
$c->logout;
|
|
if ($c->request->looks_like_browser) {
|
|
$c->response->redirect($c->request->referer || $c->uri_for('/'));
|
|
} else {
|
|
$self->status_no_content($c);
|
|
}
|
|
}
|
|
|
|
sub logout_GET {
|
|
# Probably a better way to do this
|
|
my ($self, $c) = @_;
|
|
logout_POST($self, $c);
|
|
}
|
|
|
|
|
|
sub captcha :Local Args(0) {
|
|
my ($self, $c) = @_;
|
|
$c->create_captcha();
|
|
}
|
|
|
|
|
|
sub isValidPassword {
|
|
my ($password) = @_;
|
|
return length($password) >= 6;
|
|
}
|
|
|
|
|
|
sub setPassword {
|
|
my ($user, $password) = @_;
|
|
$user->update({ password => sha1_hex($password) });
|
|
}
|
|
|
|
|
|
sub register :Local Args(0) {
|
|
my ($self, $c) = @_;
|
|
|
|
die "Not implemented!\n";
|
|
|
|
$c->stash->{template} = 'user.tt';
|
|
$c->stash->{create} = 1;
|
|
return if $c->request->method ne "POST";
|
|
|
|
my $userName = trim $c->req->params->{username};
|
|
my $fullName = trim $c->req->params->{fullname};
|
|
my $password = trim $c->req->params->{password};
|
|
$c->stash->{username} = $userName;
|
|
$c->stash->{fullname} = $fullName;
|
|
|
|
sub fail {
|
|
my ($c, $msg) = @_;
|
|
$c->stash->{errorMsg} = $msg;
|
|
}
|
|
|
|
return fail($c, "You did not enter the correct digits from the security image.")
|
|
unless $c->validate_captcha($c->req->param('captcha'));
|
|
|
|
return fail($c, "Your user name is invalid. It must start with a lower-case letter followed by lower-case letters, digits, dots or underscores.")
|
|
if $userName !~ /^$userNameRE$/;
|
|
|
|
return fail($c, "Your user name is already taken.")
|
|
if $c->find_user({ username => $userName });
|
|
|
|
return fail($c, "Your must specify your full name.") if $fullName eq "";
|
|
|
|
return fail($c, "You must specify a password of at least 6 characters.")
|
|
unless isValidPassword($password);
|
|
|
|
return fail($c, "The passwords you specified did not match.")
|
|
if $password ne trim $c->req->params->{password2};
|
|
|
|
txn_do($c->model('DB')->schema, sub {
|
|
my $user = $c->model('DB::Users')->create(
|
|
{ username => $userName
|
|
, fullname => $fullName
|
|
, password => "!"
|
|
, emailaddress => "",
|
|
});
|
|
setPassword($user, $password);
|
|
});
|
|
|
|
unless ($c->user_exists) {
|
|
$c->authenticate({username => $userName, password => $password})
|
|
or error($c, "Unable to authenticate the new user!");
|
|
}
|
|
|
|
$c->flash->{successMsg} = "User <tt>$userName</tt> has been created.";
|
|
backToReferer($c);
|
|
}
|
|
|
|
|
|
sub currentUser :Path('/current-user') :ActionClass('REST') { }
|
|
|
|
sub currentUser_GET {
|
|
my ($self, $c) = @_;
|
|
|
|
requireLogin($c) if !$c->user_exists;
|
|
|
|
$self->status_ok(
|
|
$c,
|
|
entity => $c->model('DB::Users')->find({ 'me.username' => $c->user->username}, {
|
|
columns => [ "me.fullname", "me.emailaddress", "me.username", "userroles.role" ]
|
|
, join => [ "userroles" ]
|
|
, collapse => 1
|
|
})
|
|
);
|
|
}
|
|
|
|
|
|
sub user :Chained('/') PathPart('user') CaptureArgs(1) {
|
|
my ($self, $c, $userName) = @_;
|
|
|
|
requireLogin($c) if !$c->user_exists;
|
|
|
|
error($c, "You do not have permission to edit other users.")
|
|
if $userName ne $c->user->username && !isAdmin($c);
|
|
|
|
$c->stash->{user} = $c->model('DB::Users')->find($userName)
|
|
or notFound($c, "User $userName doesn't exist.");
|
|
}
|
|
|
|
|
|
sub deleteUser {
|
|
my ($self, $c, $user) = @_;
|
|
my ($project) = $c->model('DB::Projects')->search({ owner => $user->username });
|
|
error($c, "User " . $user->username . " is still owner of project " . $project->name . ".")
|
|
if defined $project;
|
|
$c->logout() if $user->username eq $c->user->username;
|
|
$user->delete;
|
|
}
|
|
|
|
|
|
sub edit :Chained('user') :Args(0) :ActionClass('REST::ForBrowsers') { }
|
|
|
|
sub edit_GET {
|
|
my ($self, $c) = @_;
|
|
|
|
my $user = $c->stash->{user};
|
|
|
|
$c->stash->{template} = 'user.tt';
|
|
|
|
$c->session->{referer} = $c->request->referer if !defined $c->session->{referer};
|
|
|
|
$c->stash->{fullname} = $user->fullname;
|
|
|
|
$c->stash->{emailonerror} = $user->emailonerror;
|
|
}
|
|
|
|
sub edit_POST {
|
|
my ($self, $c) = @_;
|
|
|
|
my $user = $c->stash->{user};
|
|
|
|
$c->stash->{template} = 'user.tt';
|
|
|
|
$c->session->{referer} = $c->request->referer if !defined $c->session->{referer};
|
|
|
|
if (($c->stash->{params}->{submit} // "") eq "delete") {
|
|
deleteUser($self, $c, $user);
|
|
backToReferer($c);
|
|
}
|
|
|
|
if (($c->stash->{params}->{submit} // "") eq "reset-password") {
|
|
$c->stash->{json} = {};
|
|
error($c, "No email address is set for this user.")
|
|
unless $user->emailaddress;
|
|
my $password = Crypt::RandPasswd->word(8,10);
|
|
setPassword($user, $password);
|
|
sendEmail($c,
|
|
$user->emailaddress,
|
|
"Hydra password reset",
|
|
"Hi,\n\n".
|
|
"Your password has been reset. Your new password is '$password'.\n\n".
|
|
"You can change your password at " . $c->uri_for($self->action_for('edit'), [$user->username]) . ".\n\n".
|
|
"With regards,\n\nHydra.\n"
|
|
);
|
|
return;
|
|
}
|
|
|
|
my $fullName = trim $c->stash->{params}->{fullname};
|
|
|
|
txn_do($c->model('DB')->schema, sub {
|
|
|
|
error($c, "Your must specify your full name.") if $fullName eq "";
|
|
|
|
$user->update(
|
|
{ fullname => $fullName
|
|
, emailonerror => $c->stash->{params}->{"emailonerror"} ? 1 : 0
|
|
});
|
|
|
|
my $password = $c->stash->{params}->{password} // "";
|
|
if ($password ne "") {
|
|
error($c, "You must specify a password of at least 6 characters.")
|
|
unless isValidPassword($password);
|
|
error($c, "The passwords you specified did not match.")
|
|
if $password ne trim $c->stash->{params}->{password2};
|
|
setPassword($user, $password);
|
|
}
|
|
|
|
if (isAdmin($c)) {
|
|
$user->userroles->delete_all;
|
|
$user->userroles->create({ role => $_})
|
|
foreach paramToList($c, "roles");
|
|
}
|
|
|
|
});
|
|
|
|
if ($c->request->looks_like_browser) {
|
|
backToReferer($c);
|
|
} else {
|
|
$self->status_no_content($c);
|
|
}
|
|
}
|
|
|
|
|
|
1;
|