From 1eafaf4ab17aaa068bcaca5bccf9958f2d030662 Mon Sep 17 00:00:00 2001 From: Paul Dill Date: Fri, 24 Feb 2017 19:27:43 +0000 Subject: [PATCH] Moved to full Mojolicious app. --- cpanfile | 3 + foodloopserver.pl | 1146 +---------------- lib/Pear/LocalLoop.pm | 412 ++++++ lib/Pear/LocalLoop/Controller/Admin.pm | 106 ++ lib/Pear/LocalLoop/Controller/Api.pm | 59 + lib/Pear/LocalLoop/Controller/Auth.pm | 123 ++ lib/Pear/LocalLoop/Controller/Register.pm | 241 ++++ lib/Pear/LocalLoop/Controller/Upload.pm | 334 +++++ lib/Pear/LocalLoop/Schema.pm | 20 + .../LocalLoop/Schema/Result/AccountToken.pm | 98 ++ .../LocalLoop/Schema/Result/Administrator.pm | 92 ++ lib/Pear/LocalLoop/Schema/Result/AgeRange.pm | 107 ++ lib/Pear/LocalLoop/Schema/Result/Customer.pm | 137 ++ .../LocalLoop/Schema/Result/Organisation.pm | 122 ++ .../Schema/Result/PendingOrganisation.pm | 139 ++ .../Schema/Result/PendingTransaction.pm | 152 +++ .../LocalLoop/Schema/Result/SessionToken.pm | 122 ++ .../LocalLoop/Schema/Result/Transaction.pm | 152 +++ lib/Pear/LocalLoop/Schema/Result/User.pm | 261 ++++ foodloopserver.conf => pear-local_loop.conf | 0 ...t.conf => pear-local_loop.development.conf | 0 script/make_schema | 83 ++ t/admin-approve.t | 4 +- t/basic.t | 4 +- t/login.t | 4 +- t/register.t | 4 +- t/search.t | 4 +- t/upload.t | 4 +- 28 files changed, 2775 insertions(+), 1158 deletions(-) create mode 100644 lib/Pear/LocalLoop.pm create mode 100644 lib/Pear/LocalLoop/Controller/Admin.pm create mode 100644 lib/Pear/LocalLoop/Controller/Api.pm create mode 100644 lib/Pear/LocalLoop/Controller/Auth.pm create mode 100644 lib/Pear/LocalLoop/Controller/Register.pm create mode 100644 lib/Pear/LocalLoop/Controller/Upload.pm create mode 100644 lib/Pear/LocalLoop/Schema.pm create mode 100644 lib/Pear/LocalLoop/Schema/Result/AccountToken.pm create mode 100644 lib/Pear/LocalLoop/Schema/Result/Administrator.pm create mode 100644 lib/Pear/LocalLoop/Schema/Result/AgeRange.pm create mode 100644 lib/Pear/LocalLoop/Schema/Result/Customer.pm create mode 100644 lib/Pear/LocalLoop/Schema/Result/Organisation.pm create mode 100644 lib/Pear/LocalLoop/Schema/Result/PendingOrganisation.pm create mode 100644 lib/Pear/LocalLoop/Schema/Result/PendingTransaction.pm create mode 100644 lib/Pear/LocalLoop/Schema/Result/SessionToken.pm create mode 100644 lib/Pear/LocalLoop/Schema/Result/Transaction.pm create mode 100644 lib/Pear/LocalLoop/Schema/Result/User.pm rename foodloopserver.conf => pear-local_loop.conf (100%) rename foodloopserver.development.conf => pear-local_loop.development.conf (100%) create mode 100755 script/make_schema diff --git a/cpanfile b/cpanfile index c0d75f0..41ba42a 100644 --- a/cpanfile +++ b/cpanfile @@ -7,3 +7,6 @@ requires 'ORM::Date'; requires 'Authen::Passphrase::BlowfishCrypt'; requires 'Time::Fake'; requires 'Scalar::Util'; +requires 'DBIx::Class'; +requires 'DBIx::Class::Schema::Loader'; +requires 'DateTime'; diff --git a/foodloopserver.pl b/foodloopserver.pl index 50c184a..e9658f9 100644 --- a/foodloopserver.pl +++ b/foodloopserver.pl @@ -1,1143 +1,9 @@ +#!/usr/bin/env perl -#!/usr/bin/env perl -w -# NOT READY FOR PRODUCTION +use strict; +use warnings; -use Mojolicious::Lite; -use Data::UUID; -use Devel::Dwarn; -use Mojo::JSON; -use Data::Dumper; -use Email::Valid; -use ORM::Date; -use Authen::Passphrase::BlowfishCrypt; -use Scalar::Util qw(looks_like_number); +use lib 'lib'; +use Mojolicious::Commands; -# connect to database -use DBI; - -my $config = plugin 'Config'; - -my $dbh = DBI->connect($config->{dsn},$config->{user},$config->{pass}) or die "Could not connect"; -$dbh->do("PRAGMA foreign_keys = ON"); -$dbh->do("PRAGMA secure_delete = ON"); - -my $sessionTimeSeconds = 60 * 60 * 24 * 7; #1 week. -my $sessionTokenJsonName = 'sessionToken'; -my $sessionExpiresJsonName = 'sessionExpires'; - -Dwarn $config; - -# shortcut for use in template -helper db => sub { $dbh }; - - -any '/' => sub { - my $self = shift; - return $self->render(text => 'If you are seeing this, then the server is running.', success => Mojo::JSON->true); -}; - -post '/admin-approve' => sub{ - my $self = shift; - - my $userId = $self->get_active_user_id(); - if ( ! $self->is_admin($userId) ) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'You are not an admin.', - }, - status => 403,); #Forbidden request - } - - my $json = $self->req->json; - if ( ! defined $json ) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'JSON is missing.', - }, - status => 400,); #Malformed request - } - - my $unvalidatedOrganisationId = $json->{unvalidatedOrganisationId}; - if ( ! defined $unvalidatedOrganisationId ) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'unvalidatedOrganisationId is missing.', - }, - status => 400,); #Malformed request - } - elsif (! Scalar::Util::looks_like_number($unvalidatedOrganisationId)){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'unvalidatedOrganisationId does not look like a number.', - }, - status => 400,); #Malformed request - } - - - my ($id, $name, $fullAddress, $postcode) = $dbh->selectrow_array("SELECT PendingOrganisationId, Name, FullAddress, Postcode FROM PendingOrganisations WHERE PendingOrganisationId = ?", undef, ($unvalidatedOrganisationId)); - - #It does not exist. - if (! defined $id) { - return $self->render( json => { - success => Mojo::JSON->false, - message => 'the specified unvalidatedOrganisationId does not exist.', - }, - status => 400,); #Malformed request - } - - - my $nameJson = $json->{name}; - if (defined $nameJson) { - $name = $nameJson; - } - - my $fullAddressJson = $json->{fullAddress}; - if (defined $fullAddressJson) { - $fullAddress = $fullAddressJson; - } - - my $postCodeJson = $json->{postCode}; - if (defined $postCodeJson) { - $postcode = $postCodeJson; - } - - - #FIXME there may be race conditions here, so may get the wrong number, mutux is needed. - my $statementInsOrg = $self->db->prepare("INSERT INTO Organisations (Name, FullAddress, PostCode) VALUES (?, ?, ?)"); - $statementInsOrg->execute($name, $fullAddress, $postcode); - my $organisationalId = $self->db->last_insert_id(undef,undef, "Organisations", "OrganisationalId") . "\n"; - - my $statementSelectPendingTrans = $dbh->prepare("SELECT BuyerUserId_FK, ValueMicroCurrency, ProofImage, TimeDateSubmitted FROM PendingTransactions WHERE PendingSellerOrganisationId_FK = ?"); - $statementSelectPendingTrans->execute($organisationalId); - - my $statementInsTrans = $self->db->prepare("INSERT INTO Transactions (BuyerUserId_FK, SellerOrganisationId_FK, ValueMicroCurrency, ProofImage, TimeDateSubmitted) VALUES (?, ?, ?, ?, ?)"); - - #Move all transactions from pending onto verified. - while (my ($buyerUserId, $value, $imgName, $timeDate) = $statementSelectPendingTrans->fetchrow_array()) { - $statementInsTrans->execute($buyerUserId, $organisationalId, $value, $imgName, $timeDate); - } - - #Delete transactions first, so there is no dependancy when deleting the row from PendingOrganisations. - $self->db->prepare("DELETE FROM PendingTransactions WHERE PendingSellerOrganisationId_FK = ?")->execute($unvalidatedOrganisationId); - $self->db->prepare("DELETE FROM PendingOrganisations WHERE PendingOrganisationId = ?")->execute($unvalidatedOrganisationId); - - $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->true, - validatedOrganisationId => $organisationalId, - }, - status => 200,); - -}; - -#TODO this should limit the number of responses returned, when location is implemented that would be the main way of filtering. -post '/search' => sub { - my $self = shift; - my $userId = $self->get_active_user_id(); - - my $json = $self->req->json; - if ( ! defined $json ) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'JSON is missing.', - }, - status => 400,); #Malformed request - } - - my $searchName = $json->{searchName}; - if ( ! defined $searchName ) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'searchName is missing.', - }, - status => 400,); #Malformed request - } - #Is blank - elsif ( $searchName =~ m/^\s*$/) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'searchName is blank.', - }, - status => 400,); #Malformed request - } - - #Currently ingnored - #TODO implement further. - my $searchLocation = $json->{searchLocation}; - - my @validatedOrgs = (); - { - my $statementValidated = $dbh->prepare("SELECT OrganisationalId, Name, FullAddress, PostCode FROM Organisations WHERE Name LIKE ?"); - $statementValidated->execute('%'.$searchName.'%'); - - while (my ($id, $name, $address, $postcode) = $statementValidated->fetchrow_array()) { - push(@validatedOrgs, $self->create_hash($id,$name,$address,$postcode)); - } - } - - #$self->app->log->debug( "Orgs: " . Dumper @validatedOrgs ); - - my @unvalidatedOrgs = (); - { - my $statementUnvalidated = $dbh->prepare("SELECT PendingOrganisationId, Name, FullAddress, Postcode FROM PendingOrganisations WHERE Name LIKE ? AND UserSubmitted_FK = ?"); - $statementUnvalidated->execute('%'.$searchName.'%', $userId); - - while (my ($id, $name, $fullAddress, $postcode) = $statementUnvalidated->fetchrow_array()) { - push(@unvalidatedOrgs, $self->create_hash($id, $name, $fullAddress, $postcode)); - } - } - - $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->true, - unvalidated => \@unvalidatedOrgs, - validated => \@validatedOrgs, - }, - status => 200,); - -}; - - - -post '/upload' => sub { - my $self = shift; - - my $userId = $self->get_active_user_id(); - - my $json = $self->param('json'); - if ( ! defined $json ) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'JSON is missing.', - }, - status => 400,); #Malformed request - } - - $json = Mojo::JSON::decode_json($json); - $self->app->log->debug( "JSON: " . Dumper $json ); - - my $microCurrencyValue = $json->{microCurrencyValue}; - if ( ! defined $microCurrencyValue ) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'microCurrencyValue is missing.', - }, - status => 400,); #Malformed request - } - #Is valid number - elsif (! Scalar::Util::looks_like_number($microCurrencyValue)){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'microCurrencyValue does not look like a number.', - }, - status => 400,); #Malformed request - } - #Is the number range valid. - elsif ($microCurrencyValue <= 0){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'microCurrencyValue cannot be equal to or less than zero.', - }, - status => 400,); #Malformed request - } - - my $transactionAdditionType = $json->{transactionAdditionType}; - if ( ! defined $transactionAdditionType ) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'transactionAdditionType is missing.', - }, - status => 400,); #Malformed request - } - - my $file = $self->req->upload('file2'); - $self->app->log->debug( "file: " . Dumper $file ); - - if (! defined $file) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'no file uploaded.', - }, - status => 400,); #Malformed request - } - - my $ext = '.jpg'; - my $uuid = Data::UUID->new->create_str; - my $filename = $uuid . $ext; - - #TODO Check for valid image file. -# my $headers = $file->headers->content_type; -# $self->app->log->debug( "content type: " . Dumper $headers ); - #Is content type wrong? -# if ($headers ne 'image/jpeg') { -# return $self->render( json => { -# success => Mojo::JSON->false, -# message => 'Wrong image extension!', -# }, status => 400); -# }; - - #Add validated organisation. - if ($transactionAdditionType == 1){ - - my $addValidatedId = $json->{addValidatedId}; - if (! defined $addValidatedId){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'addValidatedId is missing.', - }, - status => 400,); #Malformed request - } - - if (! $self->does_organisational_id_exist($addValidatedId)){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'addValidatedId does not exist in the database.', - }, - status => 400,); #Malformed request - } - - my $time = time(); - my $statement = $self->db->prepare("INSERT INTO Transactions (BuyerUserId_FK, SellerOrganisationId_FK, ValueMicroCurrency, ProofImage, TimeDateSubmitted) VALUES (?, ?, ?, ?, ?)"); - my $rowsAdded = $statement->execute($userId, $addValidatedId, $microCurrencyValue, $filename, $time); - - #It was successful. - if ($rowsAdded != 0) { - $file->move_to('images/' . $filename); - $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->true, - message => 'Added transaction for validated organisation.', - }, - status => 200,); - } - #TODO Untested, not quite sure how to test it. - else { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'An unknown error occurred when adding the transaction.', - }, - status => 500,); - } - } - #2 and 3 are similar by the adding of a transaction at the end. - elsif ($transactionAdditionType == 2 || $transactionAdditionType == 3){ - - my $unvalidatedOrganisationId = undef; - - if ($transactionAdditionType == 2){ - $self->app->log->debug('Path: file:' . __FILE__ . ', line: ' . __LINE__); - - $unvalidatedOrganisationId = $json->{addUnvalidatedId}; - if (! defined $unvalidatedOrganisationId){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'addUnvalidatedId is missing.', - }, - status => 400,); #Malformed request - } - elsif (! Scalar::Util::looks_like_number($unvalidatedOrganisationId)){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'addUnvalidatedId does not look like a number.', - }, - status => 400,); #Malformed request - } - - my ($existsRef) = $self->db->selectrow_array("SELECT COUNT(PendingOrganisationId) FROM PendingOrganisations WHERE PendingOrganisationId = ? AND UserSubmitted_FK = ?",undef,($unvalidatedOrganisationId, $userId)); - if ($existsRef == 0) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'addUnvalidatedId does not exist in the database for the user.', - }, - status => 400,); #Malformed request - } - - } - #type need to add a organisation for type 3. - else{ # ($transactionAdditionType == 3) - $self->app->log->debug('Path: file:' . __FILE__ . ', line: ' . __LINE__); - - #TODO more validation. - my $organisationName = $json->{organisationName}; - if (! defined $organisationName){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'organisationName is missing.', - }, - status => 400,); #Malformed request - } - - #TODO validation. - #TODO check which ones are present. - my $streetName = $json->{streetName}; - my $town = $json->{town}; - my $postcode = $json->{postcode}; - - ($unvalidatedOrganisationId) = $self->db->selectrow_array("SELECT MAX(PendingOrganisationId) FROM PendingOrganisations",undef,()); - if (defined $unvalidatedOrganisationId){ - $unvalidatedOrganisationId++; - } - else{ - $unvalidatedOrganisationId = 1; - } - - my $fullAddress = ""; - - if ( defined $streetName && ! ($streetName =~ m/^\s*$/) ){ - $fullAddress = $streetName; - } - - if ( defined $town && ! ($town =~ m/^\s*$/) ){ - if ($fullAddress eq ""){ - $fullAddress = $town; - } - else{ - $fullAddress = $fullAddress . ", " . $town; - } - - } - - my $statement = $self->db->prepare("INSERT INTO PendingOrganisations (PendingOrganisationId, UserSubmitted_FK, TimeDateSubmitted, Name, FullAddress, Postcode) VALUES (?, ?, ?, ?, ?, ?)"); - my $rowsAdded = $statement->execute($unvalidatedOrganisationId,$userId,time(),$organisationName,$fullAddress,$postcode); - - #TODO, untested. It could not be added for some reason. Most likely race conditions. - if ($rowsAdded == 0) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'An unknown error occurred when adding the transaction.', - }, - status => 500,); - } - } - - - my $statement2 = $self->db->prepare("INSERT INTO PendingTransactions (BuyerUserId_FK, PendingSellerOrganisationId_FK, ValueMicroCurrency, ProofImage, TimeDateSubmitted) VALUES (?, ?, ?, ?, ?)"); - my $rowsAdded2 = $statement2->execute($userId, $unvalidatedOrganisationId, $microCurrencyValue, $filename, time()); - - if ($rowsAdded2 != 0) { - $file->move_to('images/' . $filename); - $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->true, - message => 'Added transaction for unvalidated organisation.', - }, - status => 200,); - } - else { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'An unknown error occurred when adding the transaction.', - }, - status => 500,); - } - } - else{ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'transactionAdditionType is not a valid value.', - }, - status => 400,); #Malformed request - } - -}; - -post '/register' => sub { - my $self = shift; - - my $json = $self->req->json; - $self->app->log->debug( "\n\nStart of register"); - $self->app->log->debug( "JSON: " . Dumper $json ); - - if ( ! defined $json ){ - $self->app->log->debug('Path: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'No json sent.', - }, - status => 400,); #Malformed request - } - - my $token = $json->{token}; - if ( ! defined $token ){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'No token sent.', - }, - status => 400,); #Malformed request - } - elsif ( ! $self->is_token_unused($token) ) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Token invalid or has been used.', - }, - status => 401,); #Unauthorized - } - - my $username = $json->{username}; - if ( ! defined $username ){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'No username sent.', - }, - status => 400,); #Malformed request - } - elsif ($username eq ''){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Username cannot be blank.', - }, - status => 400,); #Malformed request - } - elsif ( ! ($self->valid_username($username))){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Username can only be A-Z, a-z and 0-9 characters.', - }, - status => 400,); #Malformed request - } - elsif ( $self->does_username_exist($username) ) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Username exists.', - }, - status => 403,); #Forbidden - } - - my $email = $json->{email}; - if ( ! defined $email ){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'No email sent.', - }, - status => 400,); #Malformed request - } - elsif ( ! $self->valid_email($email)){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Email is invalid.', - }, - status => 400,); #Malformed request - } - elsif($self->does_email_exist($email)) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Email exists.', - }, - status => 403,); #Forbidden - } - - #TODO test to see if post code is valid. - my $postcode = $json->{postcode}; - if ( ! defined $postcode ){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'No postcode sent.', - }, - status => 400,); #Malformed request - } - - #TODO should we enforce password requirements. - my $password = $json->{password}; - if ( ! defined $password ){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'No password sent.', - }, - status => 400,); #Malformed request - } - my $hashedPassword = $self->generate_hashed_password($password); - - my $secondsTime = time(); - my $date = ORM::Date->new_epoch($secondsTime)->mysql_date; - - my $usertype = $json->{usertype}; - - if ( ! defined $usertype ){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'No usertype sent.', - }, - status => 400,); #Malformed request - } - elsif ($usertype eq 'customer'){ - $self->app->log->debug('Path: file:' . __FILE__ . ', line: ' . __LINE__); - - my $age = $json->{age}; - if ( ! defined $age ){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'No age sent.', - }, - status => 400,); #Malformed request - } - - my $ageForeignKey = $self->get_age_foreign_key($age); - if ( ! defined $ageForeignKey ){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Age range is invalid.', - }, - status => 400,); #Malformed request - } - - #TODO UNTESTED as it's hard to simulate. - #Token is no longer valid race condition. - if ( ! $self->set_token_as_used($token) ){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Token no longer is accepted.', - }, - status => 500,); #Internal server error. Racecondition - } - - - my ($idToUse) = $self->db->selectrow_array("SELECT MAX(CustomerId) FROM Customers"); - if (defined $idToUse){ - $idToUse++; - } - else{ - $idToUse = 1; - } - - #TODO Race condition here. - my $insertCustomer = $self->db->prepare("INSERT INTO Customers (CustomerId, UserName, AgeRange_FK, PostCode) VALUES (?, ?, ?, ?)"); - my $rowsInsertedCustomer = $insertCustomer->execute($idToUse, $username, $ageForeignKey, $postcode); - my $insertUser = $self->db->prepare("INSERT INTO Users (CustomerId_FK, Email, JoinDate, HashedPassword) VALUES (?, ?, ?, ?)"); - my $rowsInsertedUser = $insertUser->execute($idToUse, $email, $date, $hashedPassword); - - $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { success => Mojo::JSON->true } ); - } - elsif ($usertype eq 'organisation') { - $self->app->log->debug('Path: file:' . __FILE__ . ', line: ' . __LINE__); - - #TODO validation on the address. Or perhaps add the organisation to a "to be inspected" list then manually check them. - my $fullAddress = $json->{fulladdress}; - if ( ! defined $fullAddress ){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'No fulladdress sent.', - }, - status => 400,); #Malformed request - } - - #TODO UNTESTED as it's hard to simulate. - #Token is no longer valid race condition. - if ( ! $self->set_token_as_used($token) ){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Token no longer is accepted.', - }, - status => 500,); #Internal server error. Racecondition - } - - my $idToUse = $self->db->selectrow_array("SELECT MAX(OrganisationalId) FROM Organisations"); - if (defined $idToUse){ - $idToUse++; - } - else{ - $idToUse = 1; - } - - - #TODO Race condition here. - my $insertOrganisation = $self->db->prepare("INSERT INTO Organisations (OrganisationalId, Name, FullAddress, PostCode) VALUES (?, ?, ?, ?)"); - my $rowsInsertedOrganisation = $insertOrganisation->execute($idToUse, $username, $fullAddress, $postcode); - my $insertUser = $self->db->prepare("INSERT INTO Users (OrganisationalId_FK, Email, JoinDate, HashedPassword) VALUES (?, ?, ?, ?)"); - my $rowsInsertedUser = $insertUser->execute($idToUse, $email, $date, $hashedPassword); - - $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { success => Mojo::JSON->true } ); - } - else{ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => '"usertype" is invalid.', - }, - status => 400,); #Malformed request - } -}; - -post '/edit' => sub { - my $self = shift; - - my $json = $self->req->json; - - my $account = $self->get_account_by_username( $json->{username} ); - - unless ( defined $account ) { - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Username not recognised, has your token expired?', - }); -# PLUG SECURITY HOLE - } elsif ( $account->{keyused} ne 't' ) { - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Token has not been used yet!', - }); - } - my $insert = $self->db->prepare("UPDATE accounts SET fullname = ?, postcode = ?, age = ?, gender = ?, WHERE username = ?"); - $insert->execute( - @{$json}{ qw/ fullname postcode age gender / }, $account->{username}, - ); - - $self->render( json => { success => Mojo::JSON->true } ); -}; - - -hook before_dispatch => sub { - my $self = shift; - - $self->remove_all_expired_sessions(); - - #See if logged in. - my $sessionToken = $self->get_session_token(); - - #0 = no session, npn-0 is has updated session - my $hasBeenExtended = $self->extend_session($sessionToken); - - my $path = $self->req->url->to_abs->path; - - #Has valid session - if ($hasBeenExtended) { - #If logged in and requestine the login page redirect to the main page. - if ($path eq '/login') { - #Force expire and redirect. - $self->res->code(303); - $self->redirect_to('/'); - } - } - #Has expired or did not exist in the first place and the path is not login - elsif ($path ne '/login' && $path ne '/register') { - $self->res->code(303); - $self->redirect_to('/login'); - } -}; - -#FIXME placeholders -#Because of "before_dispatch" this will never be accessed unless the user is not logged in. -get '/login' => sub { - my $self = shift; - $self->render( text => 'This will be the login page.' ); -}; - -#TODO set session cookie and add it to the database. -#FIXME This suffers from replay attacks, consider a challenge response. Would TLS solve this, most likely. -#SessionToken -#Because of "before_dispatch" this will never be accessed unless the user is not logged in. -post '/login' => sub { - my $self = shift; - - my $json = $self->req->json; - $self->app->log->debug( "\n\nStart of login"); - $self->app->log->debug( "JSON: " . Dumper $json ); - - if ( ! defined $json ){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'No json sent.', - }, - status => 400,); #Malformed request - } - - my $email = $json->{email}; - if ( ! defined $email ){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'No email sent.', - }, - status => 400,); #Malformed request - } - elsif ( ! $self->valid_email($email) ) { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'email is invalid.', - }, - status => 400,); #Malformed request - } - - my $password = $json->{password}; - if ( ! defined $password ){ - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'No password sent.', - }, - status => 400,); #Malformed request - } - - - #FIXME There is a timing attack here determining if an email exists or not. - if ($self->does_email_exist($email) && $self->check_password_email($email, $password)) { - #Match. - $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); - - my $userId = $self->get_userid_foreign_key($email); - - #Generates and stores - my $hash = $self->generate_session($userId); - - $self->app->log->debug('session dump:' . Dumper ($hash)); - - return $self->render( json => { - success => Mojo::JSON->true, - $sessionTokenJsonName => $hash->{$sessionTokenJsonName}, - $sessionExpiresJsonName => $hash->{$sessionExpiresJsonName}, - }); - } - else{ - #Mismatch - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Email or password is invalid.', - }, - status => 401,); #Unauthorized request - } -}; - -post '/logout' => sub { - my $self = shift; - - my $json = $self->req->json; - $self->app->log->debug( "\n\nStart of logout"); - $self->app->log->debug( "JSON: " . Dumper $json ); - - #If the session token exists. - if ($self->expire_current_session()) { - $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->true, - message => 'you were successfully logged out.', - }); - } - #Due to the "before_dispatch" hook, this most likely will not be called. i.e. race conditions. - #FIXME untested. - #An invalid token was presented, most likely because it has expired. - else { - $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); - return $self->render( json => { - success => Mojo::JSON->false, - message => 'the session has expired or did not exist in the first place.', - }, - status => 401,); #Unauthorized request - } - -}; - - -post '/fetchuser' => sub { - my $self = shift; - - my $json = $self->req->json; - - my $account = $self->get_account_by_username( $json->{username} ); - - unless ( defined $account ) { - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Username not recognised, has your token expired?', - }); -# PLUG SECURITY HOLE - } elsif ( $account->{keyused} ne 't' ) { - return $self->render( json => { - success => Mojo::JSON->false, - message => 'Token has not been used yet!', - }); - } - -# Add stuff to send back to user below here! - $self->render( json => { - success => Mojo::JSON->true, - }); -}; - - -helper is_admin => sub{ - my ($self, $userId) = @_; - - my ($rowCount) = $self->db->selectrow_array("SELECT COUNT(UserId) FROM Administrators WHERE UserId = ?", undef, ($userId)); - - return $rowCount != 0; -}; - -helper create_hash => sub{ - my ($self, $id, $name, $fullAddress, $postcode) = @_; - - my $hash = {}; - $hash->{'id'} = $id; - $hash->{'name'} = $name; - $hash->{'fullAddress'} = $fullAddress . ", " . $postcode; - - return $hash; -}; - - - -helper valid_username => sub { - my ($self, $username) = @_; - return ($username =~ m/^[A-Za-z0-9]+$/); -}; - -helper valid_email => sub { - my ($self, $email) = @_; - return (Email::Valid->address($email)); -}; - -helper get_active_user_id => sub { - my $self = shift; - - my $token = $self->get_session_token(); - if (! defined $token){ - return undef; - } - - my @out = $self->db->selectrow_array("SELECT UserIdAssignedTo_FK FROM SessionTokens WHERE SessionTokenName = ?",undef,($token)); - if (! @out){ - return undef; - } - else{ - return $out[0]; - } -}; - -helper get_session_token => sub { - my $self = shift; - - #See if logged in. - my $sessionToken = undef; - - my $json = $self->req->json; - if (defined $json) { - $sessionToken = $json->{$sessionTokenJsonName}; - } - - if ( ! defined $sessionToken || $sessionToken eq "" ) { - $sessionToken = $self->session->{$sessionTokenJsonName}; - } - - if (defined $sessionToken && $sessionToken eq "" ) { - $sessionToken = undef; - } - - return $sessionToken; -}; - - -#This assumes the user has no current session on that device. -helper generate_session => sub { - my ($self, $userId) = @_; - - my $sessionToken = $self->generate_session_token(); - my $expireDateTime = $self->session_token_expiry_date_time(); - - my $insertStatement = $self->db->prepare('INSERT INTO SessionTokens (SessionTokenName, UserIdAssignedTo_FK, ExpireDateTime) VALUES (?, ?, ?)'); - my $rowsAdded = $insertStatement->execute($sessionToken, $userId, $expireDateTime); - - $self->session(expires => $expireDateTime); - $self->session->{$sessionTokenJsonName} = $sessionToken; - - return {$sessionTokenJsonName => $sessionToken, $sessionExpiresJsonName => $expireDateTime}; -}; - -helper generate_session_token => sub { - my $self = shift; - return Data::UUID->new->create_str(); -}; - -helper expire_all_sessions => sub { - my $self = shift; - - my $rowsDeleted = $self->db->prepare("DELETE FROM SessionTokens")->execute(); - - return $rowsDeleted; -}; - -helper session_token_expiry_date_time => sub { - my $self = shift; - return time() + $sessionTimeSeconds; -}; - -helper remove_all_expired_sessions => sub { - my $self = shift; - - my $timeDateNow = time(); - - my $removeStatement = $self->db->prepare('DELETE FROM SessionTokens WHERE ExpireDateTime < ?'); - my $rowsRemoved = $removeStatement->execute($timeDateNow); - - return $rowsRemoved; -}; - - -#1 = session update, 0 = there was no session or it expired. -#We assume the token has a valid structure. -helper extend_session => sub { - my ( $self, $sessionToken ) = @_; - - my $timeDateExpire = $self->session_token_expiry_date_time(); - - my $updateStatement = $self->db->prepare('UPDATE SessionTokens SET ExpireDateTime = ? WHERE SessionTokenName = ?'); - my $rowsChanges = $updateStatement->execute($timeDateExpire, $sessionToken); - - #Has been updated. - if ($rowsChanges != 0) { - $self->session(expires => $timeDateExpire); - return 1; - } - else { - $self->session(expires => 1); - return 0; - } -}; - -helper get_session_expiry => sub { - my ( $self, $sessionToken ) = @_; - - my ( $expireTime ) = $self->db->selectrow_array("SELECT ExpireDateTime FROM SessionTokens WHERE SessionTokenName = ?", undef, ($sessionToken)); - - return $expireTime; - -}; - -#True for session was expire, false there was no session to expire. -helper expire_current_session => sub { - my $self = shift; - - my $sessionToken = $self->get_session_token(); - - my $removeStatement = $self->db->prepare('DELETE FROM SessionTokens WHERE SessionTokenName = ?'); - my $rowsRemoved = $removeStatement->execute($sessionToken); - - $self->session(expires => 1); - $self->session->{$sessionTokenJsonName} = $sessionToken; - - return $rowsRemoved != 0; -}; - -#Return true if and only if the token exists and has not been used. -helper is_token_unused => sub { - my ( $self, $token ) = @_; - - my ( $out ) = $self->db->selectrow_array("SELECT COUNT(AccountTokenId) FROM AccountTokens WHERE AccountTokenName = ? AND Used = 0", undef, ($token)); - - return $out != 0; - -}; - -#Return true if and only if the token exists and has not been used. -helper does_organisational_id_exist => sub { - my ( $self, $organisationalId ) = @_; - - my ( $out ) = $self->db->selectrow_array("SELECT COUNT(OrganisationalId) FROM Organisations WHERE OrganisationalId = ?", undef, ($organisationalId)); - return $out != 0; -}; - -helper get_age_foreign_key => sub { - my ( $self, $ageString ) = @_; - - my ($out) = $self->db->selectrow_array("SELECT AgeRangeId FROM AgeRanges WHERE AgeRangeString = ?", undef, ($ageString)); - return $out; -}; - -helper get_userid_foreign_key => sub { - my ( $self, $email ) = @_; - - my ($out) = $self->db->selectrow_array("SELECT UserId FROM Users WHERE Email = ?", undef, ($email)); - return $out; - -}; - - -helper does_username_exist => sub { - my ( $self, $username ) = @_; - - my ($out) = $self->db->selectrow_array("SELECT COUNT(UserName) FROM Customers WHERE UserName = ?", {}, ($username)); - return $out != 0; -}; - -helper does_email_exist => sub { - my ( $self, $email ) = @_; - - my ($out) = $self->db->selectrow_array("SELECT COUNT(Email) FROM Users WHERE Email = ?", {}, ($email)); - return $out != 0; -}; - -helper set_token_as_used => sub { - my ( $self, $token ) = @_; - - #Return true if and only if the token exists and has not been used. - my $statement = $self->db->prepare("UPDATE AccountTokens SET Used = 1 WHERE AccountTokenName = ? AND Used = 0 "); - my $rows = $statement->execute($token); - - #print '-set_token_as_used-'.(Dumper($rows))."-\n"; - - return $rows != 0; -}; - -helper generate_hashed_password => sub { - my ( $self, $password ) = @_; - - my $ppr = Authen::Passphrase::BlowfishCrypt->new( - cost => 8, salt_random => 1, - passphrase => $password); - return $ppr->as_crypt; - -}; - -# We assume the user already exists. -helper check_password_email => sub{ - my ( $self, $email, $password) = @_; - - my ($hashedPassword) = $self->db->selectrow_array("SELECT HashedPassword FROM Users WHERE Email = ?", undef, ($email)); - my $ppr = Authen::Passphrase::BlowfishCrypt->from_crypt($hashedPassword); - - return $ppr->match($password); -}; - -app->start; +Mojolicious::Commands->start_app("Pear::LocalLoop"); diff --git a/lib/Pear/LocalLoop.pm b/lib/Pear/LocalLoop.pm new file mode 100644 index 0000000..e7f40e8 --- /dev/null +++ b/lib/Pear/LocalLoop.pm @@ -0,0 +1,412 @@ +package Pear::LocalLoop; + +use Mojo::Base 'Mojolicious'; +use Data::UUID; +use Devel::Dwarn; +use Mojo::JSON; +use Data::Dumper; +use Email::Valid; +use ORM::Date; +use Authen::Passphrase::BlowfishCrypt; +use Scalar::Util qw(looks_like_number); +use Pear::LocalLoop::Schema; + + +sub startup { + my $self = shift; + + +$self->plugin( 'Config', { + default => { + sessionTimeSeconds => 60 * 60 * 24 * 7, + sessionTokenJsonName => 'sessionToken', + sessionExpiresJsonName => 'sessionExpires', + }, +}); +my $config = $self->config; + +my $schema = Pear::LocalLoop::Schema->connect($config->{dsn},$config->{user},$config->{pass}) or die "Could not connect"; +my $dbh = $schema->storage->dbh; +$dbh->do("PRAGMA foreign_keys = ON"); +$dbh->do("PRAGMA secure_delete = ON"); + +my $sessionTimeSeconds = 60 * 60 * 24 * 7; #1 week. +my $sessionTokenJsonName = 'sessionToken'; +my $sessionExpiresJsonName = 'sessionExpires'; + +Dwarn $config; + +# shortcut for use in template +$self->helper( db => sub { $dbh }); + +my $r = $self->routes; + +$r->post("/register")->to('register#post_register'); + +$r->post("/upload")->to('upload#post_upload'); +$r->post("/search")->to('upload#post_search'); + +$r->post("/admin-approve")->to('admin#post_admin_approve'); + +$r->get("/login")->to('auth#get_login'); +$r->post("/login")->to('auth#post_login'); +$r->post("/logout")->to('auth#post_logout'); + +$r->post("/edit")->to('api#post_edit'); +$r->post("/fetchuser")->to('api#post_fetchuser'); + + +$r->any( '/' => sub { + my $self = shift; + return $self->render(text => 'If you are seeing this, then the server is running.', success => Mojo::JSON->true); +}); + + +#TODO this should limit the number of responses returned, when location is implemented that would be the main way of filtering. +$r->post ('/search' => sub { + my $self = shift; + my $userId = $self->get_active_user_id(); + + my $json = $self->req->json; + if ( ! defined $json ) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'JSON is missing.', + }, + status => 400,); #Malformed request + } + + my $searchName = $json->{searchName}; + if ( ! defined $searchName ) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'searchName is missing.', + }, + status => 400,); #Malformed request + } + #Is blank + elsif ( $searchName =~ m/^\s*$/) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'searchName is blank.', + }, + status => 400,); #Malformed request + } + + #Currently ignored + #TODO implement further. + my $searchLocation = $json->{searchLocation}; + + my @validatedOrgs = (); + { + my $statementValidated = $dbh->prepare("SELECT OrganisationalId, Name, FullAddress, PostCode FROM Organisations WHERE Name LIKE ?"); + $statementValidated->execute('%'.$searchName.'%'); + + while (my ($id, $name, $address, $postcode) = $statementValidated->fetchrow_array()) { + push(@validatedOrgs, $self->create_hash($id,$name,$address,$postcode)); + } + } + + #$self->app->log->debug( "Orgs: " . Dumper @validatedOrgs ); + + my @unvalidatedOrgs = (); + { + my $statementUnvalidated = $dbh->prepare("SELECT PendingOrganisationId, Name, FullAddress, Postcode FROM PendingOrganisations WHERE Name LIKE ? AND UserSubmitted_FK = ?"); + $statementUnvalidated->execute('%'.$searchName.'%', $userId); + + while (my ($id, $name, $fullAddress, $postcode) = $statementUnvalidated->fetchrow_array()) { + push(@unvalidatedOrgs, $self->create_hash($id, $name, $fullAddress, $postcode)); + } + } + + $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->true, + unvalidated => \@unvalidatedOrgs, + validated => \@validatedOrgs, + }, + status => 200,); + +}); + + + + +$self->hook( before_dispatch => sub { + my $self = shift; + + $self->remove_all_expired_sessions(); + + #See if logged in. + my $sessionToken = $self->get_session_token(); + + #0 = no session, npn-0 is has updated session + my $hasBeenExtended = $self->extend_session($sessionToken); + + my $path = $self->req->url->to_abs->path; + + #Has valid session + if ($hasBeenExtended) { + #If logged in and requestine the login page redirect to the main page. + if ($path eq '/login') { + #Force expire and redirect. + $self->res->code(303); + $self->redirect_to('/'); + } + } + #Has expired or did not exist in the first place and the path is not login + elsif ($path ne '/login' && $path ne '/register') { + $self->res->code(303); + $self->redirect_to('/login'); + } +}); + + +$self->helper( is_admin => sub{ + my ($self, $userId) = @_; + + my ($rowCount) = $self->db->selectrow_array("SELECT COUNT(UserId) FROM Administrators WHERE UserId = ?", undef, ($userId)); + + return $rowCount != 0; +}); + +$self->helper( create_hash => sub{ + my ($self, $id, $name, $fullAddress, $postcode) = @_; + + my $hash = {}; + $hash->{'id'} = $id; + $hash->{'name'} = $name; + $hash->{'fullAddress'} = $fullAddress . ", " . $postcode; + + return $hash; +}); + + + +$self->helper( valid_username => sub { + my ($self, $username) = @_; + return ($username =~ m/^[A-Za-z0-9]+$/); +}); + +$self->helper(valid_email => sub { + my ($self, $email) = @_; + return (Email::Valid->address($email)); +}); + +$self->helper(get_active_user_id => sub { + my $self = shift; + + my $token = $self->get_session_token(); + if (! defined $token){ + return undef; + } + + my @out = $self->db->selectrow_array("SELECT UserIdAssignedTo_FK FROM SessionTokens WHERE SessionTokenName = ?",undef,($token)); + if (! @out){ + return undef; + } + else{ + return $out[0]; + } +}); + +$self->helper(get_session_token => sub { + my $self = shift; + + #See if logged in. + my $sessionToken = undef; + + my $json = $self->req->json; + if (defined $json) { + $sessionToken = $json->{$sessionTokenJsonName}; + } + + if ( ! defined $sessionToken || $sessionToken eq "" ) { + $sessionToken = $self->session->{$sessionTokenJsonName}; + } + + if (defined $sessionToken && $sessionToken eq "" ) { + $sessionToken = undef; + } + + return $sessionToken; +}); + + +#This assumes the user has no current session on that device. +$self->helper(generate_session => sub { + my ($self, $userId) = @_; + + my $sessionToken = $self->generate_session_token(); + my $expireDateTime = $self->session_token_expiry_date_time(); + + my $insertStatement = $self->db->prepare('INSERT INTO SessionTokens (SessionTokenName, UserIdAssignedTo_FK, ExpireDateTime) VALUES (?, ?, ?)'); + my $rowsAdded = $insertStatement->execute($sessionToken, $userId, $expireDateTime); + + $self->session(expires => $expireDateTime); + $self->session->{$sessionTokenJsonName} = $sessionToken; + + return {$sessionTokenJsonName => $sessionToken, $sessionExpiresJsonName => $expireDateTime}; +}); + +$self->helper(generate_session_token => sub { + my $self = shift; + return Data::UUID->new->create_str(); +}); + +$self->helper(expire_all_sessions => sub { + my $self = shift; + + my $rowsDeleted = $self->db->prepare("DELETE FROM SessionTokens")->execute(); + + return $rowsDeleted; +}); + +$self->helper(session_token_expiry_date_time => sub { + my $self = shift; + return time() + $sessionTimeSeconds; +}); + +$self->helper(remove_all_expired_sessions => sub { + my $self = shift; + + my $timeDateNow = time(); + + my $removeStatement = $self->db->prepare('DELETE FROM SessionTokens WHERE ExpireDateTime < ?'); + my $rowsRemoved = $removeStatement->execute($timeDateNow); + + return $rowsRemoved; +}); + + +#1 = session update, 0 = there was no session or it expired. +#We assume the token has a valid structure. +$self->helper(extend_session => sub { + my ( $self, $sessionToken ) = @_; + + my $timeDateExpire = $self->session_token_expiry_date_time(); + + my $updateStatement = $self->db->prepare('UPDATE SessionTokens SET ExpireDateTime = ? WHERE SessionTokenName = ?'); + my $rowsChanges = $updateStatement->execute($timeDateExpire, $sessionToken); + + #Has been updated. + if ($rowsChanges != 0) { + $self->session(expires => $timeDateExpire); + return 1; + } + else { + $self->session(expires => 1); + return 0; + } +}); + +$self->helper(get_session_expiry => sub { + my ( $self, $sessionToken ) = @_; + + my ( $expireTime ) = $self->db->selectrow_array("SELECT ExpireDateTime FROM SessionTokens WHERE SessionTokenName = ?", undef, ($sessionToken)); + + return $expireTime; + +}); + +#True for session was expire, false there was no session to expire. +$self->helper(expire_current_session => sub { + my $self = shift; + + my $sessionToken = $self->get_session_token(); + + my $removeStatement = $self->db->prepare('DELETE FROM SessionTokens WHERE SessionTokenName = ?'); + my $rowsRemoved = $removeStatement->execute($sessionToken); + + $self->session(expires => 1); + $self->session->{$sessionTokenJsonName} = $sessionToken; + + return $rowsRemoved != 0; +}); + +#Return true if and only if the token exists and has not been used. +$self->helper(is_token_unused => sub { + my ( $self, $token ) = @_; + + my ( $out ) = $self->db->selectrow_array("SELECT COUNT(AccountTokenId) FROM AccountTokens WHERE AccountTokenName = ? AND Used = 0", undef, ($token)); + + return $out != 0; + +}); + +#Return true if and only if the token exists and has not been used. +$self->helper(does_organisational_id_exist => sub { + my ( $self, $organisationalId ) = @_; + + my ( $out ) = $self->db->selectrow_array("SELECT COUNT(OrganisationalId) FROM Organisations WHERE OrganisationalId = ?", undef, ($organisationalId)); + return $out != 0; +}); + +$self->helper(get_age_foreign_key => sub { + my ( $self, $ageString ) = @_; + + my ($out) = $self->db->selectrow_array("SELECT AgeRangeId FROM AgeRanges WHERE AgeRangeString = ?", undef, ($ageString)); + return $out; +}); + +$self->helper(get_userid_foreign_key => sub { + my ( $self, $email ) = @_; + + my ($out) = $self->db->selectrow_array("SELECT UserId FROM Users WHERE Email = ?", undef, ($email)); + return $out; + +}); + + +$self->helper(does_username_exist => sub { + my ( $self, $username ) = @_; + + my ($out) = $self->db->selectrow_array("SELECT COUNT(UserName) FROM Customers WHERE UserName = ?", {}, ($username)); + return $out != 0; +}); + +$self->helper(does_email_exist => sub { + my ( $self, $email ) = @_; + + my ($out) = $self->db->selectrow_array("SELECT COUNT(Email) FROM Users WHERE Email = ?", {}, ($email)); + return $out != 0; +}); + +$self->helper(set_token_as_used => sub { + my ( $self, $token ) = @_; + + #Return true if and only if the token exists and has not been used. + my $statement = $self->db->prepare("UPDATE AccountTokens SET Used = 1 WHERE AccountTokenName = ? AND Used = 0 "); + my $rows = $statement->execute($token); + + #print '-set_token_as_used-'.(Dumper($rows))."-\n"; + + return $rows != 0; +}); + +$self->helper(generate_hashed_password => sub { + my ( $self, $password ) = @_; + + my $ppr = Authen::Passphrase::BlowfishCrypt->new( + cost => 8, salt_random => 1, + passphrase => $password); + return $ppr->as_crypt; + +}); + +# We assume the user already exists. +$self->helper(check_password_email => sub{ + my ( $self, $email, $password) = @_; + + my ($hashedPassword) = $self->db->selectrow_array("SELECT HashedPassword FROM Users WHERE Email = ?", undef, ($email)); + my $ppr = Authen::Passphrase::BlowfishCrypt->from_crypt($hashedPassword); + + return $ppr->match($password); +}); + +} + +1; diff --git a/lib/Pear/LocalLoop/Controller/Admin.pm b/lib/Pear/LocalLoop/Controller/Admin.pm new file mode 100644 index 0000000..4790e48 --- /dev/null +++ b/lib/Pear/LocalLoop/Controller/Admin.pm @@ -0,0 +1,106 @@ +package Pear::LocalLoop::Controller::Admin; +use Mojo::Base 'Mojolicious::Controller'; +use Data::Dumper; + + +sub post_admin_approve { + my $self = shift; + + my $userId = $self->get_active_user_id(); + if ( ! $self->is_admin($userId) ) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'You are not an admin.', + }, + status => 403,); #Forbidden request + } + + my $json = $self->req->json; + if ( ! defined $json ) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'JSON is missing.', + }, + status => 400,); #Malformed request + } + + my $unvalidatedOrganisationId = $json->{unvalidatedOrganisationId}; + if ( ! defined $unvalidatedOrganisationId ) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'unvalidatedOrganisationId is missing.', + }, + status => 400,); #Malformed request + } + elsif (! Scalar::Util::looks_like_number($unvalidatedOrganisationId)){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'unvalidatedOrganisationId does not look like a number.', + }, + status => 400,); #Malformed request + } + + + my ($id, $name, $fullAddress, $postcode) = $self->db->selectrow_array("SELECT PendingOrganisationId, Name, FullAddress, Postcode FROM PendingOrganisations WHERE PendingOrganisationId = ?", undef, ($unvalidatedOrganisationId)); + + #It does not exist. + if (! defined $id) { + return $self->render( json => { + success => Mojo::JSON->false, + message => 'the specified unvalidatedOrganisationId does not exist.', + }, + status => 400,); #Malformed request + } + + + my $nameJson = $json->{name}; + if (defined $nameJson) { + $name = $nameJson; + } + + my $fullAddressJson = $json->{fullAddress}; + if (defined $fullAddressJson) { + $fullAddress = $fullAddressJson; + } + + my $postCodeJson = $json->{postCode}; + if (defined $postCodeJson) { + $postcode = $postCodeJson; + } + + + #FIXME there may be race conditions here, so may get the wrong number, mutux is needed. + my $statementInsOrg = $self->db->prepare("INSERT INTO Organisations (Name, FullAddress, PostCode) VALUES (?, ?, ?)"); + $statementInsOrg->execute($name, $fullAddress, $postcode); + my $organisationalId = $self->db->last_insert_id(undef,undef, "Organisations", "OrganisationalId") . "\n"; + + my $statementSelectPendingTrans = $self->db->prepare("SELECT BuyerUserId_FK, ValueMicroCurrency, ProofImage, TimeDateSubmitted FROM PendingTransactions WHERE PendingSellerOrganisationId_FK = ?"); + $statementSelectPendingTrans->execute($organisationalId); + + my $statementInsTrans = $self->db->prepare("INSERT INTO Transactions (BuyerUserId_FK, SellerOrganisationId_FK, ValueMicroCurrency, ProofImage, TimeDateSubmitted) VALUES (?, ?, ?, ?, ?)"); + + #Move all transactions from pending onto verified. + while (my ($buyerUserId, $value, $imgName, $timeDate) = $statementSelectPendingTrans->fetchrow_array()) { + $statementInsTrans->execute($buyerUserId, $organisationalId, $value, $imgName, $timeDate); + } + + #Delete transactions first, so there is no dependancy when deleting the row from PendingOrganisations. + $self->db->prepare("DELETE FROM PendingTransactions WHERE PendingSellerOrganisationId_FK = ?")->execute($unvalidatedOrganisationId); + $self->db->prepare("DELETE FROM PendingOrganisations WHERE PendingOrganisationId = ?")->execute($unvalidatedOrganisationId); + + $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->true, + validatedOrganisationId => $organisationalId, + }, + status => 200,); + +} + + +1; + diff --git a/lib/Pear/LocalLoop/Controller/Api.pm b/lib/Pear/LocalLoop/Controller/Api.pm new file mode 100644 index 0000000..a6ca16f --- /dev/null +++ b/lib/Pear/LocalLoop/Controller/Api.pm @@ -0,0 +1,59 @@ +package Pear::LocalLoop::Controller::Api; +use Mojo::Base 'Mojolicious::Controller'; +use Data::Dumper; + +sub post_edit { + my $self = shift; + + my $json = $self->req->json; + + my $account = $self->get_account_by_username( $json->{username} ); + + unless ( defined $account ) { + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Username not recognised, has your token expired?', + }); +# PLUG SECURITY HOLE + } elsif ( $account->{keyused} ne 't' ) { + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Token has not been used yet!', + }); + } + my $insert = $self->db->prepare("UPDATE accounts SET fullname = ?, postcode = ?, age = ?, gender = ?, WHERE username = ?"); + $insert->execute( + @{$json}{ qw/ fullname postcode age gender / }, $account->{username}, + ); + + $self->render( json => { success => Mojo::JSON->true } ); +} + + +sub post_fetchuser { + my $self = shift; + + my $json = $self->req->json; + + my $account = $self->get_account_by_username( $json->{username} ); + + unless ( defined $account ) { + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Username not recognised, has your token expired?', + }); +# PLUG SECURITY HOLE + } elsif ( $account->{keyused} ne 't' ) { + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Token has not been used yet!', + }); + } + +# Add stuff to send back to user below here! + $self->render( json => { + success => Mojo::JSON->true, + }); +} + +1; diff --git a/lib/Pear/LocalLoop/Controller/Auth.pm b/lib/Pear/LocalLoop/Controller/Auth.pm new file mode 100644 index 0000000..e47b959 --- /dev/null +++ b/lib/Pear/LocalLoop/Controller/Auth.pm @@ -0,0 +1,123 @@ +package Pear::LocalLoop::Controller::Auth; +use Mojo::Base 'Mojolicious::Controller'; +use Data::Dumper; +use Mojo::JSON; + + +#FIXME placeholders +#Because of "before_dispatch" this will never be accessed unless the user is not logged in. +sub get_login { + my $self = shift; + $self->render( text => 'This will be the login page.' ); +} + +#TODO set session cookie and add it to the database. +#FIXME This suffers from replay attacks, consider a challenge response. Would TLS solve this, most likely. +#SessionToken +#Because of "before_dispatch" this will never be accessed unless the user is not logged in. +sub post_login { + my $self = shift; + + my $json = $self->req->json; + $self->app->log->debug( "\n\nStart of login"); + $self->app->log->debug( "JSON: " . Dumper $json ); + + if ( ! defined $json ){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'No json sent.', + }, + status => 400,); #Malformed request + } + + my $email = $json->{email}; + if ( ! defined $email ){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'No email sent.', + }, + status => 400,); #Malformed request + } + elsif ( ! $self->valid_email($email) ) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'email is invalid.', + }, + status => 400,); #Malformed request + } + + my $password = $json->{password}; + if ( ! defined $password ){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'No password sent.', + }, + status => 400,); #Malformed request + } + + + #FIXME There is a timing attack here determining if an email exists or not. + if ($self->does_email_exist($email) && $self->check_password_email($email, $password)) { + #Match. + $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); + + my $userId = $self->get_userid_foreign_key($email); + + #Generates and stores + my $hash = $self->generate_session($userId); + + $self->app->log->debug('session dump:' . Dumper ($hash)); + + return $self->render( json => { + success => Mojo::JSON->true, + $self->config->{sessionTokenJsonName} => $hash->{$self->config->{sessionTokenJsonName}}, + $self->config->{sessionExpiresJsonName} => $hash->{$self->config->{sessionExpiresJsonName}}, + }); + } + else{ + #Mismatch + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Email or password is invalid.', + }, + status => 401,); #Unauthorized request + } +} + +sub post_logout { + my $self = shift; + + my $json = $self->req->json; + $self->app->log->debug( "\n\nStart of logout"); + $self->app->log->debug( "JSON: " . Dumper $json ); + + #If the session token exists. + if ($self->expire_current_session()) { + $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->true, + message => 'you were successfully logged out.', + }); + } + #Due to the "before_dispatch" hook, this most likely will not be called. i.e. race conditions. + #FIXME untested. + #An invalid token was presented, most likely because it has expired. + else { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'the session has expired or did not exist in the first place.', + }, + status => 401,); #Unauthorized request + } + +} + + + +1; diff --git a/lib/Pear/LocalLoop/Controller/Register.pm b/lib/Pear/LocalLoop/Controller/Register.pm new file mode 100644 index 0000000..480b824 --- /dev/null +++ b/lib/Pear/LocalLoop/Controller/Register.pm @@ -0,0 +1,241 @@ +package Pear::LocalLoop::Controller::Register; +use Mojo::Base 'Mojolicious::Controller'; +use Data::Dumper; + +sub post_register{ + my $self = shift; + + my $json = $self->req->json; + $self->app->log->debug( "\n\nStart of register"); + $self->app->log->debug( "JSON: " . Dumper $json ); + + if ( ! defined $json ){ + $self->app->log->debug('Path: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'No json sent.', + }, + status => 400,); #Malformed request + } + + my $token = $json->{token}; + if ( ! defined $token ){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'No token sent.', + }, + status => 400,); #Malformed request + } + elsif ( ! $self->is_token_unused($token) ) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Token invalid or has been used.', + }, + status => 401,); #Unauthorized + } + + my $username = $json->{username}; + if ( ! defined $username ){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'No username sent.', + }, + status => 400,); #Malformed request + } + elsif ($username eq ''){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Username cannot be blank.', + }, + status => 400,); #Malformed request + } + elsif ( ! ($self->valid_username($username))){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Username can only be A-Z, a-z and 0-9 characters.', + }, + status => 400,); #Malformed request + } + elsif ( $self->does_username_exist($username) ) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Username exists.', + }, + status => 403,); #Forbidden + } + + my $email = $json->{email}; + if ( ! defined $email ){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'No email sent.', + }, + status => 400,); #Malformed request + } + elsif ( ! $self->valid_email($email)){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Email is invalid.', + }, + status => 400,); #Malformed request + } + elsif($self->does_email_exist($email)) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Email exists.', + }, + status => 403,); #Forbidden + } + + #TODO test to see if post code is valid. + my $postcode = $json->{postcode}; + if ( ! defined $postcode ){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'No postcode sent.', + }, + status => 400,); #Malformed request + } + + #TODO should we enforce password requirements. + my $password = $json->{password}; + if ( ! defined $password ){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'No password sent.', + }, + status => 400,); #Malformed request + } + my $hashedPassword = $self->generate_hashed_password($password); + + my $secondsTime = time(); + my $date = ORM::Date->new_epoch($secondsTime)->mysql_date; + + my $usertype = $json->{usertype}; + + if ( ! defined $usertype ){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'No usertype sent.', + }, + status => 400,); #Malformed request + } + elsif ($usertype eq 'customer'){ + $self->app->log->debug('Path: file:' . __FILE__ . ', line: ' . __LINE__); + + my $age = $json->{age}; + if ( ! defined $age ){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'No age sent.', + }, + status => 400,); #Malformed request + } + + my $ageForeignKey = $self->get_age_foreign_key($age); + if ( ! defined $ageForeignKey ){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Age range is invalid.', + }, + status => 400,); #Malformed request + } + + #TODO UNTESTED as it's hard to simulate. + #Token is no longer valid race condition. + if ( ! $self->set_token_as_used($token) ){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Token no longer is accepted.', + }, + status => 500,); #Internal server error. Racecondition + } + + + my ($idToUse) = $self->db->selectrow_array("SELECT MAX(CustomerId) FROM Customers"); + if (defined $idToUse){ + $idToUse++; + } + else{ + $idToUse = 1; + } + + #TODO Race condition here. + my $insertCustomer = $self->db->prepare("INSERT INTO Customers (CustomerId, UserName, AgeRange_FK, PostCode) VALUES (?, ?, ?, ?)"); + my $rowsInsertedCustomer = $insertCustomer->execute($idToUse, $username, $ageForeignKey, $postcode); + my $insertUser = $self->db->prepare("INSERT INTO Users (CustomerId_FK, Email, JoinDate, HashedPassword) VALUES (?, ?, ?, ?)"); + my $rowsInsertedUser = $insertUser->execute($idToUse, $email, $date, $hashedPassword); + + $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { success => Mojo::JSON->true } ); + } + elsif ($usertype eq 'organisation') { + $self->app->log->debug('Path: file:' . __FILE__ . ', line: ' . __LINE__); + + #TODO validation on the address. Or perhaps add the organisation to a "to be inspected" list then manually check them. + my $fullAddress = $json->{fulladdress}; + if ( ! defined $fullAddress ){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'No fulladdress sent.', + }, + status => 400,); #Malformed request + } + + #TODO UNTESTED as it's hard to simulate. + #Token is no longer valid race condition. + if ( ! $self->set_token_as_used($token) ){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'Token no longer is accepted.', + }, + status => 500,); #Internal server error. Racecondition + } + + my $idToUse = $self->db->selectrow_array("SELECT MAX(OrganisationalId) FROM Organisations"); + if (defined $idToUse){ + $idToUse++; + } + else{ + $idToUse = 1; + } + + + #TODO Race condition here. + my $insertOrganisation = $self->db->prepare("INSERT INTO Organisations (OrganisationalId, Name, FullAddress, PostCode) VALUES (?, ?, ?, ?)"); + my $rowsInsertedOrganisation = $insertOrganisation->execute($idToUse, $username, $fullAddress, $postcode); + my $insertUser = $self->db->prepare("INSERT INTO Users (OrganisationalId_FK, Email, JoinDate, HashedPassword) VALUES (?, ?, ?, ?)"); + my $rowsInsertedUser = $insertUser->execute($idToUse, $email, $date, $hashedPassword); + + $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { success => Mojo::JSON->true } ); + } + else{ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => '"usertype" is invalid.', + }, + status => 400,); #Malformed request + } +} + +1; + diff --git a/lib/Pear/LocalLoop/Controller/Upload.pm b/lib/Pear/LocalLoop/Controller/Upload.pm new file mode 100644 index 0000000..fe20acf --- /dev/null +++ b/lib/Pear/LocalLoop/Controller/Upload.pm @@ -0,0 +1,334 @@ +package Pear::LocalLoop::Controller::Upload; +use Mojo::Base 'Mojolicious::Controller'; +use Data::Dumper; + +sub post_upload { + my $self = shift; + + my $userId = $self->get_active_user_id(); + + my $json = $self->param('json'); + if ( ! defined $json ) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'JSON is missing.', + }, + status => 400,); #Malformed request + } + + $json = Mojo::JSON::decode_json($json); + $self->app->log->debug( "JSON: " . Dumper $json ); + + my $microCurrencyValue = $json->{microCurrencyValue}; + if ( ! defined $microCurrencyValue ) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'microCurrencyValue is missing.', + }, + status => 400,); #Malformed request + } + #Is valid number + elsif (! Scalar::Util::looks_like_number($microCurrencyValue)){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'microCurrencyValue does not look like a number.', + }, + status => 400,); #Malformed request + } + #Is the number range valid. + elsif ($microCurrencyValue <= 0){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'microCurrencyValue cannot be equal to or less than zero.', + }, + status => 400,); #Malformed request + } + + my $transactionAdditionType = $json->{transactionAdditionType}; + if ( ! defined $transactionAdditionType ) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'transactionAdditionType is missing.', + }, + status => 400,); #Malformed request + } + + my $file = $self->req->upload('file2'); + $self->app->log->debug( "file: " . Dumper $file ); + + if (! defined $file) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'no file uploaded.', + }, + status => 400,); #Malformed request + } + + my $ext = '.jpg'; + my $uuid = Data::UUID->new->create_str; + my $filename = $uuid . $ext; + + #TODO Check for valid image file. +# my $headers = $file->headers->content_type; +# $self->app->log->debug( "content type: " . Dumper $headers ); + #Is content type wrong? +# if ($headers ne 'image/jpeg') { +# return $self->render( json => { +# success => Mojo::JSON->false, +# message => 'Wrong image extension!', +# }, status => 400); +# }; + + #Add validated organisation. + if ($transactionAdditionType == 1){ + + my $addValidatedId = $json->{addValidatedId}; + if (! defined $addValidatedId){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'addValidatedId is missing.', + }, + status => 400,); #Malformed request + } + + if (! $self->does_organisational_id_exist($addValidatedId)){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'addValidatedId does not exist in the database.', + }, + status => 400,); #Malformed request + } + + my $time = time(); + my $statement = $self->db->prepare("INSERT INTO Transactions (BuyerUserId_FK, SellerOrganisationId_FK, ValueMicroCurrency, ProofImage, TimeDateSubmitted) VALUES (?, ?, ?, ?, ?)"); + my $rowsAdded = $statement->execute($userId, $addValidatedId, $microCurrencyValue, $filename, $time); + + #It was successful. + if ($rowsAdded != 0) { + $file->move_to('images/' . $filename); + $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->true, + message => 'Added transaction for validated organisation.', + }, + status => 200,); + } + #TODO Untested, not quite sure how to test it. + else { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'An unknown error occurred when adding the transaction.', + }, + status => 500,); + } + } + #2 and 3 are similar by the adding of a transaction at the end. + elsif ($transactionAdditionType == 2 || $transactionAdditionType == 3){ + + my $unvalidatedOrganisationId = undef; + + if ($transactionAdditionType == 2){ + $self->app->log->debug('Path: file:' . __FILE__ . ', line: ' . __LINE__); + + $unvalidatedOrganisationId = $json->{addUnvalidatedId}; + if (! defined $unvalidatedOrganisationId){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'addUnvalidatedId is missing.', + }, + status => 400,); #Malformed request + } + elsif (! Scalar::Util::looks_like_number($unvalidatedOrganisationId)){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'addUnvalidatedId does not look like a number.', + }, + status => 400,); #Malformed request + } + + my ($existsRef) = $self->db->selectrow_array("SELECT COUNT(PendingOrganisationId) FROM PendingOrganisations WHERE PendingOrganisationId = ? AND UserSubmitted_FK = ?",undef,($unvalidatedOrganisationId, $userId)); + if ($existsRef == 0) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'addUnvalidatedId does not exist in the database for the user.', + }, + status => 400,); #Malformed request + } + + } + #type need to add a organisation for type 3. + else{ # ($transactionAdditionType == 3) + $self->app->log->debug('Path: file:' . __FILE__ . ', line: ' . __LINE__); + + #TODO more validation. + my $organisationName = $json->{organisationName}; + if (! defined $organisationName){ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'organisationName is missing.', + }, + status => 400,); #Malformed request + } + + #TODO validation. + #TODO check which ones are present. + my $streetName = $json->{streetName}; + my $town = $json->{town}; + my $postcode = $json->{postcode}; + + ($unvalidatedOrganisationId) = $self->db->selectrow_array("SELECT MAX(PendingOrganisationId) FROM PendingOrganisations",undef,()); + if (defined $unvalidatedOrganisationId){ + $unvalidatedOrganisationId++; + } + else{ + $unvalidatedOrganisationId = 1; + } + + my $fullAddress = ""; + + if ( defined $streetName && ! ($streetName =~ m/^\s*$/) ){ + $fullAddress = $streetName; + } + + if ( defined $town && ! ($town =~ m/^\s*$/) ){ + if ($fullAddress eq ""){ + $fullAddress = $town; + } + else{ + $fullAddress = $fullAddress . ", " . $town; + } + + } + + my $statement = $self->db->prepare("INSERT INTO PendingOrganisations (PendingOrganisationId, UserSubmitted_FK, TimeDateSubmitted, Name, FullAddress, Postcode) VALUES (?, ?, ?, ?, ?, ?)"); + my $rowsAdded = $statement->execute($unvalidatedOrganisationId,$userId,time(),$organisationName,$fullAddress,$postcode); + + #TODO, untested. It could not be added for some reason. Most likely race conditions. + if ($rowsAdded == 0) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'An unknown error occurred when adding the transaction.', + }, + status => 500,); + } + } + + + my $statement2 = $self->db->prepare("INSERT INTO PendingTransactions (BuyerUserId_FK, PendingSellerOrganisationId_FK, ValueMicroCurrency, ProofImage, TimeDateSubmitted) VALUES (?, ?, ?, ?, ?)"); + my $rowsAdded2 = $statement2->execute($userId, $unvalidatedOrganisationId, $microCurrencyValue, $filename, time()); + + if ($rowsAdded2 != 0) { + $file->move_to('images/' . $filename); + $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->true, + message => 'Added transaction for unvalidated organisation.', + }, + status => 200,); + } + else { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'An unknown error occurred when adding the transaction.', + }, + status => 500,); + } + } + else{ + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'transactionAdditionType is not a valid value.', + }, + status => 400,); #Malformed request + } + +} + + +#TODO this should limit the number of responses returned, when location is implemented that would be the main way of filtering. +sub post_search { + my $self = shift; + my $userId = $self->get_active_user_id(); + + my $json = $self->req->json; + if ( ! defined $json ) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'JSON is missing.', + }, + status => 400,); #Malformed request + } + + my $searchName = $json->{searchName}; + if ( ! defined $searchName ) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'searchName is missing.', + }, + status => 400,); #Malformed request + } + #Is blank + elsif ( $searchName =~ m/^\s*$/) { + $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->false, + message => 'searchName is blank.', + }, + status => 400,); #Malformed request + } + + #Currently ignored + #TODO implement further. + my $searchLocation = $json->{searchLocation}; + + my @validatedOrgs = (); + { + my $statementValidated = $self->db->prepare("SELECT OrganisationalId, Name, FullAddress, PostCode FROM Organisations WHERE Name LIKE ?"); + $statementValidated->execute('%'.$searchName.'%'); + + while (my ($id, $name, $address, $postcode) = $statementValidated->fetchrow_array()) { + push(@validatedOrgs, $self->create_hash($id,$name,$address,$postcode)); + } + } + + #$self->app->log->debug( "Orgs: " . Dumper @validatedOrgs ); + + my @unvalidatedOrgs = (); + { + my $statementUnvalidated = $self->db->prepare("SELECT PendingOrganisationId, Name, FullAddress, Postcode FROM PendingOrganisations WHERE Name LIKE ? AND UserSubmitted_FK = ?"); + $statementUnvalidated->execute('%'.$searchName.'%', $userId); + + while (my ($id, $name, $fullAddress, $postcode) = $statementUnvalidated->fetchrow_array()) { + push(@unvalidatedOrgs, $self->create_hash($id, $name, $fullAddress, $postcode)); + } + } + + $self->app->log->debug('Path Success: file:' . __FILE__ . ', line: ' . __LINE__); + return $self->render( json => { + success => Mojo::JSON->true, + unvalidated => \@unvalidatedOrgs, + validated => \@validatedOrgs, + }, + status => 200,); + +} + +1; diff --git a/lib/Pear/LocalLoop/Schema.pm b/lib/Pear/LocalLoop/Schema.pm new file mode 100644 index 0000000..cea012b --- /dev/null +++ b/lib/Pear/LocalLoop/Schema.pm @@ -0,0 +1,20 @@ +use utf8; +package Pear::LocalLoop::Schema; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +use strict; +use warnings; + +use base 'DBIx::Class::Schema'; + +__PACKAGE__->load_namespaces; + + +# Created by DBIx::Class::Schema::Loader v0.07046 @ 2017-02-24 17:32:21 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:JNwB+HEKyNyE5ZP1Br1pog + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/AccountToken.pm b/lib/Pear/LocalLoop/Schema/Result/AccountToken.pm new file mode 100644 index 0000000..7bace3b --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/AccountToken.pm @@ -0,0 +1,98 @@ +use utf8; +package Pear::LocalLoop::Schema::Result::AccountToken; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Pear::LocalLoop::Schema::Result::AccountToken + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("AccountTokens"); + +=head1 ACCESSORS + +=head2 accounttokenid + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 accounttokenname + + data_type: 'text' + is_nullable: 0 + +=head2 used + + data_type: 'integer' + default_value: 0 + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "accounttokenid", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "accounttokenname", + { data_type => "text", is_nullable => 0 }, + "used", + { data_type => "integer", default_value => 0, is_nullable => 0 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("accounttokenid"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("accounttokenname_unique", ["accounttokenname"]); + + +# Created by DBIx::Class::Schema::Loader v0.07046 @ 2017-02-24 17:32:21 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:MeN6dMZY0drrWk+En7E5Ag + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/Administrator.pm b/lib/Pear/LocalLoop/Schema/Result/Administrator.pm new file mode 100644 index 0000000..fcc190c --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/Administrator.pm @@ -0,0 +1,92 @@ +use utf8; +package Pear::LocalLoop::Schema::Result::Administrator; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Pear::LocalLoop::Schema::Result::Administrator + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("Administrators"); + +=head1 ACCESSORS + +=head2 userid + + data_type: 'integer' + is_auto_increment: 1 + is_foreign_key: 1 + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "userid", + { + data_type => "integer", + is_auto_increment => 1, + is_foreign_key => 1, + is_nullable => 0, + }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("userid"); + +=head1 RELATIONS + +=head2 userid + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "userid", + "Pear::LocalLoop::Schema::Result::User", + { userid => "userid" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07046 @ 2017-02-24 17:32:21 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:YLzlp1ru+1id/O4bTJGqbw + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/AgeRange.pm b/lib/Pear/LocalLoop/Schema/Result/AgeRange.pm new file mode 100644 index 0000000..1adf168 --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/AgeRange.pm @@ -0,0 +1,107 @@ +use utf8; +package Pear::LocalLoop::Schema::Result::AgeRange; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Pear::LocalLoop::Schema::Result::AgeRange + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("AgeRanges"); + +=head1 ACCESSORS + +=head2 agerangeid + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 agerangestring + + data_type: 'text' + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "agerangeid", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "agerangestring", + { data_type => "text", is_nullable => 0 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("agerangeid"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("agerangestring_unique", ["agerangestring"]); + +=head1 RELATIONS + +=head2 customers + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "customers", + "Pear::LocalLoop::Schema::Result::Customer", + { "foreign.agerange_fk" => "self.agerangeid" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07046 @ 2017-02-24 17:32:21 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:4zGNm0RlwptF9hlj9oErVA + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/Customer.pm b/lib/Pear/LocalLoop/Schema/Result/Customer.pm new file mode 100644 index 0000000..c59f178 --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/Customer.pm @@ -0,0 +1,137 @@ +use utf8; +package Pear::LocalLoop::Schema::Result::Customer; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Pear::LocalLoop::Schema::Result::Customer + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("Customers"); + +=head1 ACCESSORS + +=head2 customerid + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 username + + data_type: 'text' + is_nullable: 0 + +=head2 agerange_fk + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 postcode + + data_type: 'text' + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "customerid", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "username", + { data_type => "text", is_nullable => 0 }, + "agerange_fk", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "postcode", + { data_type => "text", is_nullable => 0 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("customerid"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("username_unique", ["username"]); + +=head1 RELATIONS + +=head2 agerange_fk + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "agerange_fk", + "Pear::LocalLoop::Schema::Result::AgeRange", + { agerangeid => "agerange_fk" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + +=head2 user + +Type: might_have + +Related object: L + +=cut + +__PACKAGE__->might_have( + "user", + "Pear::LocalLoop::Schema::Result::User", + { "foreign.customerid_fk" => "self.customerid" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07046 @ 2017-02-24 17:32:21 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ocoCGZYvw9O9wxzr14okiQ + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/Organisation.pm b/lib/Pear/LocalLoop/Schema/Result/Organisation.pm new file mode 100644 index 0000000..2014575 --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/Organisation.pm @@ -0,0 +1,122 @@ +use utf8; +package Pear::LocalLoop::Schema::Result::Organisation; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Pear::LocalLoop::Schema::Result::Organisation + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("Organisations"); + +=head1 ACCESSORS + +=head2 organisationalid + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 name + + data_type: 'text' + is_nullable: 0 + +=head2 fulladdress + + data_type: 'text' + is_nullable: 0 + +=head2 postcode + + data_type: 'text' + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "organisationalid", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "name", + { data_type => "text", is_nullable => 0 }, + "fulladdress", + { data_type => "text", is_nullable => 0 }, + "postcode", + { data_type => "text", is_nullable => 0 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("organisationalid"); + +=head1 RELATIONS + +=head2 transactions + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "transactions", + "Pear::LocalLoop::Schema::Result::Transaction", + { "foreign.sellerorganisationid_fk" => "self.organisationalid" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 user + +Type: might_have + +Related object: L + +=cut + +__PACKAGE__->might_have( + "user", + "Pear::LocalLoop::Schema::Result::User", + { "foreign.organisationalid_fk" => "self.organisationalid" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07046 @ 2017-02-24 17:32:21 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:p9FzM/H5YQbo2d0lN/DfCg + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/PendingOrganisation.pm b/lib/Pear/LocalLoop/Schema/Result/PendingOrganisation.pm new file mode 100644 index 0000000..e9790fc --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/PendingOrganisation.pm @@ -0,0 +1,139 @@ +use utf8; +package Pear::LocalLoop::Schema::Result::PendingOrganisation; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Pear::LocalLoop::Schema::Result::PendingOrganisation + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("PendingOrganisations"); + +=head1 ACCESSORS + +=head2 pendingorganisationid + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 usersubmitted_fk + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 timedatesubmitted + + data_type: 'integer' + is_nullable: 0 + +=head2 name + + data_type: 'text' + is_nullable: 0 + +=head2 fulladdress + + data_type: 'text' + is_nullable: 1 + +=head2 postcode + + data_type: 'text' + is_nullable: 1 + +=cut + +__PACKAGE__->add_columns( + "pendingorganisationid", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "usersubmitted_fk", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "timedatesubmitted", + { data_type => "datetime", is_nullable => 0 }, + "name", + { data_type => "text", is_nullable => 0 }, + "fulladdress", + { data_type => "text", is_nullable => 1 }, + "postcode", + { data_type => "text", is_nullable => 1 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("pendingorganisationid"); + +=head1 RELATIONS + +=head2 pending_transactions + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "pending_transactions", + "Pear::LocalLoop::Schema::Result::PendingTransaction", + { + "foreign.pendingsellerorganisationid_fk" => "self.pendingorganisationid", + }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 usersubmitted_fk + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "usersubmitted_fk", + "Pear::LocalLoop::Schema::Result::User", + { userid => "usersubmitted_fk" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07046 @ 2017-02-24 17:32:21 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:ekEOt+ESCwQxrqqlMurehA + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/PendingTransaction.pm b/lib/Pear/LocalLoop/Schema/Result/PendingTransaction.pm new file mode 100644 index 0000000..07ea25c --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/PendingTransaction.pm @@ -0,0 +1,152 @@ +use utf8; +package Pear::LocalLoop::Schema::Result::PendingTransaction; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Pear::LocalLoop::Schema::Result::PendingTransaction + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("PendingTransactions"); + +=head1 ACCESSORS + +=head2 pendingtransactionid + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 buyeruserid_fk + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 pendingsellerorganisationid_fk + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 valuemicrocurrency + + data_type: 'integer' + is_nullable: 0 + +=head2 proofimage + + data_type: 'text' + is_nullable: 0 + +=head2 timedatesubmitted + + data_type: 'integer' + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "pendingtransactionid", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "buyeruserid_fk", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "pendingsellerorganisationid_fk", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "valuemicrocurrency", + { data_type => "integer", is_nullable => 0 }, + "proofimage", + { data_type => "text", is_nullable => 0 }, + "timedatesubmitted", + { data_type => "datetime", is_nullable => 0 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("pendingtransactionid"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("proofimage_unique", ["proofimage"]); + +=head1 RELATIONS + +=head2 buyeruserid_fk + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "buyeruserid_fk", + "Pear::LocalLoop::Schema::Result::User", + { userid => "buyeruserid_fk" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + +=head2 pendingsellerorganisationid_fk + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "pendingsellerorganisationid_fk", + "Pear::LocalLoop::Schema::Result::PendingOrganisation", + { pendingorganisationid => "pendingsellerorganisationid_fk" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07046 @ 2017-02-24 17:32:21 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:zwtxzfW5uB4FNA6mKdFOvg + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/SessionToken.pm b/lib/Pear/LocalLoop/Schema/Result/SessionToken.pm new file mode 100644 index 0000000..3458fdb --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/SessionToken.pm @@ -0,0 +1,122 @@ +use utf8; +package Pear::LocalLoop::Schema::Result::SessionToken; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Pear::LocalLoop::Schema::Result::SessionToken + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("SessionTokens"); + +=head1 ACCESSORS + +=head2 sessiontokenid + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 sessiontokenname + + data_type: 'text' + is_nullable: 0 + +=head2 useridassignedto_fk + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 expiredatetime + + data_type: 'integer' + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "sessiontokenid", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "sessiontokenname", + { data_type => "text", is_nullable => 0 }, + "useridassignedto_fk", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "expiredatetime", + { data_type => "datetime", is_nullable => 0 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("sessiontokenid"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("sessiontokenname_unique", ["sessiontokenname"]); + +=head1 RELATIONS + +=head2 useridassignedto_fk + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "useridassignedto_fk", + "Pear::LocalLoop::Schema::Result::User", + { userid => "useridassignedto_fk" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07046 @ 2017-02-24 17:32:21 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:/mNAPeSmfsDSIpey+eUucg + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/Transaction.pm b/lib/Pear/LocalLoop/Schema/Result/Transaction.pm new file mode 100644 index 0000000..9f22a25 --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/Transaction.pm @@ -0,0 +1,152 @@ +use utf8; +package Pear::LocalLoop::Schema::Result::Transaction; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Pear::LocalLoop::Schema::Result::Transaction + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("Transactions"); + +=head1 ACCESSORS + +=head2 transactionid + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 buyeruserid_fk + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 sellerorganisationid_fk + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 0 + +=head2 valuemicrocurrency + + data_type: 'integer' + is_nullable: 0 + +=head2 proofimage + + data_type: 'text' + is_nullable: 0 + +=head2 timedatesubmitted + + data_type: 'integer' + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "transactionid", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "buyeruserid_fk", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "sellerorganisationid_fk", + { data_type => "integer", is_foreign_key => 1, is_nullable => 0 }, + "valuemicrocurrency", + { data_type => "integer", is_nullable => 0 }, + "proofimage", + { data_type => "text", is_nullable => 0 }, + "timedatesubmitted", + { data_type => "datetime", is_nullable => 0 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("transactionid"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("proofimage_unique", ["proofimage"]); + +=head1 RELATIONS + +=head2 buyeruserid_fk + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "buyeruserid_fk", + "Pear::LocalLoop::Schema::Result::User", + { userid => "buyeruserid_fk" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + +=head2 sellerorganisationid_fk + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "sellerorganisationid_fk", + "Pear::LocalLoop::Schema::Result::Organisation", + { organisationalid => "sellerorganisationid_fk" }, + { is_deferrable => 0, on_delete => "NO ACTION", on_update => "NO ACTION" }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07046 @ 2017-02-24 17:32:21 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:CfPoE2egoSD1tKo7fYjZdg + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/lib/Pear/LocalLoop/Schema/Result/User.pm b/lib/Pear/LocalLoop/Schema/Result/User.pm new file mode 100644 index 0000000..b8b35e7 --- /dev/null +++ b/lib/Pear/LocalLoop/Schema/Result/User.pm @@ -0,0 +1,261 @@ +use utf8; +package Pear::LocalLoop::Schema::Result::User; + +# Created by DBIx::Class::Schema::Loader +# DO NOT MODIFY THE FIRST PART OF THIS FILE + +=head1 NAME + +Pear::LocalLoop::Schema::Result::User + +=cut + +use strict; +use warnings; + +use base 'DBIx::Class::Core'; + +=head1 COMPONENTS LOADED + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->load_components("InflateColumn::DateTime"); + +=head1 TABLE: C + +=cut + +__PACKAGE__->table("Users"); + +=head1 ACCESSORS + +=head2 userid + + data_type: 'integer' + is_auto_increment: 1 + is_nullable: 0 + +=head2 customerid_fk + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 1 + +=head2 organisationalid_fk + + data_type: 'integer' + is_foreign_key: 1 + is_nullable: 1 + +=head2 email + + data_type: 'text' + is_nullable: 0 + +=head2 joindate + + data_type: 'integer' + is_nullable: 0 + +=head2 hashedpassword + + data_type: 'text' + is_nullable: 0 + +=cut + +__PACKAGE__->add_columns( + "userid", + { data_type => "integer", is_auto_increment => 1, is_nullable => 0 }, + "customerid_fk", + { data_type => "integer", is_foreign_key => 1, is_nullable => 1 }, + "organisationalid_fk", + { data_type => "integer", is_foreign_key => 1, is_nullable => 1 }, + "email", + { data_type => "text", is_nullable => 0 }, + "joindate", + { data_type => "datetime", is_nullable => 0 }, + "hashedpassword", + { data_type => "text", is_nullable => 0 }, +); + +=head1 PRIMARY KEY + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->set_primary_key("userid"); + +=head1 UNIQUE CONSTRAINTS + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("customerid_fk_unique", ["customerid_fk"]); + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("email_unique", ["email"]); + +=head2 C + +=over 4 + +=item * L + +=back + +=cut + +__PACKAGE__->add_unique_constraint("organisationalid_fk_unique", ["organisationalid_fk"]); + +=head1 RELATIONS + +=head2 administrator + +Type: might_have + +Related object: L + +=cut + +__PACKAGE__->might_have( + "administrator", + "Pear::LocalLoop::Schema::Result::Administrator", + { "foreign.userid" => "self.userid" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 customerid_fk + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "customerid_fk", + "Pear::LocalLoop::Schema::Result::Customer", + { customerid => "customerid_fk" }, + { + is_deferrable => 0, + join_type => "LEFT", + on_delete => "NO ACTION", + on_update => "NO ACTION", + }, +); + +=head2 organisationalid_fk + +Type: belongs_to + +Related object: L + +=cut + +__PACKAGE__->belongs_to( + "organisationalid_fk", + "Pear::LocalLoop::Schema::Result::Organisation", + { organisationalid => "organisationalid_fk" }, + { + is_deferrable => 0, + join_type => "LEFT", + on_delete => "NO ACTION", + on_update => "NO ACTION", + }, +); + +=head2 pending_organisations + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "pending_organisations", + "Pear::LocalLoop::Schema::Result::PendingOrganisation", + { "foreign.usersubmitted_fk" => "self.userid" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 pending_transactions + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "pending_transactions", + "Pear::LocalLoop::Schema::Result::PendingTransaction", + { "foreign.buyeruserid_fk" => "self.userid" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 session_tokens + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "session_tokens", + "Pear::LocalLoop::Schema::Result::SessionToken", + { "foreign.useridassignedto_fk" => "self.userid" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + +=head2 transactions + +Type: has_many + +Related object: L + +=cut + +__PACKAGE__->has_many( + "transactions", + "Pear::LocalLoop::Schema::Result::Transaction", + { "foreign.buyeruserid_fk" => "self.userid" }, + { cascade_copy => 0, cascade_delete => 0 }, +); + + +# Created by DBIx::Class::Schema::Loader v0.07046 @ 2017-02-24 17:32:21 +# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:qjAgtJR1zaUr00HsiR1aPw + + +# You can replace this text with custom code or comments, and it will be preserved on regeneration +1; diff --git a/foodloopserver.conf b/pear-local_loop.conf similarity index 100% rename from foodloopserver.conf rename to pear-local_loop.conf diff --git a/foodloopserver.development.conf b/pear-local_loop.development.conf similarity index 100% rename from foodloopserver.development.conf rename to pear-local_loop.development.conf diff --git a/script/make_schema b/script/make_schema new file mode 100755 index 0000000..151c354 --- /dev/null +++ b/script/make_schema @@ -0,0 +1,83 @@ +#! /usr/bin/env perl + +use strict; +use warnings; + +=head1 NAME + +make_schema - Make or update a schema from the database + +=cut + +use Getopt::Long; +use Pod::Usage; +use FindBin qw/ $Bin /; + +use DBIx::Class::Schema::Loader qw/ make_schema_at /; + +my $username = ''; +my $password = ''; +my $connection; +my $schema = 'public'; +my $dbic_schema = 'MyApp::Schema'; +my $help = 0; +my $man = 0; +my $overwrite = 0; +my $verbose = 0; + +GetOptions ( + "username|u=s" => \$username, + "password|p=s" => \$password, + "connection|c=s" => \$connection, + "schema|s=s" => \$schema, + "dbic|d=s" => \$dbic_schema, + "force" => \$overwrite, + "verbose" => \$verbose, + "help|?" => \$help, + "man" => \$man, +) or pod2usage(2); + +pod2usage(0) if $help; +pod2usage(-exitval => 0, -verbose => 2) if $man; + +pod2usage( + -message => "Must define a connection", + -exitval => 2, +) unless defined $connection; + +=head1 SYNOPSIS + +make_schema [--help|?] [--man] --connection|-c [--user|-u ] [--pass|-p ] + + Options: + --help Print Brief Help + --man Print Full Documentation + --connection The DBI connection string to use + --user [Optional] Username for the DBI connection + --pass [Optional] Password for the DBI connection + --schema [Optional - default 'public'] Schema to dump from + --dbic [Optional - default 'MyApp::Schema'] Package namespace for your DBIC Schema + --force [Optional] will overwrite modifications in the schema + --verbose [Optional] Print out all tables during processing + +=cut + +make_schema_at( + $dbic_schema, + { + db_schema => $schema, + relationships => 1, + dump_directory => "$Bin/../lib", + debug => 1, + overwrite_modifications => $overwrite, + components => [qw/ + InflateColumn::DateTime + /], + }, + [ + $connection, + $username, + $password, + ], +); + diff --git a/t/admin-approve.t b/t/admin-approve.t index ed6cf50..a296fd0 100644 --- a/t/admin-approve.t +++ b/t/admin-approve.t @@ -7,9 +7,7 @@ use FindBin; $ENV{MOJO_MODE} = 'development'; $ENV{MOJO_LOG_LEVEL} = 'debug'; -require "$FindBin::Bin/../foodloopserver.pl"; - -my $t = Test::Mojo->new; +my $t = Test::Mojo->new("Pear::LocalLoop"); my $dbh = $t->app->db; diff --git a/t/basic.t b/t/basic.t index ccabc36..c0c82aa 100644 --- a/t/basic.t +++ b/t/basic.t @@ -2,9 +2,7 @@ use Test::More; use Test::Mojo; use FindBin; -require "$FindBin::Bin/../foodloopserver.pl"; - -my $t = Test::Mojo->new; +my $t = Test::Mojo->new("Pear::LocalLoop"); $t->get_ok('/login')->status_is(200)->content_like(qr/login page/); done_testing(); diff --git a/t/login.t b/t/login.t index 9ff8616..f4f7fd0 100644 --- a/t/login.t +++ b/t/login.t @@ -8,9 +8,7 @@ use FindBin; $ENV{MOJO_MODE} = 'development'; $ENV{MOJO_LOG_LEVEL} = 'debug'; -require "$FindBin::Bin/../foodloopserver.pl"; - -my $t = Test::Mojo->new; +my $t = Test::Mojo->new("Pear::LocalLoop"); my $dbh = $t->app->db; diff --git a/t/register.t b/t/register.t index fd91d8c..e49ece9 100644 --- a/t/register.t +++ b/t/register.t @@ -7,9 +7,7 @@ use FindBin; $ENV{MOJO_MODE} = 'development'; $ENV{MOJO_LOG_LEVEL} = 'debug'; -require "$FindBin::Bin/../foodloopserver.pl"; - -my $t = Test::Mojo->new; +my $t = Test::Mojo->new("Pear::LocalLoop"); my $dbh = $t->app->db; diff --git a/t/search.t b/t/search.t index 4a852a9..97fe477 100644 --- a/t/search.t +++ b/t/search.t @@ -9,9 +9,7 @@ use FindBin; $ENV{MOJO_MODE} = 'development'; $ENV{MOJO_LOG_LEVEL} = 'debug'; -require "$FindBin::Bin/../foodloopserver.pl"; - -my $t = Test::Mojo->new; +my $t = Test::Mojo->new("Pear::LocalLoop"); my $dbh = $t->app->db; diff --git a/t/upload.t b/t/upload.t index 8a1dc75..223a1c8 100644 --- a/t/upload.t +++ b/t/upload.t @@ -7,9 +7,7 @@ use FindBin; $ENV{MOJO_MODE} = 'development'; $ENV{MOJO_LOG_LEVEL} = 'debug'; -require "$FindBin::Bin/../foodloopserver.pl"; - -my $t = Test::Mojo->new; +my $t = Test::Mojo->new("Pear::LocalLoop"); my $dbh = $t->app->db;