diff --git a/cpanfile b/cpanfile index f41ce5c..5ce2d2e 100644 --- a/cpanfile +++ b/cpanfile @@ -5,3 +5,4 @@ requires 'Mojo::JSON'; requires 'Email::Valid'; requires 'ORM::Date'; requires 'Authen::Passphrase::BlowfishCrypt'; +requires 'Time::Fake'; diff --git a/dropschema.sql b/dropschema.sql index 5e90f8c..42c35f3 100644 --- a/dropschema.sql +++ b/dropschema.sql @@ -1,4 +1,5 @@ -DROP TABLE IF EXISTS Tokens; +DROP TABLE IF EXISTS SessionTokens; +DROP TABLE IF EXISTS AccountTokens; DROP TABLE IF EXISTS Transactions; DROP TABLE IF EXISTS Users; DROP TABLE IF EXISTS Customers; diff --git a/foodloopserver.pl b/foodloopserver.pl index ce01ef2..28498de 100644 --- a/foodloopserver.pl +++ b/foodloopserver.pl @@ -20,15 +20,20 @@ my $dbh = DBI->connect($config->{dsn},$config->{user},$config->{pass}) or die "C $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; - $self->render(text => 'If you are seeing this, then the server is running.'); + return $self->render(text => 'If you are seeing this, then the server is running.', success => Mojo::JSON->true); }; post '/upload' => sub { @@ -117,7 +122,7 @@ post '/register' => sub { }, status => 400,); #Malformed request } - elsif ( ! ($username =~ m/^[A-Za-z0-9]+$/)){ + elsif ( ! ($self->valid_username($username))){ $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); return $self->render( json => { success => Mojo::JSON->false, @@ -143,7 +148,7 @@ post '/register' => sub { }, status => 400,); #Malformed request } - elsif ( ! Email::Valid->address($email)){ + elsif ( ! $self->valid_email($email)){ $self->app->log->debug('Path Error: file:' . __FILE__ . ', line: ' . __LINE__); return $self->render( json => { success => Mojo::JSON->false, @@ -329,6 +334,150 @@ post '/edit' => sub { }; +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; @@ -355,6 +504,17 @@ post '/fetchuser' => sub { }); }; + +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_account_by_username => sub { my ( $self, $username ) = @_; @@ -365,11 +525,125 @@ helper get_account_by_username => sub { ); }; +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(TokenId) FROM Tokens WHERE TokenName = ? AND Used = 0", undef, ($token)); + my ( $out ) = $self->db->selectrow_array("SELECT COUNT(AccountTokenId) FROM AccountTokens WHERE AccountTokenName = ? AND Used = 0", undef, ($token)); return $out != 0; @@ -387,12 +661,19 @@ helper get_age_foreign_key => sub { 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)); - #print "-". Dumper($out) ."-"; return $out != 0; }; @@ -400,18 +681,16 @@ helper does_username_exist => sub { helper does_email_exist => sub { my ( $self, $email ) = @_; - return defined ($self->db->selectrow_hashref( - "SELECT Email FROM Users WHERE Email = ?", - {}, - $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 Tokens SET Used = 1 WHERE TokenName = ? AND Used = 0 "); + 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"; @@ -420,7 +699,7 @@ helper set_token_as_used => sub { }; helper generate_hashed_password => sub { - my ( $self, $password) = @_; + my ( $self, $password ) = @_; my $ppr = Authen::Passphrase::BlowfishCrypt->new( cost => 8, salt_random => 1, @@ -433,14 +712,10 @@ helper generate_hashed_password => sub { helper check_password_email => sub{ my ( $self, $email, $password) = @_; - my $statement = $self->db->prepare("SELECT HashedPassword FROM Users WHERE Email = ?"); - my $result -> execute($email); - my ($hashedPassword) = $result->fetchrow_array; - + 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; diff --git a/schema.sql b/schema.sql index 54ede92..8acd729 100644 --- a/schema.sql +++ b/schema.sql @@ -45,8 +45,16 @@ CREATE TABLE Transactions ( FOREIGN KEY (SellerOrganisationId_FK) REFERENCES Organisations (OrganisationalId) ); -CREATE TABLE Tokens ( - TokenId INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, - TokenName TEXT UNIQUE NOT NULL, +CREATE TABLE AccountTokens ( + AccountTokenId INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + AccountTokenName TEXT UNIQUE NOT NULL, Used INTEGER NOT NULL DEFAULT 0 ); + +CREATE TABLE SessionTokens ( + SessionTokenId INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE NOT NULL, + SessionTokenName TEXT UNIQUE NOT NULL, + UserIdAssignedTo_FK INTEGER NOT NULL, + ExpireDateTime INTEGER NOT NULL, + FOREIGN KEY (UserIdAssignedTo_FK) REFERENCES Users (UserId) +); diff --git a/t/basic.t b/t/basic.t index f6c8676..ccabc36 100644 --- a/t/basic.t +++ b/t/basic.t @@ -5,6 +5,6 @@ use FindBin; require "$FindBin::Bin/../foodloopserver.pl"; my $t = Test::Mojo->new; -$t->get_ok('/')->status_is(200)->content_like(qr/server/); +$t->get_ok('/login')->status_is(200)->content_like(qr/login page/); -done_testing(); \ No newline at end of file +done_testing(); diff --git a/t/login.t b/t/login.t new file mode 100644 index 0000000..9ff8616 --- /dev/null +++ b/t/login.t @@ -0,0 +1,311 @@ +use Test::More; +use Test::Mojo; +use Mojo::JSON; +use Time::Fake; + +use FindBin; + +$ENV{MOJO_MODE} = 'development'; +$ENV{MOJO_LOG_LEVEL} = 'debug'; + +require "$FindBin::Bin/../foodloopserver.pl"; + +my $t = Test::Mojo->new; + +my $dbh = $t->app->db; + +#Dump all pf the test tables and start again. +my $sqlDeployment = Mojo::File->new("$FindBin::Bin/../dropschema.sql")->slurp; +for (split ';', $sqlDeployment){ + $dbh->do($_) or die $dbh->errstr; +} + +my $sqlDeployment = Mojo::File->new("$FindBin::Bin/../schema.sql")->slurp; +for (split ';', $sqlDeployment){ + $dbh->do($_) or die $dbh->errstr; +} + +my $accountToken = 'a'; +my $tokenStatement = $dbh->prepare('INSERT INTO AccountTokens (AccountTokenName) VALUES (?)'); +$tokenStatement->execute($accountToken); + +my $sessionTimeSeconds = 60 * 60 * 24 * 7; #1 week. +my $sessionTokenJsonName = 'sessionToken'; +my $sessionExpiresJsonName = 'sessionExpires'; + + +#This depends on "register.t" working + +#Valid customer, this also tests that redirects are disabled for register. +print "test 1 - Initial create user account\n"; +my $email = 'rufus@shinra.energy'; +my $password = 'MakoGold'; +my $testJson = { + 'usertype' => 'customer', + 'token' => $accountToken, + 'username' => 'RufusShinra', + 'email' => $email, + 'postcode' => 'LA1 1AA', + 'password' => $password, + 'age' => '20-35' +}; +$t->post_ok('/register' => json => $testJson) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true); + + +#Test login, this also checks that redirects are disabled for login when logged out. +print "test 2 - Login (cookies)\n"; +$testJson = { + 'email' => $email, + 'password' => $password, +}; +$t->post_ok('/login' => json => $testJson) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true) + ->json_has("/$sessionTokenJsonName") + ->json_has("/$sessionExpiresJsonName"); + +print "test 3 - Login, no redirect on login paths (cookies)\n"; +#No redirect, as you're logged in. +$t->get_ok('/') + ->status_is(200); + +my $location_is = sub { + my ($t, $value, $desc) = @_; + $desc ||= "Location: $value"; + local $Test::Builder::Level = $Test::Builder::Level + 1; + return $t->success(is($t->tx->res->headers->location, $value, $desc)); +}; + +print "test 4 - Login, redirect to root as already logged in (cookies)\n"; +#Check for redirect to root when logged in. +$t->get_ok('/login') + ->status_is(303) + ->$location_is('/'); + + +#Does login/logout work with a cookie based session. +print "test 5 - Logout (cookies)\n"; +$t->post_ok('/logout') + ->status_is(200) + ->json_is('/success', Mojo::JSON->true) + ->content_like(qr/you were successfully logged out/i); + +$t->reset_session; + +#Login. +print "test 6 - Login (json)\n"; +$testJson = { + 'email' => $email, + 'password' => $password, +}; +$t->post_ok('/login' => json => $testJson) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true) + ->json_has("/$sessionTokenJsonName") + ->json_has("/$sessionExpiresJsonName"); + +my $sessionJsonTest = $t->tx->res->json; +my $expires = $sessionJsonTest->{$sessionExpiresJsonName}; +my $sessionToken = $sessionJsonTest->{$sessionTokenJsonName}; + +#Reset the current state so you are still logged in but there are no cookies. +$t->reset_session; + +#Redirect, as no cookies are set +print "test 7 - Login, no cookies or json redirect to login\n"; +$t->get_ok('/') + ->status_is(303) + ->$location_is('/login'); + +print "test 8 - Login, no redirect on login paths (json)\n"; +$t->get_ok('/' => json => {$sessionTokenJsonName => $sessionToken}) + ->status_is(200); + +#No token send so redirect +print "test 9 - Logout, no cookies or json\n"; +$t->post_ok('/logout') + ->status_is(303) + ->$location_is('/login'); + +#Token sent logout +print "test 10 - Logout, (json)\n"; +$t->post_ok('/logout' => json => {$sessionTokenJsonName => $sessionToken}) + ->status_is(200); + +#Send logged out expired token, +print "test 11 - Logout,expired session redirect (json)\n"; +$t->post_ok('/logout' => json => {$sessionTokenJsonName => $sessionToken}) + ->status_is(303) + ->$location_is('/login'); + +$t->reset_session; + +#TODO it's difficult to test cookies as they automatically get removed. + +#Login. +print "test 12 - Login test with fake time (json)\n"; +$testJson = { + 'email' => $email, + 'password' => $password, +}; +$t->post_ok('/login' => json => $testJson) + ->status_is(200) + ->json_is('/success', Mojo::JSON->true) + ->json_has("/$sessionTokenJsonName") + ->json_has("/$sessionExpiresJsonName"); + +$sessionJsonTest = $t->tx->res->json; +$expires = $sessionJsonTest->{$sessionExpiresJsonName}; +$sessionToken = $sessionJsonTest->{$sessionTokenJsonName}; + +#Clear cookies +$t->reset_session; + +#Offset time +Time::Fake->offset("+".($sessionTimeSeconds * 2)."s"); + +#Send time expired token, +print "test 13 - Fake time expired session redirect (json)\n"; +$t->post_ok('/logout' => json => {$sessionTokenJsonName => $sessionToken}) + ->status_is(303) + ->$location_is('/login'); + +Time::Fake->reset(); + +$t->reset_session; + +#Attempt to logout without any session +# This is different from the one above as it's has no state. +print "test 14 - Logout, no session\n"; +$t->post_ok('/logout') + ->status_is(303) + ->$location_is('/login'); + +#Clear the session state +$t->reset_session; + +#Not logged in, redirect to login. +print "test 15 - Not logged in, get request redirect to login\n"; +$t->get_ok('/') + ->status_is(303) + ->$location_is('/login'); + +$t->reset_session; + +#Not logged in, redirect to login. +print "test 16 - Not logged in, get request one redirection is ok.\n"; +$t->ua->max_redirects(1); +$t->get_ok('/') + ->status_is(200); +$t->ua->max_redirects(0); + +$t->reset_session; + +#Not logged in, redirect to login. +print "test 17 - Not logged in, post request redirect to login\n"; +$t->post_ok('/') + ->status_is(303) + ->$location_is('/login'); + +$t->reset_session; + +#Not logged in, redirect to login. +print "test 18 - Not logged in, post request one redirection is ok.\n"; +$t->ua->max_redirects(1); +$t->post_ok('/') + ->status_is(200); +$t->ua->max_redirects(0); + +$t->reset_session; + +#Here on is just input params checking, no session testing. + +#Test no JSON sent. +print "test 19 - No JSON sent.\n"; +$t->post_ok('/login') + ->status_is(400) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/No json sent/i); + +$t->reset_session; + +#Test no email sent +print "test 20 - Email missing\n"; +$testJson = { + 'password' => $password, +}; +$t->post_ok('/login' => json => $testJson) + ->status_is(400) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/No email sent/i); + +$t->reset_session; + +#Invalid email +print "test 21 - Invalid email\n"; +$testJson = { + 'email' => ($email . '@'), + 'password' => $password, +}; +$t->post_ok('/login' => json => $testJson) + ->status_is(400) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/email is invalid/i); + +$t->reset_session; + +#Test no password sent +print "test 22 - No password sent.\n"; +$testJson = { + 'email' => $email, +}; +$t->post_ok('/login' => json => $testJson) + ->status_is(400) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/No password sent/i); + +$t->reset_session; + +#Email does not exist +print "test 23 - Email does not exist in the database\n"; +$testJson = { + 'email' => 'heidegger@shinra.energy', + 'password' => $password, +}; +$t->post_ok('/login' => json => $testJson) + ->status_is(401) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/Email or password is invalid/i); + +$t->reset_session; + +#Password is wrong +print "test 24 - Password is wrong\n"; +$testJson = { + 'email' => $email, + 'password' => ($password . 'MoreText'), +}; +$t->post_ok('/login' => json => $testJson) + ->status_is(401) + ->json_is('/success', Mojo::JSON->false) + ->content_like(qr/Email or password is invalid/i); + +$t->reset_session; + + + +#$testJson = { +# 'email' => $email, +# 'password' => $password, +#}; +#$t->post_ok('/login' => json => $testJson) +# ->status_is(200) +# ->json_is('/success', Mojo::JSON->true); + + +#TODO expire session. + + +done_testing(); diff --git a/t/register.t b/t/register.t index bd59e91..fd91d8c 100644 --- a/t/register.t +++ b/t/register.t @@ -28,7 +28,7 @@ for (split ';', $sqlDeployment){ my @names = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'); my @emails = ('a@a.com', 'b@a.com', 'c@a.com', 'd@a.com', 'e@a.com', 'f@a.com', 'g@a.com', 'h@a.com', 'i@a.com', 'j@a.com', 'k@a.com', 'l@a.com', 'm@a.com', 'n@a.com', 'o@a.com', 'p@a.com', 'q@a.com', 'r@a.com', 's@a.com', 't@a.com', 'u@a.com', 'v@a.com', 'w@a.com', 'x@a.com', 'y@a.com', 'z@a.com'); my @tokens = ('a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z'); -my $tokenStatement = $dbh->prepare('INSERT INTO Tokens (TokenName) VALUES (?)'); +my $tokenStatement = $dbh->prepare('INSERT INTO AccountTokens (AccountTokenName) VALUES (?)'); foreach (@tokens){ my $rowsAdded = $tokenStatement->execute($_); }