Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
package pkg
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2023-02-01 10:02:32 +00:00
|
|
|
"errors"
|
2023-02-03 11:30:56 +00:00
|
|
|
"github.com/julienschmidt/httprouter"
|
2023-02-01 13:15:02 +00:00
|
|
|
"html/template"
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
"net"
|
|
|
|
"net/http"
|
2023-06-13 12:21:54 +00:00
|
|
|
"strings"
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
"time"
|
2023-01-23 00:18:47 +00:00
|
|
|
|
|
|
|
"golang.org/x/text/language"
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
Add user_profile view to update the profile with form
Since users do not have access to the auth scheme, i had to add a view
that selects only the data that they can see of themselves (i.e., no
password or cookie).
I wanted to use the `request.user.id` setting that i set in
check_cookie, but this would be bad because anyone can change that
parameter and, since the view is created by the owner, could see and
*change* the values of everyone just by knowing their id. Thus, now i
use the cookie instead, because it is way harder to figure out, and if
you already have it you can just set to your browser and the user is
fucked anyway; the database can not help here.
I **am** going to use the user id in row level security policies, but
not the value coming for the setting but instaed the one in the
`user_profile`, since it already is “derived” from the cookie, that’s
why i added that column to the view.
The profile includes the language, that i do not use it yet to switch
the locale, so i had to add a relation of the available languages, for
constraint purposes. There is no NULL language, and instead i added the
“Undefined” language, with ‘und’ tag’, to represent “do not know/use
content negotiation”.
The languages in that relation are the same i used to have inside
locale.go, because there is no point on having options for languages i
do not have the translation for, so i now configure the list of
available languages user in content negotiation from that relation.
Finally, i have added all font from RemixIcon because that’s what we
used in the design and i am going to use quite a lot of them.
There is duplication in the views; i will address that in a different
commit.
2023-01-22 01:23:09 +00:00
|
|
|
ContextUserKey = "numerus-user"
|
Add a function to set request settings and the role
I did not like the idea that it was the Go server who should set values
such as request.user or set the role, because this is mostly something
that only the database wants for itself, such as when calling logout. I
am also planning to use these setings for row security with the user’s
id, that the Go application has no need for, but with the current
approach i would need to return it from check_cookie so that it can
return it back to the database when acquiring the connection.
I would have used the same function to set the settings and the role,
but security definer functions—obviously in retrospect—can not set the
role, because then could switch to any role of the user that defined the
function, not the roles they are member of. Thus, a new function.
I did not want to do that every time i needed the database connection
within the same request, because it would perform the same operations
each time—it is the same cookie, afterall—, so new connections are
request scoped and passed along in the context.
2023-01-19 12:07:32 +00:00
|
|
|
ContextCookieKey = "numerus-cookie"
|
Add user_profile view to update the profile with form
Since users do not have access to the auth scheme, i had to add a view
that selects only the data that they can see of themselves (i.e., no
password or cookie).
I wanted to use the `request.user.id` setting that i set in
check_cookie, but this would be bad because anyone can change that
parameter and, since the view is created by the owner, could see and
*change* the values of everyone just by knowing their id. Thus, now i
use the cookie instead, because it is way harder to figure out, and if
you already have it you can just set to your browser and the user is
fucked anyway; the database can not help here.
I **am** going to use the user id in row level security policies, but
not the value coming for the setting but instaed the one in the
`user_profile`, since it already is “derived” from the cookie, that’s
why i added that column to the view.
The profile includes the language, that i do not use it yet to switch
the locale, so i had to add a relation of the available languages, for
constraint purposes. There is no NULL language, and instead i added the
“Undefined” language, with ‘und’ tag’, to represent “do not know/use
content negotiation”.
The languages in that relation are the same i used to have inside
locale.go, because there is no point on having options for languages i
do not have the translation for, so i now configure the list of
available languages user in content negotiation from that relation.
Finally, i have added all font from RemixIcon because that’s what we
used in the design and i am going to use quite a lot of them.
There is duplication in the views; i will address that in a different
commit.
2023-01-22 01:23:09 +00:00
|
|
|
ContextConnKey = "numerus-database"
|
|
|
|
sessionCookie = "numerus-session"
|
|
|
|
defaultRole = "guest"
|
2023-02-02 10:39:34 +00:00
|
|
|
csrfTokenField = "csfrToken"
|
2024-08-11 01:22:37 +00:00
|
|
|
csrfTokenHeader = "X-CSRFToken"
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
)
|
|
|
|
|
2023-02-01 10:02:32 +00:00
|
|
|
type loginForm struct {
|
|
|
|
locale *Locale
|
|
|
|
Errors []error
|
|
|
|
Email *InputField
|
|
|
|
Password *InputField
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
}
|
|
|
|
|
Prefill login form when using the demo database
This is to help up “sell” the service: people can look around the demo
to see whether it fits them. Of course, everyone should have the same
username in the demo.
We talked about having the username and password displayed above the
form in the template, but i think it makes more sense to give users as
little work as necessary. Plus, that means i do not have to write them
down while developing.
Whether the database is demo or not is not something that directly
depends on the environment, but rather on which database we are
connected to, thus an environment variable would not make much sense—it
has to be something of the database.
PostgreSQL has no PRAGMA application_id or PRAGMA user_version as with
SQLite to include application-specific values to the database. The
equivalent would be customized options[0], intended for modules
configuration, but that would require me to execute an ALTER DATABASE
in demo.sql with an specific datbase name, or force the use of psql to
run script the script, because then i can use the :DBNAME placeholder.
I guess that the most “standard” way is to just create a function that
returns a know value if the database is demo. Sqitch does not add that
function, therefore it is unlikely to be there by change unless it is
the demo database.
https://www.postgresql.org/docs/15/runtime-config-custom.html
2024-01-20 19:23:18 +00:00
|
|
|
func newLoginForm(demo bool, locale *Locale) *loginForm {
|
|
|
|
form := &loginForm{
|
2023-02-01 10:02:32 +00:00
|
|
|
locale: locale,
|
|
|
|
Email: &InputField{
|
|
|
|
Name: "email",
|
|
|
|
Label: pgettext("input", "Email", locale),
|
|
|
|
Type: "email",
|
|
|
|
Required: true,
|
2023-02-01 13:15:02 +00:00
|
|
|
Attributes: []template.HTMLAttr{
|
|
|
|
`autofocus="autofocus"`,
|
|
|
|
`autocomplete="username"`,
|
|
|
|
`autocapitalize="none"`,
|
2023-02-01 10:02:32 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
Password: &InputField{
|
|
|
|
Name: "password",
|
|
|
|
Label: pgettext("input", "Password", locale),
|
|
|
|
Type: "password",
|
|
|
|
Required: true,
|
2023-02-01 13:15:02 +00:00
|
|
|
Attributes: []template.HTMLAttr{
|
|
|
|
`autocomplete="current-password"`,
|
2023-02-01 10:02:32 +00:00
|
|
|
},
|
|
|
|
},
|
|
|
|
}
|
Prefill login form when using the demo database
This is to help up “sell” the service: people can look around the demo
to see whether it fits them. Of course, everyone should have the same
username in the demo.
We talked about having the username and password displayed above the
form in the template, but i think it makes more sense to give users as
little work as necessary. Plus, that means i do not have to write them
down while developing.
Whether the database is demo or not is not something that directly
depends on the environment, but rather on which database we are
connected to, thus an environment variable would not make much sense—it
has to be something of the database.
PostgreSQL has no PRAGMA application_id or PRAGMA user_version as with
SQLite to include application-specific values to the database. The
equivalent would be customized options[0], intended for modules
configuration, but that would require me to execute an ALTER DATABASE
in demo.sql with an specific datbase name, or force the use of psql to
run script the script, because then i can use the :DBNAME placeholder.
I guess that the most “standard” way is to just create a function that
returns a know value if the database is demo. Sqitch does not add that
function, therefore it is unlikely to be there by change unless it is
the demo database.
https://www.postgresql.org/docs/15/runtime-config-custom.html
2024-01-20 19:23:18 +00:00
|
|
|
if demo {
|
|
|
|
form.Email.Val = "admin@numerus"
|
|
|
|
form.Password.Val = "admin"
|
|
|
|
}
|
|
|
|
return form
|
2023-02-01 10:02:32 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func (form *loginForm) Parse(r *http.Request) error {
|
|
|
|
err := r.ParseForm()
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
form.Email.FillValue(r)
|
|
|
|
form.Password.FillValue(r)
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (form *loginForm) Validate() bool {
|
2023-02-01 10:30:30 +00:00
|
|
|
validator := newFormValidator()
|
|
|
|
if validator.CheckRequiredInput(form.Email, gettext("Email can not be empty.", form.locale)) {
|
|
|
|
validator.CheckValidEmailInput(form.Email, gettext("This value is not a valid email. It should be like name@domain.com.", form.locale))
|
2023-02-01 10:02:32 +00:00
|
|
|
}
|
2023-02-01 10:30:30 +00:00
|
|
|
validator.CheckRequiredInput(form.Password, gettext("Password can not be empty.", form.locale))
|
|
|
|
return validator.AllOK()
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
}
|
|
|
|
|
Prefill login form when using the demo database
This is to help up “sell” the service: people can look around the demo
to see whether it fits them. Of course, everyone should have the same
username in the demo.
We talked about having the username and password displayed above the
form in the template, but i think it makes more sense to give users as
little work as necessary. Plus, that means i do not have to write them
down while developing.
Whether the database is demo or not is not something that directly
depends on the environment, but rather on which database we are
connected to, thus an environment variable would not make much sense—it
has to be something of the database.
PostgreSQL has no PRAGMA application_id or PRAGMA user_version as with
SQLite to include application-specific values to the database. The
equivalent would be customized options[0], intended for modules
configuration, but that would require me to execute an ALTER DATABASE
in demo.sql with an specific datbase name, or force the use of psql to
run script the script, because then i can use the :DBNAME placeholder.
I guess that the most “standard” way is to just create a function that
returns a know value if the database is demo. Sqitch does not add that
function, therefore it is unlikely to be there by change unless it is
the demo database.
https://www.postgresql.org/docs/15/runtime-config-custom.html
2024-01-20 19:23:18 +00:00
|
|
|
func GetLoginForm(w http.ResponseWriter, r *http.Request, demo bool) {
|
2023-02-03 11:30:56 +00:00
|
|
|
user := getUser(r)
|
|
|
|
if user.LoggedIn {
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
locale := getLocale(r)
|
Prefill login form when using the demo database
This is to help up “sell” the service: people can look around the demo
to see whether it fits them. Of course, everyone should have the same
username in the demo.
We talked about having the username and password displayed above the
form in the template, but i think it makes more sense to give users as
little work as necessary. Plus, that means i do not have to write them
down while developing.
Whether the database is demo or not is not something that directly
depends on the environment, but rather on which database we are
connected to, thus an environment variable would not make much sense—it
has to be something of the database.
PostgreSQL has no PRAGMA application_id or PRAGMA user_version as with
SQLite to include application-specific values to the database. The
equivalent would be customized options[0], intended for modules
configuration, but that would require me to execute an ALTER DATABASE
in demo.sql with an specific datbase name, or force the use of psql to
run script the script, because then i can use the :DBNAME placeholder.
I guess that the most “standard” way is to just create a function that
returns a know value if the database is demo. Sqitch does not add that
function, therefore it is unlikely to be there by change unless it is
the demo database.
https://www.postgresql.org/docs/15/runtime-config-custom.html
2024-01-20 19:23:18 +00:00
|
|
|
form := newLoginForm(demo, locale)
|
2023-02-03 11:30:56 +00:00
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
mustRenderLoginForm(w, r, form)
|
|
|
|
}
|
|
|
|
|
Prefill login form when using the demo database
This is to help up “sell” the service: people can look around the demo
to see whether it fits them. Of course, everyone should have the same
username in the demo.
We talked about having the username and password displayed above the
form in the template, but i think it makes more sense to give users as
little work as necessary. Plus, that means i do not have to write them
down while developing.
Whether the database is demo or not is not something that directly
depends on the environment, but rather on which database we are
connected to, thus an environment variable would not make much sense—it
has to be something of the database.
PostgreSQL has no PRAGMA application_id or PRAGMA user_version as with
SQLite to include application-specific values to the database. The
equivalent would be customized options[0], intended for modules
configuration, but that would require me to execute an ALTER DATABASE
in demo.sql with an specific datbase name, or force the use of psql to
run script the script, because then i can use the :DBNAME placeholder.
I guess that the most “standard” way is to just create a function that
returns a know value if the database is demo. Sqitch does not add that
function, therefore it is unlikely to be there by change unless it is
the demo database.
https://www.postgresql.org/docs/15/runtime-config-custom.html
2024-01-20 19:23:18 +00:00
|
|
|
func HandleLoginForm(w http.ResponseWriter, r *http.Request, demo bool) {
|
2023-02-03 11:30:56 +00:00
|
|
|
user := getUser(r)
|
|
|
|
if user.LoggedIn {
|
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
locale := getLocale(r)
|
Prefill login form when using the demo database
This is to help up “sell” the service: people can look around the demo
to see whether it fits them. Of course, everyone should have the same
username in the demo.
We talked about having the username and password displayed above the
form in the template, but i think it makes more sense to give users as
little work as necessary. Plus, that means i do not have to write them
down while developing.
Whether the database is demo or not is not something that directly
depends on the environment, but rather on which database we are
connected to, thus an environment variable would not make much sense—it
has to be something of the database.
PostgreSQL has no PRAGMA application_id or PRAGMA user_version as with
SQLite to include application-specific values to the database. The
equivalent would be customized options[0], intended for modules
configuration, but that would require me to execute an ALTER DATABASE
in demo.sql with an specific datbase name, or force the use of psql to
run script the script, because then i can use the :DBNAME placeholder.
I guess that the most “standard” way is to just create a function that
returns a know value if the database is demo. Sqitch does not add that
function, therefore it is unlikely to be there by change unless it is
the demo database.
https://www.postgresql.org/docs/15/runtime-config-custom.html
2024-01-20 19:23:18 +00:00
|
|
|
form := newLoginForm(demo, locale)
|
2023-02-03 11:30:56 +00:00
|
|
|
if err := form.Parse(r); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if form.Validate() {
|
|
|
|
conn := getConn(r)
|
|
|
|
cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", form.Email, form.Password, remoteAddr(r))
|
|
|
|
if cookie != "" {
|
|
|
|
setSessionCookie(w, cookie)
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
Add Catalan and Spanish translation with gotext[3]
I had to choose between [1], [2], and [3].
As far as i could find, [1] is not easy to work with templates[4] and at
the moment is not maintained[5].
Both [2] and [3] use the same approach to be used from within templates:
you have to define a FuncMap with template functions that call the
message catalog. Also, both libraries seems to be reasonably
maintained, and have packages in Debian’s repository.
However, [2] repeats the same mistakes that POSIX did with its
catalogs—using identifiers that are not the strings in the source
language—, however this time the catalogs are written in JSON or YAML!
This, somehow, makes things worse….
[3], the one i settled with, is fine and decently maintained. There are
some surprising things, such as to be able to use directly the PO file,
and that it has higher priority over the corresponding MO, or that the
order of parameters is reversed in respect to gettext. However, it uses
a saner format, and is a lot easier to work with than [3].
The problem, of course, is that xgettext does not know how to find
translatable strings inside the template. [3] includes a CLI tool
similar to xgettext, but is not a drop-in replacement[6] and does not
process templates.
The proper way to handle this would be to add a parser to xgettext, but
for now i found out that if i surround the call to the translation
functions from within the template with parentheses, i can trick
xgettext into believing it is parsing Scheme code, and extracts the
strings successfully—at least, for what i have tried. Had to add the
keyword for pgettext, because Schemed does not have it, but at least i
can do that with command line parameters.
For now i left only Spanish and Catalan as the two available languages,
even though the source text is written in English, because that way i
can make sure i do not leave strings untranslated.
[1]: https://golang.org/x/text
[2]: https://github.com/nicksnyder/go-i18n
[3]: https://github.com/leonelquinteros/gotext
[4]: https://github.com/golang/go/issues/39954
[5]: https://github.com/golang/go/issues/12750
[6]: https://github.com/leonelquinteros/gotext/issues/38
2023-01-18 18:07:42 +00:00
|
|
|
return
|
|
|
|
}
|
2023-02-03 11:30:56 +00:00
|
|
|
form.Errors = append(form.Errors, errors.New(gettext("Invalid user or password.", locale)))
|
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
|
|
} else {
|
|
|
|
w.WriteHeader(http.StatusUnprocessableEntity)
|
|
|
|
}
|
|
|
|
mustRenderLoginForm(w, r, form)
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
}
|
|
|
|
|
2023-02-03 11:30:56 +00:00
|
|
|
func mustRenderLoginForm(w http.ResponseWriter, r *http.Request, form *loginForm) {
|
|
|
|
mustRenderWebTemplate(w, r, "login.gohtml", form)
|
|
|
|
}
|
|
|
|
|
|
|
|
func HandleLogout(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
|
|
|
|
if err := verifyCsrfTokenValid(r); err != nil {
|
|
|
|
http.Error(w, err.Error(), http.StatusForbidden)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
conn := getConn(r)
|
|
|
|
conn.MustExec(r.Context(), "select logout()")
|
|
|
|
http.SetCookie(w, createSessionCookie("", -24*time.Hour))
|
|
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func remoteAddr(r *http.Request) string {
|
2023-06-13 12:21:54 +00:00
|
|
|
address, _, _ := net.SplitHostPort(r.RemoteAddr)
|
|
|
|
if address != "localhost" && address != "127.0.0.1" && address != "::1" {
|
|
|
|
return address
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
}
|
2023-06-13 12:21:54 +00:00
|
|
|
forwarded := r.Header.Get("X-Forwarded-For")
|
|
|
|
if forwarded == "" {
|
|
|
|
return address
|
|
|
|
}
|
|
|
|
ips := strings.Split(forwarded, ", ")
|
|
|
|
forwarded = ips[0]
|
|
|
|
if forwarded == "" {
|
|
|
|
return address
|
|
|
|
}
|
|
|
|
return forwarded
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
}
|
|
|
|
|
2023-01-23 20:18:55 +00:00
|
|
|
func setSessionCookie(w http.ResponseWriter, cookie string) {
|
|
|
|
http.SetCookie(w, createSessionCookie(cookie, 8766*24*time.Hour))
|
|
|
|
}
|
|
|
|
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
func createSessionCookie(value string, duration time.Duration) *http.Cookie {
|
|
|
|
return &http.Cookie{
|
|
|
|
Name: sessionCookie,
|
|
|
|
Value: value,
|
|
|
|
Path: "/",
|
|
|
|
Expires: time.Now().Add(duration),
|
|
|
|
HttpOnly: true,
|
|
|
|
SameSite: http.SameSiteLaxMode,
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-02-01 10:02:32 +00:00
|
|
|
type AppUser struct {
|
2023-02-02 10:39:34 +00:00
|
|
|
Email string
|
|
|
|
LoggedIn bool
|
|
|
|
Role string
|
|
|
|
Language language.Tag
|
|
|
|
CsrfToken string
|
2023-02-01 10:02:32 +00:00
|
|
|
}
|
|
|
|
|
2023-02-03 11:30:56 +00:00
|
|
|
func LoginChecker(db *Db, next http.Handler) http.Handler {
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
Add user_profile view to update the profile with form
Since users do not have access to the auth scheme, i had to add a view
that selects only the data that they can see of themselves (i.e., no
password or cookie).
I wanted to use the `request.user.id` setting that i set in
check_cookie, but this would be bad because anyone can change that
parameter and, since the view is created by the owner, could see and
*change* the values of everyone just by knowing their id. Thus, now i
use the cookie instead, because it is way harder to figure out, and if
you already have it you can just set to your browser and the user is
fucked anyway; the database can not help here.
I **am** going to use the user id in row level security policies, but
not the value coming for the setting but instaed the one in the
`user_profile`, since it already is “derived” from the cookie, that’s
why i added that column to the view.
The profile includes the language, that i do not use it yet to switch
the locale, so i had to add a relation of the available languages, for
constraint purposes. There is no NULL language, and instead i added the
“Undefined” language, with ‘und’ tag’, to represent “do not know/use
content negotiation”.
The languages in that relation are the same i used to have inside
locale.go, because there is no point on having options for languages i
do not have the translation for, so i now configure the list of
available languages user in content negotiation from that relation.
Finally, i have added all font from RemixIcon because that’s what we
used in the design and i am going to use quite a lot of them.
There is duplication in the views; i will address that in a different
commit.
2023-01-22 01:23:09 +00:00
|
|
|
var ctx = r.Context()
|
Add a function to set request settings and the role
I did not like the idea that it was the Go server who should set values
such as request.user or set the role, because this is mostly something
that only the database wants for itself, such as when calling logout. I
am also planning to use these setings for row security with the user’s
id, that the Go application has no need for, but with the current
approach i would need to return it from check_cookie so that it can
return it back to the database when acquiring the connection.
I would have used the same function to set the settings and the role,
but security definer functions—obviously in retrospect—can not set the
role, because then could switch to any role of the user that defined the
function, not the roles they are member of. Thus, a new function.
I did not want to do that every time i needed the database connection
within the same request, because it would perform the same operations
each time—it is the same cookie, afterall—, so new connections are
request scoped and passed along in the context.
2023-01-19 12:07:32 +00:00
|
|
|
if cookie, err := r.Cookie(sessionCookie); err == nil {
|
Add user_profile view to update the profile with form
Since users do not have access to the auth scheme, i had to add a view
that selects only the data that they can see of themselves (i.e., no
password or cookie).
I wanted to use the `request.user.id` setting that i set in
check_cookie, but this would be bad because anyone can change that
parameter and, since the view is created by the owner, could see and
*change* the values of everyone just by knowing their id. Thus, now i
use the cookie instead, because it is way harder to figure out, and if
you already have it you can just set to your browser and the user is
fucked anyway; the database can not help here.
I **am** going to use the user id in row level security policies, but
not the value coming for the setting but instaed the one in the
`user_profile`, since it already is “derived” from the cookie, that’s
why i added that column to the view.
The profile includes the language, that i do not use it yet to switch
the locale, so i had to add a relation of the available languages, for
constraint purposes. There is no NULL language, and instead i added the
“Undefined” language, with ‘und’ tag’, to represent “do not know/use
content negotiation”.
The languages in that relation are the same i used to have inside
locale.go, because there is no point on having options for languages i
do not have the translation for, so i now configure the list of
available languages user in content negotiation from that relation.
Finally, i have added all font from RemixIcon because that’s what we
used in the design and i am going to use quite a lot of them.
There is duplication in the views; i will address that in a different
commit.
2023-01-22 01:23:09 +00:00
|
|
|
ctx = context.WithValue(ctx, ContextCookieKey, cookie.Value)
|
Add a function to set request settings and the role
I did not like the idea that it was the Go server who should set values
such as request.user or set the role, because this is mostly something
that only the database wants for itself, such as when calling logout. I
am also planning to use these setings for row security with the user’s
id, that the Go application has no need for, but with the current
approach i would need to return it from check_cookie so that it can
return it back to the database when acquiring the connection.
I would have used the same function to set the settings and the role,
but security definer functions—obviously in retrospect—can not set the
role, because then could switch to any role of the user that defined the
function, not the roles they are member of. Thus, a new function.
I did not want to do that every time i needed the database connection
within the same request, because it would perform the same operations
each time—it is the same cookie, afterall—, so new connections are
request scoped and passed along in the context.
2023-01-19 12:07:32 +00:00
|
|
|
}
|
|
|
|
|
2023-01-22 19:37:43 +00:00
|
|
|
conn := db.MustAcquire(ctx)
|
Add user_profile view to update the profile with form
Since users do not have access to the auth scheme, i had to add a view
that selects only the data that they can see of themselves (i.e., no
password or cookie).
I wanted to use the `request.user.id` setting that i set in
check_cookie, but this would be bad because anyone can change that
parameter and, since the view is created by the owner, could see and
*change* the values of everyone just by knowing their id. Thus, now i
use the cookie instead, because it is way harder to figure out, and if
you already have it you can just set to your browser and the user is
fucked anyway; the database can not help here.
I **am** going to use the user id in row level security policies, but
not the value coming for the setting but instaed the one in the
`user_profile`, since it already is “derived” from the cookie, that’s
why i added that column to the view.
The profile includes the language, that i do not use it yet to switch
the locale, so i had to add a relation of the available languages, for
constraint purposes. There is no NULL language, and instead i added the
“Undefined” language, with ‘und’ tag’, to represent “do not know/use
content negotiation”.
The languages in that relation are the same i used to have inside
locale.go, because there is no point on having options for languages i
do not have the translation for, so i now configure the list of
available languages user in content negotiation from that relation.
Finally, i have added all font from RemixIcon because that’s what we
used in the design and i am going to use quite a lot of them.
There is duplication in the views; i will address that in a different
commit.
2023-01-22 01:23:09 +00:00
|
|
|
defer conn.Release()
|
Add a function to set request settings and the role
I did not like the idea that it was the Go server who should set values
such as request.user or set the role, because this is mostly something
that only the database wants for itself, such as when calling logout. I
am also planning to use these setings for row security with the user’s
id, that the Go application has no need for, but with the current
approach i would need to return it from check_cookie so that it can
return it back to the database when acquiring the connection.
I would have used the same function to set the settings and the role,
but security definer functions—obviously in retrospect—can not set the
role, because then could switch to any role of the user that defined the
function, not the roles they are member of. Thus, a new function.
I did not want to do that every time i needed the database connection
within the same request, because it would perform the same operations
each time—it is the same cookie, afterall—, so new connections are
request scoped and passed along in the context.
2023-01-19 12:07:32 +00:00
|
|
|
ctx = context.WithValue(ctx, ContextConnKey, conn)
|
|
|
|
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
user := &AppUser{
|
|
|
|
Email: "",
|
|
|
|
LoggedIn: false,
|
|
|
|
Role: defaultRole,
|
|
|
|
}
|
2023-02-02 10:39:34 +00:00
|
|
|
row := conn.QueryRow(ctx, "select coalesce(email, ''), role, lang_tag, csrf_token from user_profile")
|
2023-01-23 00:18:47 +00:00
|
|
|
var langTag string
|
2023-02-02 10:39:34 +00:00
|
|
|
if err := row.Scan(&user.Email, &user.Role, &langTag, &user.CsrfToken); err != nil {
|
Add user_profile view to update the profile with form
Since users do not have access to the auth scheme, i had to add a view
that selects only the data that they can see of themselves (i.e., no
password or cookie).
I wanted to use the `request.user.id` setting that i set in
check_cookie, but this would be bad because anyone can change that
parameter and, since the view is created by the owner, could see and
*change* the values of everyone just by knowing their id. Thus, now i
use the cookie instead, because it is way harder to figure out, and if
you already have it you can just set to your browser and the user is
fucked anyway; the database can not help here.
I **am** going to use the user id in row level security policies, but
not the value coming for the setting but instaed the one in the
`user_profile`, since it already is “derived” from the cookie, that’s
why i added that column to the view.
The profile includes the language, that i do not use it yet to switch
the locale, so i had to add a relation of the available languages, for
constraint purposes. There is no NULL language, and instead i added the
“Undefined” language, with ‘und’ tag’, to represent “do not know/use
content negotiation”.
The languages in that relation are the same i used to have inside
locale.go, because there is no point on having options for languages i
do not have the translation for, so i now configure the list of
available languages user in content negotiation from that relation.
Finally, i have added all font from RemixIcon because that’s what we
used in the design and i am going to use quite a lot of them.
There is duplication in the views; i will address that in a different
commit.
2023-01-22 01:23:09 +00:00
|
|
|
panic(err)
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
}
|
Add a function to set request settings and the role
I did not like the idea that it was the Go server who should set values
such as request.user or set the role, because this is mostly something
that only the database wants for itself, such as when calling logout. I
am also planning to use these setings for row security with the user’s
id, that the Go application has no need for, but with the current
approach i would need to return it from check_cookie so that it can
return it back to the database when acquiring the connection.
I would have used the same function to set the settings and the role,
but security definer functions—obviously in retrospect—can not set the
role, because then could switch to any role of the user that defined the
function, not the roles they are member of. Thus, a new function.
I did not want to do that every time i needed the database connection
within the same request, because it would perform the same operations
each time—it is the same cookie, afterall—, so new connections are
request scoped and passed along in the context.
2023-01-19 12:07:32 +00:00
|
|
|
user.LoggedIn = user.Email != ""
|
2023-01-23 00:18:47 +00:00
|
|
|
user.Language, _ = language.Parse(langTag)
|
Add a function to set request settings and the role
I did not like the idea that it was the Go server who should set values
such as request.user or set the role, because this is mostly something
that only the database wants for itself, such as when calling logout. I
am also planning to use these setings for row security with the user’s
id, that the Go application has no need for, but with the current
approach i would need to return it from check_cookie so that it can
return it back to the database when acquiring the connection.
I would have used the same function to set the settings and the role,
but security definer functions—obviously in retrospect—can not set the
role, because then could switch to any role of the user that defined the
function, not the roles they are member of. Thus, a new function.
I did not want to do that every time i needed the database connection
within the same request, because it would perform the same operations
each time—it is the same cookie, afterall—, so new connections are
request scoped and passed along in the context.
2023-01-19 12:07:32 +00:00
|
|
|
ctx = context.WithValue(ctx, ContextUserKey, user)
|
|
|
|
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
next.ServeHTTP(w, r.WithContext(ctx))
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-02-02 10:39:34 +00:00
|
|
|
func verifyCsrfTokenValid(r *http.Request) error {
|
|
|
|
user := getUser(r)
|
2024-08-11 01:22:37 +00:00
|
|
|
token := r.Header.Get(csrfTokenHeader)
|
|
|
|
if token == "" {
|
|
|
|
token = r.FormValue(csrfTokenField)
|
|
|
|
}
|
2023-02-02 10:39:34 +00:00
|
|
|
if user.CsrfToken == token {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
locale := getLocale(r)
|
|
|
|
return errors.New(locale.Get("Cross-site request forgery detected."))
|
|
|
|
}
|
|
|
|
|
Implement login cookie, its verification, and logout
At first i thought that i would need to implement sessions, the ones
that keep small files onto the disk, to know which user is talking to
the server, but then i realized that, for now at least, i only need a
very large number, plus the email address, to be used as a lookup, and
that can be stored in the user table, in a separate schema.
Had to change login to avoid raising exceptions when login failed
because i now keep a record of login attemps, and functions are always
run in a single transaction, thus the exception would prevent me to
insert into login_attempt. Even if i use a separate procedure, i could
not keep the records.
I did not want to add a parameter to the logout function because i was
afraid that it could be called from separate users. I do not know
whether it is possible with the current approach, since the settings
variable is also set by the same applications; time will tell.
2023-01-17 19:48:50 +00:00
|
|
|
func getUser(r *http.Request) *AppUser {
|
|
|
|
return r.Context().Value(ContextUserKey).(*AppUser)
|
|
|
|
}
|
Add a function to set request settings and the role
I did not like the idea that it was the Go server who should set values
such as request.user or set the role, because this is mostly something
that only the database wants for itself, such as when calling logout. I
am also planning to use these setings for row security with the user’s
id, that the Go application has no need for, but with the current
approach i would need to return it from check_cookie so that it can
return it back to the database when acquiring the connection.
I would have used the same function to set the settings and the role,
but security definer functions—obviously in retrospect—can not set the
role, because then could switch to any role of the user that defined the
function, not the roles they are member of. Thus, a new function.
I did not want to do that every time i needed the database connection
within the same request, because it would perform the same operations
each time—it is the same cookie, afterall—, so new connections are
request scoped and passed along in the context.
2023-01-19 12:07:32 +00:00
|
|
|
|
|
|
|
func getConn(r *http.Request) *Conn {
|
|
|
|
return r.Context().Value(ContextConnKey).(*Conn)
|
|
|
|
}
|
2023-01-24 20:44:23 +00:00
|
|
|
|
2023-02-03 11:30:56 +00:00
|
|
|
func Authenticated(next httprouter.Handle) httprouter.Handle {
|
|
|
|
return func(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
|
2023-01-24 20:44:23 +00:00
|
|
|
user := getUser(r)
|
|
|
|
if user.LoggedIn {
|
2023-02-03 11:30:56 +00:00
|
|
|
next(w, r, params)
|
2023-01-24 20:44:23 +00:00
|
|
|
} else {
|
|
|
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
|
|
|
}
|
2023-02-03 11:30:56 +00:00
|
|
|
}
|
2023-01-24 20:44:23 +00:00
|
|
|
}
|