Make UserManagement like everything else.

This commit is contained in:
Kaitlyn Parkhurst 2021-07-10 21:28:09 -07:00
parent 7ca9fc0b5f
commit 5df89f5057
6 changed files with 93 additions and 396 deletions

View file

@ -43,18 +43,11 @@ sub startup ($self) {
$self->plugin( 'MeshMage::Web::Plugin::MinionTasks' );
$self->plugin( 'MeshMage::Web::Plugin::Helpers' );
# Router
# Standard router.
my $r = $self->routes;
# Handle user login & first account creation.
# Routes:
# /login GET view_login, POST post_login
# /first GET view_first, POST post_first
# /logout GET logout,
$self->plugin( 'MeshMage::Web::Plugin::UserManagement' => { } );
# Ensure that only authenticated users can access routes under
# $auth.
# Create a router chain that ensures the request is from an authenticated
# user.
my $auth = $r->under( '/' => sub ($c) {
# Login via session cookie.
@ -65,14 +58,22 @@ sub startup ($self) {
$c->stash->{person} = $person;
return 1;
}
$c->redirect_to( $c->url_for( 'view_login' ) );
$c->redirect_to( $c->url_for( 'auth_login' ) );
return undef;
}
$c->redirect_to( $c->url_for( 'view_login' ) );
$c->redirect_to( $c->url_for( 'auth_login' ) );
return undef;
});
# Controllers to handle initial user creation, login and logout.
$r->get ('/auth/init' )->to('Auth#init' )->name('auth_init' );
$r->post('/auth/init' )->to('Auth#create_init' )->name('create_auth_init' );
$r->get ('/auth/login' )->to('Auth#login' )->name('auth_login' );
$r->post('/auth/login' )->to('Auth#create_login')->name('create_auth_login');
$r->get ('/auth/logout')->to('Auth#logout' )->name('logout' );
# The /minion stuff is handled here because we needed to place it under $auth.
$self->plugin( 'Minion::Admin' => { route => $auth->under( '/minion' ) } );
# Send requests for / to the dashboard.

View file

@ -0,0 +1,76 @@
package MeshMage::Web::Controller::Auth;
use Mojo::Base 'Mojolicious::Controller', -signatures;
use Try::Tiny;
sub init ($c) {
my $user_count = $c->db->resultset('Person')->count;
if ( $user_count >= 1 ) {
$c->redirect_to( $c->url_for( 'auth_login' ) );
return;
}
}
sub create_init ($c) {
# This code should only run when there are no user accounts, if
# a user account exists, redirect the user to the login panel.
my $user_count = $c->db->resultset('Person')->count;
if ( $user_count >= 1 ) {
$c->redirect_to( $c->url_for( 'view_login' ) );
return;
}
my $person = try {
$c->db->storage->schema->txn_do( sub {
my $person = $c->db->resultset('Person')->create({
email => $c->param('email'),
name => $c->param('name'),
});
$person->new_related('auth_password', {})->set_password($c->param('password'));
return $person;
});
} catch {
push @{$c->stash->{errors}}, "Account could not be created: $_";
return;
};
$c->session->{uid} = $person->id;
$c->redirect_to( $c->url_for( 'dashboard' ) );
}
sub login ($c) {
# If we have no user accounts, redirect the user to the
# initial create user page.
my $user_count = $c->db->resultset('Person')->count;
if ( $user_count == 0 ) {
$c->redirect_to( $c->url_for( 'auth_init' ) );
return;
}
}
sub create_login ($c) {
my $email = $c->stash->{form_email} = $c->param('email');
my $password = $c->stash->{form_password} = $c->param('password');
my $person = $c->db->resultset('Person')->find( { email => $email } )
or push @{$c->stash->{errors}}, "Invalid email address or password.";
return if $c->stash->{errors};
$person->auth_password->check_password( $password )
or push @{$c->stash->{errors}}, "Invalid email address or password.";
return if $c->stash->{errors};
$c->session->{uid} = $person->id;
$c->redirect_to( $c->url_for( 'dashboard' ) );
}
sub logout ($c) {
undef $c->session->{uid};
$c->redirect_to( $c->url_for( 'auth_login' ) );
}
1;

View file

@ -1,94 +0,0 @@
package MeshMage::Web::Plugin::UserManagement;
use Mojo::Base 'Mojolicious::Plugin', -signatures;
use Try::Tiny;
sub register ( $self, $app, $config ) {
my $r = $config->{route} || $app->routes;
# User Management
$r->get ( '/first' )->to( cb => sub ($c) {
my $user_count = $c->db->resultset('Person')->count;
if ( $user_count >= 1 ) {
$c->redirect_to( $c->url_for( 'view_login' ) );
return;
}
$c->render( template => 'first', format => 'html', handler => 'tx' );
})->name( 'view_first' );
$r->post( '/first' )->to( cb => sub ($c) {
# This code should only run when there are no user accounts, if
# a user account exists, redirect the user to the login panel.
my $user_count = $c->db->resultset('Person')->count;
if ( $user_count >= 1 ) {
$c->redirect_to( $c->url_for( 'view_login' ) );
return;
}
my $person = try {
$c->db->storage->schema->txn_do( sub {
my $person = $c->db->resultset('Person')->create({
email => $c->param('email'),
name => $c->param('name'),
});
$person->new_related('auth_password', {})->set_password($c->param('password'));
return $person;
});
} catch {
push @{$c->stash->{errors}}, "Account could not be created: $_";
$c->render( template => 'first', format => 'html', handler => 'tx' );
return;
};
$c->session->{uid} = $person->id;
$c->redirect_to( $c->url_for( 'dashboard' ) );
})->name( 'post_first' );
$r->get ( '/login' )->to( cb => sub ($c) {
# If we have no user accounts, redirect the user to the
# initial create user page.
my $user_count = $c->db->resultset('Person')->count;
if ( $user_count == 0 ) {
$c->redirect_to( $c->url_for( 'view_first' ) );
return;
}
$c->render( template => 'login', format => 'html', handler => 'tx' );
})->name( 'view_login' );
$r->post( '/login' )->to( cb => sub ($c) {
my $email = $c->stash->{form_email} = $c->param('email');
my $password = $c->stash->{form_password} = $c->param('password');
my $person = $c->db->resultset('Person')->find( { email => $email } )
or push @{$c->stash->{errors}}, "Invalid email address or password.";
if ( $c->stash->{errors} or not $person ) {
$c->render( template => 'login', format => 'html', handler => 'tx' );
return;
}
$person->auth_password->check_password( $password )
or push @{$c->stash->{errors}}, "Invalid email address or password.";
if ( $c->stash->{errors} ) {
$c->render( template => 'login', format => 'html', handler => 'tx' );
return;
}
$c->session->{uid} = $person->id;
$c->redirect_to( $c->url_for( 'dashboard' ) );
})->name( 'post_login' );
$r->get ( '/logout' )->to( cb => sub ($c) {
undef $c->session->{uid};
$c->redirect_to( $c->url_for( 'view_login' ) );
})->name( 'logout' );
}
1;

View file

@ -16,7 +16,7 @@
</div>
%% }
<form style="margin-top: 1.5em" method="POST" action="/first">
<form style="margin-top: 1.5em" method="POST" action="[% $c.url_for( 'auth_init' ) %]">
%% include '_base/form/input.tx' { type => 'text', name => 'name',
%% title => 'Your name',

View file

@ -16,7 +16,7 @@
</div>
%% }
<form style="margin-top: 1.5em" method="POST" action="/login">
<form style="margin-top: 1.5em" method="POST" action="[% $c.url_for( 'auth_login' ) %]">
%% include '_base/form/input.tx' { type => 'email', name => 'email',
%% title => 'Email Address',

View file

@ -1,286 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="Mark Otto, Jacob Thornton, and Bootstrap contributors">
<meta name="generator" content="Hugo 0.83.1">
<title>Dashboard Template · Bootstrap v5.0</title>
<!-- Bootstrap core CSS -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-+0n0xVW2eSR5OomGNYDnhzAbDsOXxcvSN1TPprVMTNDbiYZCxYbOOl7+AMvyTG2x" crossorigin="anonymous">
<!-- Favicons -->
<link rel="apple-touch-icon" href="/docs/5.0/assets/img/favicons/apple-touch-icon.png" sizes="180x180">
<link rel="icon" href="/docs/5.0/assets/img/favicons/favicon-32x32.png" sizes="32x32" type="image/png">
<link rel="icon" href="/docs/5.0/assets/img/favicons/favicon-16x16.png" sizes="16x16" type="image/png">
<link rel="manifest" href="/docs/5.0/assets/img/favicons/manifest.json">
<link rel="mask-icon" href="/docs/5.0/assets/img/favicons/safari-pinned-tab.svg" color="#7952b3">
<link rel="icon" href="/docs/5.0/assets/img/favicons/favicon.ico">
<meta name="theme-color" content="#7952b3">
<!-- Custom styles for this template -->
<link href="/assets/css/dashboard.css" rel="stylesheet">
</head>
<body>
<header class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0 shadow">
<a class="navbar-brand col-md-3 col-lg-2 me-0 px-3" href="#">Company name</a>
<button class="navbar-toggler position-absolute d-md-none collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#sidebarMenu" aria-controls="sidebarMenu" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<input class="form-control form-control-dark w-100" type="text" placeholder="Search" aria-label="Search">
<ul class="navbar-nav px-3">
<li class="nav-item text-nowrap">
<a class="nav-link" href="#">Sign out</a>
</li>
</ul>
</header>
<div class="container-fluid">
<div class="row">
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
<div class="position-sticky pt-3">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">
<span data-feather="home"></span>
Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="file"></span>
Orders
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="shopping-cart"></span>
Products
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="users"></span>
Customers
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="bar-chart-2"></span>
Reports
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="layers"></span>
Integrations
</a>
</li>
</ul>
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted">
<span>Saved reports</span>
<a class="link-secondary" href="#" aria-label="Add a new report">
<span data-feather="plus-circle"></span>
</a>
</h6>
<ul class="nav flex-column mb-2">
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="file-text"></span>
Current month
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="file-text"></span>
Last quarter
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="file-text"></span>
Social engagement
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">
<span data-feather="file-text"></span>
Year-end sale
</a>
</li>
</ul>
</div>
</nav>
<main class="col-md-9 ms-sm-auto col-lg-10 px-md-4">
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h1 class="h2">Dashboard</h1>
<div class="btn-toolbar mb-2 mb-md-0">
<div class="btn-group me-2">
<button type="button" class="btn btn-sm btn-outline-secondary">Share</button>
<button type="button" class="btn btn-sm btn-outline-secondary">Export</button>
</div>
<button type="button" class="btn btn-sm btn-outline-secondary dropdown-toggle">
<span data-feather="calendar"></span>
This week
</button>
</div>
</div>
<canvas class="my-4 w-100" id="myChart" width="900" height="380"></canvas>
<h2>Section title</h2>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>#</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
<th>Header</th>
</tr>
</thead>
<tbody>
<tr>
<td>1,001</td>
<td>random</td>
<td>data</td>
<td>placeholder</td>
<td>text</td>
</tr>
<tr>
<td>1,002</td>
<td>placeholder</td>
<td>irrelevant</td>
<td>visual</td>
<td>layout</td>
</tr>
<tr>
<td>1,003</td>
<td>data</td>
<td>rich</td>
<td>dashboard</td>
<td>tabular</td>
</tr>
<tr>
<td>1,003</td>
<td>information</td>
<td>placeholder</td>
<td>illustrative</td>
<td>data</td>
</tr>
<tr>
<td>1,004</td>
<td>text</td>
<td>random</td>
<td>layout</td>
<td>dashboard</td>
</tr>
<tr>
<td>1,005</td>
<td>dashboard</td>
<td>irrelevant</td>
<td>text</td>
<td>placeholder</td>
</tr>
<tr>
<td>1,006</td>
<td>dashboard</td>
<td>illustrative</td>
<td>rich</td>
<td>data</td>
</tr>
<tr>
<td>1,007</td>
<td>placeholder</td>
<td>tabular</td>
<td>information</td>
<td>irrelevant</td>
</tr>
<tr>
<td>1,008</td>
<td>random</td>
<td>data</td>
<td>placeholder</td>
<td>text</td>
</tr>
<tr>
<td>1,009</td>
<td>placeholder</td>
<td>irrelevant</td>
<td>visual</td>
<td>layout</td>
</tr>
<tr>
<td>1,010</td>
<td>data</td>
<td>rich</td>
<td>dashboard</td>
<td>tabular</td>
</tr>
<tr>
<td>1,011</td>
<td>information</td>
<td>placeholder</td>
<td>illustrative</td>
<td>data</td>
</tr>
<tr>
<td>1,012</td>
<td>text</td>
<td>placeholder</td>
<td>layout</td>
<td>dashboard</td>
</tr>
<tr>
<td>1,013</td>
<td>dashboard</td>
<td>irrelevant</td>
<td>text</td>
<td>visual</td>
</tr>
<tr>
<td>1,014</td>
<td>dashboard</td>
<td>illustrative</td>
<td>rich</td>
<td>data</td>
</tr>
<tr>
<td>1,015</td>
<td>random</td>
<td>tabular</td>
<td>information</td>
<td>text</td>
</tr>
</tbody>
</table>
</div>
</main>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.1/dist/js/bootstrap.bundle.min.js"
integrity="sha384-gtEjrD/SeCtmISkJkNUaaKMoLD0//ElJ19smozuHV6z3Iehds+3Ulb9Bn9Plx0x4"
crossorigin="anonymous">
</script>
<script src="https://cdn.jsdelivr.net/npm/feather-icons@4.28.0/dist/feather.min.js"
integrity="sha384-uO3SXW5IuS1ZpFPKugNNWqTZRRglnUJK6UAZ/gxOX80nxEkN9NcGZTftn6RzhGWE"
crossorigin="anonymous">
</script>
<script src="https://cdn.jsdelivr.net/npm/chart.js@2.9.4/dist/Chart.min.js"
integrity="sha384-zNy6FEbO50N+Cg5wap8IKA4M/ZnLJgzc6w2NqACZaK0u0FXfOWRRJOnQtpZun8ha"
crossorigin="anonymous">
</script>
<script src="/assets/js/dashboard.js"></script>
</body>
</html>