Database support #1
25 changed files with 483 additions and 1 deletions
|
@ -1,4 +1,4 @@
|
|||
{
|
||||
password => '',
|
||||
db_file => 'sql/fotostore.db',
|
||||
invite_code => 'very_secure_invite_code',
|
||||
}
|
39
fotostore.pl
Normal file → Executable file
39
fotostore.pl
Normal file → Executable file
|
@ -81,6 +81,45 @@ get '/logout' => sub {
|
|||
$self->render( text => 'bye' );
|
||||
};
|
||||
|
||||
get '/register' => ( authenticated => 0 ) => sub {
|
||||
|
||||
};
|
||||
|
||||
post '/register' => ( authenticated => 0 ) => sub {
|
||||
my $self = shift;
|
||||
my $username = $self->req->param('username');
|
||||
my $password = $self->req->param('password');
|
||||
my $fullname = $self->req->param('fullname');
|
||||
my $invite = $self->req->param('invite');
|
||||
|
||||
if ($invite eq $config->{'invite_code'}) {
|
||||
#chek that username is not taken
|
||||
my $user = $db->get_user($username);
|
||||
if ($user->{'user_id'} > 0) {
|
||||
$self->render(template => 'error', message => 'Username already taken!');
|
||||
return 0;
|
||||
}
|
||||
|
||||
if ($fullname eq '') {
|
||||
$fullname = $username;
|
||||
}
|
||||
|
||||
my $digest = $sha->add($password);
|
||||
$db->add_user($username, $digest->hexdigest(), $fullname);
|
||||
|
||||
#Authenticate user after add
|
||||
if ( $self->authenticate( $username, $password ) ) {
|
||||
$self->redirect_to('/');
|
||||
}
|
||||
else {
|
||||
$self->render( text => 'Login failed :(' );
|
||||
}
|
||||
|
||||
} else {
|
||||
$self->render(template => 'error', message => 'invalid invite code');
|
||||
}
|
||||
};
|
||||
|
||||
# Display top page
|
||||
get '/' => sub {
|
||||
my $self = shift;
|
||||
|
|
67
lib/FotoStore/DB.pm
Normal file
67
lib/FotoStore/DB.pm
Normal file
|
@ -0,0 +1,67 @@
|
|||
package FotoStore::DB;
|
||||
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
use feature qw(signatures);
|
||||
no warnings qw(experimental::signatures);
|
||||
|
||||
sub new {
|
||||
my $class = shift;
|
||||
my $db_file = shift;
|
||||
|
||||
my $dbh = DBI->connect(sprintf('dbi:SQLite:dbname=%s', $db_file),"","");
|
||||
my $self = {
|
||||
dbh => $dbh,
|
||||
};
|
||||
bless $self, $class;
|
||||
return $self;
|
||||
}
|
||||
|
||||
sub check_user ($self, $nickname, $password) {
|
||||
print STDERR "[$nickname][$password]";
|
||||
my ($user_id) = $self->{'dbh'}->selectrow_array(q~select user_id from users where nickname=? and password=?~, undef, ($nickname, $password));
|
||||
return $user_id;
|
||||
}
|
||||
|
||||
sub get_user ($self, $user_id) {
|
||||
if ($user_id =~ /^\d+$/) {
|
||||
return $self->_get_user_by_user_id($user_id);
|
||||
} else {
|
||||
return $self->_get_user_by_username($user_id);
|
||||
}
|
||||
}
|
||||
|
||||
sub _get_user_by_user_id ($self, $user_id) {
|
||||
my $user_data = $self->{'dbh'}->selectrow_hashref(q~select user_id, nickname, fullname, timestamp from users where user_id=?~, {}, ($user_id));
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
sub _get_user_by_username($self, $username) {
|
||||
my $user_data = $self->{'dbh'}->selectrow_hashref(q~select user_id, nickname, fullname, timestamp from users where nickname=?~, {}, ($username));
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
|
||||
sub add_user($self, $username, $password, $fullname) {
|
||||
my $rows = $self->{'dbh'}->do(q~insert into users (nickname, password, fullname) values (?, ?, ?)~, undef, ($username, $password, $fullname));
|
||||
if ($self->{'dbh'}->errstr) {
|
||||
die $self->{'dbh'}->errstr;
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
|
||||
sub add_file($self, $user_id, $filename) {
|
||||
my $rows = $self->{'dbh'}->do(q~insert into images (owner_id, file_name) values (?, ?)~, undef, ($user_id, $filename));
|
||||
if ($self->{'dbh'}->errstr) {
|
||||
die $self->{'dbh'}->errstr;
|
||||
}
|
||||
return $rows;
|
||||
}
|
||||
|
||||
sub get_files($self, $user_id, $count=20, $start_at=0) {
|
||||
return $self->{'dbh'}->selectall_arrayref(q~select * from images where owner_id=? order by created_time desc~, { Slice => {} }, $user_id );
|
||||
}
|
||||
|
||||
1;
|
9
public/css/main.css
Normal file
9
public/css/main.css
Normal file
|
@ -0,0 +1,9 @@
|
|||
.foto-block {
|
||||
/* border: 1px solid black; */
|
||||
}
|
||||
.foto-block .image {
|
||||
padding: 5px;
|
||||
}
|
||||
.foto-block .foto-notes {
|
||||
padding: 5px;
|
||||
}
|
3
public/file_uploader/.gitignore
vendored
Executable file
3
public/file_uploader/.gitignore
vendored
Executable file
|
@ -0,0 +1,3 @@
|
|||
.DS_Store
|
||||
*.pyc
|
||||
node_modules
|
81
public/file_uploader/.jshintrc
Executable file
81
public/file_uploader/.jshintrc
Executable file
|
@ -0,0 +1,81 @@
|
|||
{
|
||||
"bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.)
|
||||
"camelcase" : true, // true: Identifiers must be in camelCase
|
||||
"curly" : true, // true: Require {} for every new block or scope
|
||||
"eqeqeq" : true, // true: Require triple equals (===) for comparison
|
||||
"forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty()
|
||||
"immed" : true, // true: Require immediate invocations to be wrapped in parens
|
||||
// e.g. `(function () { } ());`
|
||||
"indent" : 4, // {int} Number of spaces to use for indentation
|
||||
"latedef" : true, // true: Require variables/functions to be defined before being used
|
||||
"newcap" : true, // true: Require capitalization of all constructor functions e.g. `new F()`
|
||||
"noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee`
|
||||
"noempty" : true, // true: Prohibit use of empty blocks
|
||||
"nonew" : true, // true: Prohibit use of constructors for side-effects (without assignment)
|
||||
"plusplus" : false, // true: Prohibit use of `++` & `--`
|
||||
"quotmark" : "single", // Quotation mark consistency:
|
||||
// false : do nothing (default)
|
||||
// true : ensure whatever is used is consistent
|
||||
// "single" : require single quotes
|
||||
// "double" : require double quotes
|
||||
"undef" : true, // true: Require all non-global variables to be declared (prevents global leaks)
|
||||
"unused" : true, // true: Require all defined variables be used
|
||||
"strict" : true, // true: Requires all functions run in ES5 Strict Mode
|
||||
"trailing" : true, // true: Prohibit trailing whitespaces
|
||||
"maxparams" : false, // {int} Max number of formal params allowed per function
|
||||
"maxdepth" : false, // {int} Max depth of nested blocks (within functions)
|
||||
"maxstatements" : false, // {int} Max number statements per function
|
||||
"maxcomplexity" : false, // {int} Max cyclomatic complexity per function
|
||||
"maxlen" : false, // {int} Max number of characters per line
|
||||
|
||||
// Relaxing
|
||||
"asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons)
|
||||
"boss" : false, // true: Tolerate assignments where comparisons would be expected
|
||||
"debug" : false, // true: Allow debugger statements e.g. browser breakpoints.
|
||||
"eqnull" : false, // true: Tolerate use of `== null`
|
||||
"es5" : false, // true: Allow ES5 syntax (ex: getters and setters)
|
||||
"esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`)
|
||||
"moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features)
|
||||
// (ex: `for each`, multiple try/catch, function expression…)
|
||||
"evil" : false, // true: Tolerate use of `eval` and `new Function()`
|
||||
"expr" : false, // true: Tolerate `ExpressionStatement` as Programs
|
||||
"funcscope" : false, // true: Tolerate defining variables inside control statements"
|
||||
"globalstrict" : false, // true: Allow global "use strict" (also enables 'strict')
|
||||
"iterator" : false, // true: Tolerate using the `__iterator__` property
|
||||
"lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block
|
||||
"laxbreak" : false, // true: Tolerate possibly unsafe line breakings
|
||||
"laxcomma" : false, // true: Tolerate comma-first style coding
|
||||
"loopfunc" : false, // true: Tolerate functions being defined in loops
|
||||
"multistr" : false, // true: Tolerate multi-line strings
|
||||
"proto" : false, // true: Tolerate using the `__proto__` property
|
||||
"scripturl" : false, // true: Tolerate script-targeted URLs
|
||||
"smarttabs" : false, // true: Tolerate mixed tabs/spaces when used for alignment
|
||||
"shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;`
|
||||
"sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation
|
||||
"supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;`
|
||||
"validthis" : false, // true: Tolerate using this in a non-constructor function
|
||||
|
||||
// Environments
|
||||
"browser" : false, // Web Browser (window, document, etc)
|
||||
"couch" : false, // CouchDB
|
||||
"devel" : false, // Development/debugging (alert, confirm, etc)
|
||||
"dojo" : false, // Dojo Toolkit
|
||||
"jquery" : false, // jQuery
|
||||
"mootools" : false, // MooTools
|
||||
"node" : false, // Node.js
|
||||
"nonstandard" : false, // Widely adopted globals (escape, unescape, etc)
|
||||
"prototypejs" : false, // Prototype and Scriptaculous
|
||||
"rhino" : false, // Rhino
|
||||
"worker" : false, // Web Workers
|
||||
"wsh" : false, // Windows Scripting Host
|
||||
"yui" : false, // Yahoo User Interface
|
||||
|
||||
// Legacy
|
||||
"nomen" : true, // true: Prohibit dangling `_` in variables
|
||||
"onevar" : true, // true: Allow only one `var` statement per function
|
||||
"passfail" : false, // true: Stop on first error
|
||||
"white" : true, // true: Check against strict whitespace and indentation rules
|
||||
|
||||
// Custom Globals
|
||||
"globals" : {} // additional predefined global variables
|
||||
}
|
20
public/file_uploader/.npmignore
Executable file
20
public/file_uploader/.npmignore
Executable file
|
@ -0,0 +1,20 @@
|
|||
*
|
||||
!css/jquery.fileupload-noscript.css
|
||||
!css/jquery.fileupload-ui-noscript.css
|
||||
!css/jquery.fileupload-ui.css
|
||||
!css/jquery.fileupload.css
|
||||
!img/loading.gif
|
||||
!img/progressbar.gif
|
||||
!js/cors/jquery.postmessage-transport.js
|
||||
!js/cors/jquery.xdr-transport.js
|
||||
!js/vendor/jquery.ui.widget.js
|
||||
!js/jquery.fileupload-angular.js
|
||||
!js/jquery.fileupload-audio.js
|
||||
!js/jquery.fileupload-image.js
|
||||
!js/jquery.fileupload-jquery-ui.js
|
||||
!js/jquery.fileupload-process.js
|
||||
!js/jquery.fileupload-ui.js
|
||||
!js/jquery.fileupload-validate.js
|
||||
!js/jquery.fileupload-video.js
|
||||
!js/jquery.fileupload.js
|
||||
!js/jquery.iframe-transport.js
|
14
sql/deploy/album_images.sql
Normal file
14
sql/deploy/album_images.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
-- Deploy fotostore:album_images to sqlite
|
||||
-- requires: images
|
||||
-- requires: albums
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE album_images (
|
||||
record_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
album_id INTEGER REFERENCES albums (album_id),
|
||||
image_id INTEGER REFERENCES images (file_id)
|
||||
);
|
||||
|
||||
|
||||
COMMIT;
|
14
sql/deploy/albums.sql
Normal file
14
sql/deploy/albums.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
-- Deploy fotostore:albums to sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE albums (
|
||||
album_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name STRING NOT NULL,
|
||||
description TEXT,
|
||||
created DATETIME DEFAULT (CURRENT_TIMESTAMP),
|
||||
modified DATETIME DEFAULT (CURRENT_TIMESTAMP),
|
||||
deleted BOOLEAN
|
||||
);
|
||||
|
||||
COMMIT;
|
14
sql/deploy/user_albums.sql
Normal file
14
sql/deploy/user_albums.sql
Normal file
|
@ -0,0 +1,14 @@
|
|||
-- Deploy fotostore:user_albums to sqlite
|
||||
-- requires: users
|
||||
-- requires: albums
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE user_albums (
|
||||
record_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER REFERENCES users (user_id),
|
||||
album_id INTEGER REFERENCES albums (album_id)
|
||||
);
|
||||
|
||||
|
||||
COMMIT;
|
12
sql/deploy/user_images.sql
Normal file
12
sql/deploy/user_images.sql
Normal file
|
@ -0,0 +1,12 @@
|
|||
-- Deploy fotostore:user_images to sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
CREATE TABLE user_images (
|
||||
record_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER REFERENCES users (user_id) ON DELETE CASCADE,
|
||||
image_id INTEGER REFERENCES images (file_id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
|
||||
COMMIT;
|
7
sql/revert/album_images.sql
Normal file
7
sql/revert/album_images.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Revert fotostore:album_images from sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE album_images;
|
||||
|
||||
COMMIT;
|
7
sql/revert/albums.sql
Normal file
7
sql/revert/albums.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Revert fotostore:albums from sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE albums;
|
||||
|
||||
COMMIT;
|
7
sql/revert/user_albums.sql
Normal file
7
sql/revert/user_albums.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Revert fotostore:user_albums from sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE user_albums;
|
||||
|
||||
COMMIT;
|
7
sql/revert/user_images.sql
Normal file
7
sql/revert/user_images.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Revert fotostore:user_images from sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
DROP TABLE user_images;
|
||||
|
||||
COMMIT;
|
7
sql/verify/album_images.sql
Normal file
7
sql/verify/album_images.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Verify fotostore:album_images on sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
select record_id, album_id, image_id from album_images where 0;
|
||||
|
||||
ROLLBACK;
|
7
sql/verify/albums.sql
Normal file
7
sql/verify/albums.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Verify fotostore:albums on sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
select album_id, name, description, created, modified, deleted from albums where 0;
|
||||
|
||||
ROLLBACK;
|
7
sql/verify/user_albums.sql
Normal file
7
sql/verify/user_albums.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Verify fotostore:user_albums on sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
select record_id, album_id, user_id from user_albums where 0;
|
||||
|
||||
ROLLBACK;
|
7
sql/verify/user_images.sql
Normal file
7
sql/verify/user_images.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
-- Verify fotostore:user_images on sqlite
|
||||
|
||||
BEGIN;
|
||||
|
||||
select record_id, user_id, image_id from user_images where 0;
|
||||
|
||||
ROLLBACK;
|
10
templates/error.html.ep
Normal file
10
templates/error.html.ep
Normal file
|
@ -0,0 +1,10 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" >
|
||||
<title>Error</title>
|
||||
</head>
|
||||
<body>
|
||||
<%= $message %>
|
||||
</body>
|
||||
</html>
|
||||
|
54
templates/images_list.html.ep
Normal file
54
templates/images_list.html.ep
Normal file
|
@ -0,0 +1,54 @@
|
|||
<div class="container">
|
||||
<div class="logout"><a href="/logout">Logout</a></div>
|
||||
<div class"upload-form">
|
||||
<input id="fileupload" type="file" name="image" data-url="/upload" multiple>
|
||||
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
|
||||
<script src="/file_uploader/js/vendor/jquery.ui.widget.js"></script>
|
||||
<script src="/file_uploader/js/jquery.iframe-transport.js"></script>
|
||||
<script src="/file_uploader/js/jquery.fileupload.js"></script>
|
||||
<script>
|
||||
$(function () {
|
||||
$('#fileupload').fileupload({
|
||||
dataType: 'json',
|
||||
done: function (e, data) {
|
||||
$.each(data.result.files, function (index, file) {
|
||||
$('<p/>').text(file.name).appendTo('#lastUploadLog');
|
||||
});
|
||||
},
|
||||
sequentialUploads: true,
|
||||
progressall: function (e, data) {
|
||||
var progress = parseInt(data.loaded / data.total * 100, 10);
|
||||
$('#progress .bar').css(
|
||||
'width',
|
||||
progress + '%'
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
</div>
|
||||
<div id="progress">
|
||||
<div class="bar" style="width: 0%;"></div>
|
||||
</div>
|
||||
<div id="lastUploadLog"></div>
|
||||
</div>
|
||||
|
||||
<!-- display images from server -->
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<% foreach my $image (@$images) { %>
|
||||
<div class="foto-block col-md-3">
|
||||
<div class="image">
|
||||
<img src="<%= "/$image_base/$user_id/$thumbs_size/$image" %>">
|
||||
</div>
|
||||
<div class="foto-notes">
|
||||
<a href='<%= "/$image_base/$user_id/$orig/$image" %>'>Image original</a>
|
||||
<% for my $scale (@$scales) { %>
|
||||
<a href='<%= "/$image_base/$user_id/$scale/$image" %>'><%= $scale %></a>
|
||||
<% } %>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<% } %>
|
||||
</div>
|
||||
</div>
|
18
templates/index.html.ep
Normal file
18
templates/index.html.ep
Normal file
|
@ -0,0 +1,18 @@
|
|||
% layout 'base';
|
||||
<h1>Rough, Slow, Stupid, Contrary Photohosting</h1>
|
||||
|
||||
<% if (is_user_authenticated()) { %>
|
||||
%= include 'images_list'
|
||||
<% } else { %>
|
||||
|
||||
<div class="login-form">
|
||||
<form method="post" action="<%= url_for('login') %>" >
|
||||
<input type="text" name="username" >
|
||||
<input type="password" name="password">
|
||||
<input type="submit" value="Login">
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<% } %>
|
||||
|
||||
|
29
templates/layouts/base.html.ep
Normal file
29
templates/layouts/base.html.ep
Normal file
|
@ -0,0 +1,29 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" >
|
||||
<title>Rough, Slow, Stupid, Contrary Photohosting</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<!-- Bootstrap styles -->
|
||||
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css">
|
||||
<!-- Generic page styles -->
|
||||
<link rel="stylesheet" href="/file_uploader/css/style.css">
|
||||
<!-- blueimp Gallery styles -->
|
||||
<link rel="stylesheet" href="//blueimp.github.io/Gallery/css/blueimp-gallery.min.css">
|
||||
<!-- CSS to style the file input field as button and adjust the Bootstrap progress bars -->
|
||||
<link rel="stylesheet" href="/file_uploader/css/jquery.fileupload.css">
|
||||
<link rel="stylesheet" href="/file_uploader/css/jquery.fileupload-ui.css">
|
||||
<link rel="stylesheet" href="/css/main.css">
|
||||
<!-- CSS adjustments for browsers with JavaScript disabled -->
|
||||
<noscript><link rel="stylesheet" href="/file_uploader/css/jquery.fileupload-noscript.css"></noscript>
|
||||
<noscript><link rel="stylesheet" href="/file_uploader/css/jquery.fileupload-ui-noscript.css"></noscript>
|
||||
<style>
|
||||
.bar {
|
||||
height: 18px;
|
||||
background: green;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<%= content %>
|
||||
</body>
|
||||
</html>
|
17
templates/no_logged.html.ep
Normal file
17
templates/no_logged.html.ep
Normal file
|
@ -0,0 +1,17 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" >
|
||||
<title>Rough, Slow, Stupid, Contrary Photohosting</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Rough, Slow, Stupid, Contrary Photohosting</h1>
|
||||
<form method="post" action="<%= url_for('login') %>" >
|
||||
<div>
|
||||
<input type="text" name="username" >
|
||||
<input type="password" name="password">
|
||||
<input type="submit" value="Login">
|
||||
</div>
|
||||
</form>
|
||||
</body>
|
||||
</html>
|
||||
|
25
templates/register.html.ep
Normal file
25
templates/register.html.ep
Normal file
|
@ -0,0 +1,25 @@
|
|||
% layout 'base';
|
||||
|
||||
<div class="container">
|
||||
<div class="register">
|
||||
<form method="post" action="<%= url_for('register') %>" >
|
||||
<div>
|
||||
<p>
|
||||
Login:
|
||||
<input type="text" name="username" >
|
||||
</p><p>
|
||||
Fullname:
|
||||
<input type="text" name="Fullname" >
|
||||
</p><p>
|
||||
Password:
|
||||
<input type="password" name="password">
|
||||
</p><p>
|
||||
Invite code:
|
||||
<input type="text" name="invite" >
|
||||
</p><p>
|
||||
<input type="submit" value="Register">
|
||||
</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
Loading…
Reference in a new issue