This is a complete SaaS example (Software-as-a-Service) using PostgreSQL as a database and Gliimly as a web service engine; it includes user signup/login/logout with an email and password, separate user accounts and data, and a notes application. All in about 200 lines of code!
First, create a directory for your application where the source code will be:
mkdir -p notes
cd notes
Create a PostgreSQL user (with the same name as your logged-on Linux user, so no password needed), and the database "db_app":
echo "create user $(whoami);
create database db_app with owner=$(whoami);
grant all on database db_app to $(whoami);
\q" | sudo -u postgres psql
Create a database configuration file to describe your PostgreSQL database above:
echo "user=$(whoami) dbname=db_app" > db_app
Create database objects we'll need - a users table for application users, and a notes table to hold their notes:
echo "create table if not exists notes (dateOf timestamp, noteId bigserial primary key, userId bigint, note varchar(1000));
create table if not exists users (userId bigserial primary key, email varchar(100), hashed_pwd varchar(100), verified smallint, verify_token varchar(30), session varchar(100));
create unique index if not exists users1 on users (email);" | psql -d db_app
Create the application "notes" owned by your Linux user:
sudo mgrg -i -u $(whoami) notes
This executes before any other handler in an application, making sure all requests are authorized, file "before-handler.gliim":
vi before-handler.gliim
Copy and paste:
before-handler
set-param displayed_logout = false, is_logged_in = false
call-handler "/session/check"
end-before-handler
Signup users, login, logout
This is a generic session management web service that handles user creation, verification, login, and logout. Create file "session.gliim":
vi session.gliim
Copy and paste:
// Display link to login or signup
%% /session/login-or-signup private
@<a href="<<p-path "/session/user/login">>">Login</a> <a href="<<p-path "/session/user/new/form">>">Sign Up</a><hr/>
%%
// Login with email and password, and create a new session, then display home pag
%% /session/login public
get-param pwd, email
hash-string pwd to hashed_pwd
random-string to sess_id length 30
run-query @db_app = "select userId from users where email='%s' and hashed_pwd='%s'" output sess_user_id : email, hashed_pwd
run-query @db_app no-loop = "update users set session='%s' where userId='%s'" input sess_id, sess_user_id affected-rows arows
if-true arows not-equal 1
@Could not create a session. Please try again. <<call-handler "/session/login-or-signup">> <hr/>
exit-handler
end-if
set-cookie "sess_user_id" = sess_user_id path "/", "sess_id" = sess_id path "/"
call-handler "/session/check"
call-handler "/session/show-home"
exit-handler
end-query
@Email or password are not correct. <<call-handler "/session/login-or-signup">><hr/>
%%
// Starting point of the application. Either display login form or a home page:
%% /session/start public
get-param action, is_logged_in type bool
if-true is_logged_in equal true
if-true action not-equal "logout"
call-handler "/session/show-home"
exit-handler
end-if
end-if
call-handler "/session/user/login"
%%
// Generic home page, you can call anything from here, in this case a list of note
%% /session/show-home private
call-handler "/notes/list"
%%
// Logout user and display home, which will ask to either login or signup
%% /session/logout public
get-param is_logged_in type bool
if-true is_logged_in equal true
get-param sess_user_id
run-query @db_app = "update users set session='' where userId='%s'" input sess_user_id no-loop affected-rows arows
if-true arows equal 1
set-param is_logged_in = false
@You have been logged out.<hr/>
commit-transaction @db_app
end-if
end-if
call-handler "/session/show-home"
%%
// Check session based on session cookie. If session cookie corresponds to the email address, the request is a part of an authorized session
%% /session/check private
get-cookie sess_user_id="sess_user_id", sess_id="sess_id"
set-param sess_id, sess_user_id
if-true sess_id not-equal ""
set-param is_logged_in = false
run-query @db_app = "select email from users where userId='%s' and session='%s'" output email input sess_user_id, sess_id row-count rcount
set-param is_logged_in = true
get-param displayed_logout type bool
if-true displayed_logout equal false
get-param action
if-true action not-equal "logout"
@Hi <<p-out email>>! <a href="<<p-path "/session/logout">>">Logout</a><br/>
end-if
set-param displayed_logout = true
end-if
end-query
if-true rcount not-equal 1
set-param is_logged_in = false
end-if
end-if
%%
// Check that email verification token is the one actually sent to the email address
%% /session/verify-signup public
get-param code, email
run-query @db_app = "select verify_token from users where email='%s'" output db_verify : email
if-true code equal db_verify
@Your email has been verifed. Please <a href="<<p-path "/session/user/login">>">Login</a>.
run-query @db_app no-loop = "update users set verified=1 where email='%s'" : email
exit-handler
end-if
end-query
@Could not verify the code. Please try <a href="<<p-path "/session/user/new/verify-form">>">again</a>.
exit-handler
%%
// Display login form that asks for email and password
%% /session/user/login public
call-handler "/session/login-or-signup"
@Please Login:<hr/>
@<form action="<<p-path "/session/login">>" method="POST">
@<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email">
@<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password">
@<button type="submit">Go</button>
@</form>
%%
// Display form for a new user, asking for an email and password
%% /session/user/new/form public
@Create New User<hr/>
@<form action="<<p-path "/session/user/new/create">>" method="POST">
@<input name="email" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Email">
@<input name="pwd" type="password" value="" size="50" maxlength="50" required placeholder="Password">
@<input type="submit" value="Sign Up">
@</form>
%%
// Send verification email
%% /session/user/new/send-verify private
get-param email, verify
write-string msg
@From: [email protected]
@To: <<p-out email>>
@Subject: verify your account
@
@Your verification code is: <<p-out verify>>
end-write-string
exec-program "/usr/sbin/sendmail" args "-i", "-t" input msg status st
if-true st not-equal 0 or true equal false
@Could not send email to <<p-out email>>, code is <<p-out verify>>
set-param verify_sent = false
else-if
set-param verify_sent = true
end-if
%%
// Create new user from email and password
%% /session/user/new/create public
get-param email, pwd
hash-string pwd to hashed_pwd
random-string to verify length 5 number
begin-transaction @db_app
run-query @db_app no-loop = "insert into users (email, hashed_pwd, verified, verify_token, session) values ('%s', '%s', '0', '%s', '')" input email, hashed_pwd, verify affected-rows arows error err on-error-continue
if-true err not-equal "0" or arows not-equal 1
call-handler "/session/login-or-signup"
@User with this email already exists.
rollback-transaction @db_app
else-if
set-param email, verify
call-handler "/session/user/new/send-verify"
get-param verify_sent type bool
if-true verify_sent equal false
rollback-transaction @db_app
exit-handler
end-if
commit-transaction @db_app
call-handler "/session/user/new/verify-form"
end-if
%%
// Display form to enter the code emailed to user to verify the email address
%% /session/user/new/verify-form public
get-param email
@Please check your email and enter verification code here:
@<form action="<<p-path "/session/verify-signup">>" method="POST">
@<input name="email" type="hidden" value="<<p-out email>>">
@<input name="code" type="text" value="" size="50" maxlength="50" required autofocus placeholder="Verification code">
@<button type="submit">Verify</button>
@</form>
%%
Notes application
This is the actual application that uses the above session management services. Create file "notes.gliim":
vi notes.gliim
Copy and paste:
// Delete a note
%% /notes/delete public
call-handler "/notes/header"
get-param sess_user_id, note_id
run-query @db_app = "delete from notes where noteId='%s' and userId='%s'" : note_id, sess_user_id \
affected-rows arows no-loop error errnote
if-true arows equal 1
@Note deleted
else-if
@Could not delete note (<<p-out errnote>>)
end-if
%%
// Display a form to add a note
%% /notes/form-add public
call-handler "/notes/header"
@Add New Note
@<form action="<<p-path "/notes/add">>" method="POST">
@<textarea name="note" rows="5" cols="50" required autofocus placeholder="Enter Note"></textarea>
@<button type="submit">Create</button>
@</form>
%%
// Add a note
%% /notes/add public
call-handler "/notes/header"
get-param note, sess_user_id
run-query @db_app = "insert into notes (dateOf, userId, note) values (now(), '%s', '%s')" : sess_user_id, note \
affected-rows arows no-loop error errnote
if-true arows equal 1
@Note added
else-if
@Could not add note (<<p-out errnote>>)
end-if
%%
// List all notes
%% /notes/list public
call-handler "/notes/header"
get-param sess_user_id
run-query @db_app = "select dateOf, note, noteId from notes where userId='%s' order by dateOf desc" \
input sess_user_id output dateOf, note, noteId
match-regex "\n" in note replace-with "<br/>\n" result with_breaks status st cache
if-true st equal 0
set-string with_breaks = note
end-if
@Date: <<p-out dateOf>> (<a href="<<p-path "/notes/ask-delete">>?note_id=<<p-out noteId>>">delete note</a>)<br/>
@Note: <<p-out with_breaks>><br/>
@<hr/>
end-query
%%
// Display a question whether to delete a note or not
%% /notes/ask-delete public
call-handler "/notes/header"
get-param note_id
@Are you sure you want to delete a note? Use Back button to go back,\
or <a href="<<p-path "/notes/delete">>?note_id=<<p-out note_id>>">delete note now</a>.
%%
// Check if session is authorized, and display an appropriate header
%% /notes/header private
get-param is_logged_in type bool
if-true is_logged_in equal false
call-handler "/session/login-or-signup"
end-if
@<h1>Welcome to Notes!</h1><hr/>
if-true is_logged_in equal false
exit-handler
end-if
@<a href="<<p-path "/notes/form-add">>">Add Note</a> <a href="<<p-path "/notes/list">>">List Notes</a><hr/>
%%
gg -q --db=postgres:db_app
mgrg notes
In order to use this example, you need to be able to email local users, which means email addresses such as \"myuser@localhost\". To do that, install Postfix (or sendmail). On Debian systems (like Ubuntu):
sudo apt install postfix
sudo systemctl start postfix
and on Fedora systems (like RedHat):
sudo dnf install postfix
sudo systemctl start postfix
When the application sends an email to a local user, such as <OS user>@localhost, then you can see the email sent at:
sudo vi /var/mail/<OS user>
A web server sits in front of the Gliimly application server, so it needs to be set up. This example is for Ubuntu, so edit the Nginx config file there:
sudo vi /etc/nginx/sites-enabled/default
Add this in the "server {}" section:
location /notes/ { include /etc/nginx/fastcgi_params; fastcgi_pass unix:///var/lib/gg/notes/sock/sock; }
Restart Nginx:
sudo systemctl restart nginx
Go to your web browser, and enter:
http://127.0.0.1/notes/session/start