This is a complete SaaS example (Software-as-a-Service) using PostgreSQL as a database and Golf 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
Setup Postgres Database
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 A Golf Application
Create the application "notes" owned by your Linux user:
sudo mgrg -i -u $(whoami) notes
Source Code
This executes before any other handler in an application, making sure all requests are authorized, file "before-handler.golf":
vi before-handler.golf
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.golf":
vi session.golf
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: service@your-service.com
@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.golf":
vi notes.golf
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/>
%%
Build Application
gg -q --db=postgres:db_app
Run Web Services Application Server
mgrg notes
Emailing
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>
Setup Nginx
A web server sits in front of the Golf 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
You're Done; Run It!
Go to your web browser, and enter:
http://127.0.0.1/notes/session/start