Compare commits
8 Commits
8fa3367f6c
...
c84f3f9e80
Author | SHA1 | Date |
---|---|---|
jordi fita mas | c84f3f9e80 | |
jordi fita mas | b5968b1179 | |
jordi fita mas | c6eb1ef24e | |
jordi fita mas | 1675ada70b | |
jordi fita mas | 5505fa41c3 | |
jordi fita mas | fa6ddc70b3 | |
jordi fita mas | 7e5e6121ac | |
jordi fita mas | 6f2da865c0 |
9
Makefile
9
Makefile
|
@ -1,8 +1,10 @@
|
||||||
INPUT_FILES := $(shell find web -name *.html)
|
HTML_FILES := $(shell find web -name *.html)
|
||||||
|
GO_FILES := $(shell find . -name *.go)
|
||||||
DEFAULT_DOMAIN = numerus
|
DEFAULT_DOMAIN = numerus
|
||||||
POT_FILE = po/$(DEFAULT_DOMAIN).pot
|
POT_FILE = po/$(DEFAULT_DOMAIN).pot
|
||||||
LINGUAS = ca es
|
LINGUAS = ca es
|
||||||
MO_FILES = $(patsubst %,locales/%/LC_MESSAGES/$(DEFAULT_DOMAIN).mo,$(LINGUAS))
|
MO_FILES = $(patsubst %,locales/%/LC_MESSAGES/$(DEFAULT_DOMAIN).mo,$(LINGUAS))
|
||||||
|
XGETTEXTFLAGS = --no-wrap --from-code=UTF-8 --package-name=numerus --msgid-bugs-address=jordi@tandem.blog
|
||||||
|
|
||||||
locales: $(MO_FILES)
|
locales: $(MO_FILES)
|
||||||
|
|
||||||
|
@ -13,8 +15,9 @@ locales/%/LC_MESSAGES/numerus.mo: po/%.po
|
||||||
po/%.po: $(POT_FILE)
|
po/%.po: $(POT_FILE)
|
||||||
msgmerge --no-wrap --update --backup=off $@ $<
|
msgmerge --no-wrap --update --backup=off $@ $<
|
||||||
|
|
||||||
$(POT_FILE): $(INPUT_FILES)
|
$(POT_FILE): $(HTML_FILES) $(GO_FILES)
|
||||||
xgettext --no-wrap --language=Scheme --from-code=UTF-8 --output=$@ --keyword=pgettext:1,2c --package-name=numerus --msgid-bugs-address=jordi@tandem.blog $^
|
xgettext $(XGETTEXTFLAGS) --language=Scheme --output=$@ --keyword=pgettext:1,2c $(HTML_FILES)
|
||||||
|
xgettext $(XGETTEXTFLAGS) --language=C --output=$@ --join-existing $(GO_FILES)
|
||||||
|
|
||||||
test-deploy:
|
test-deploy:
|
||||||
sqitch deploy --db-name $(PGDATABASE)
|
sqitch deploy --db-name $(PGDATABASE)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
-- Deploy numerus:logout to pg
|
-- Deploy numerus:logout to pg
|
||||||
-- requires: schema_auth
|
-- requires: schema_auth
|
||||||
-- requires: user
|
-- requires: user
|
||||||
|
-- requires: current_app_user
|
||||||
|
|
||||||
begin;
|
begin;
|
||||||
|
|
||||||
|
@ -11,7 +12,9 @@ $$
|
||||||
update "user"
|
update "user"
|
||||||
set cookie = default
|
set cookie = default
|
||||||
, cookie_expires_at = default
|
, cookie_expires_at = default
|
||||||
where user_id = current_setting('request.user.id', true)::integer
|
where cookie = current_app_user()
|
||||||
|
and cookie_expires_at > current_timestamp
|
||||||
|
and length(cookie) > 30
|
||||||
$$
|
$$
|
||||||
language sql
|
language sql
|
||||||
security definer
|
security definer
|
||||||
|
|
|
@ -17,8 +17,33 @@ select user_id
|
||||||
, lang_tag
|
, lang_tag
|
||||||
from auth."user"
|
from auth."user"
|
||||||
where cookie = current_app_user()
|
where cookie = current_app_user()
|
||||||
|
and cookie_expires_at > current_timestamp
|
||||||
|
and length(cookie) > 30
|
||||||
|
union all
|
||||||
|
select 0
|
||||||
|
, null::email
|
||||||
|
, ''
|
||||||
|
, 'guest'::name
|
||||||
|
, 'und'
|
||||||
|
where not exists (
|
||||||
|
select 1
|
||||||
|
from auth."user"
|
||||||
|
where cookie = current_app_user()
|
||||||
|
and cookie_expires_at > current_timestamp
|
||||||
|
and length(cookie) > 30
|
||||||
|
);
|
||||||
|
|
||||||
|
create rule update_user_profile as on update to user_profile
|
||||||
|
do instead update auth."user"
|
||||||
|
set email = new.email
|
||||||
|
, name = new.name
|
||||||
|
, lang_tag = new.lang_tag
|
||||||
|
where cookie = current_app_user()
|
||||||
|
and cookie_expires_at > current_timestamp
|
||||||
|
and length(cookie) > 30
|
||||||
;
|
;
|
||||||
|
|
||||||
|
grant select on table user_profile to guest;
|
||||||
grant select, update(email, name, lang_tag) on table user_profile to invoicer;
|
grant select, update(email, name, lang_tag) on table user_profile to invoicer;
|
||||||
grant select, update(email, name, lang_tag) on table user_profile to admin;
|
grant select, update(email, name, lang_tag) on table user_profile to admin;
|
||||||
|
|
||||||
|
|
18
pkg/db.go
18
pkg/db.go
|
@ -29,9 +29,9 @@ func NewDatabase(ctx context.Context, connString string) (*Db, error) {
|
||||||
cookie = value
|
cookie = value
|
||||||
}
|
}
|
||||||
if _, err := conn.Exec(ctx, "select set_cookie($1)", cookie); err != nil {
|
if _, err := conn.Exec(ctx, "select set_cookie($1)", cookie); err != nil {
|
||||||
log.Printf("ERROR - Failed to set role: %v", err)
|
log.Printf("ERROR - Failed to set role: %v", err)
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -58,11 +58,19 @@ func (db *Db) Acquire(ctx context.Context) (*Conn, error) {
|
||||||
return &Conn{conn}, nil
|
return &Conn{conn}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (db *Db) MustAcquire(ctx context.Context) *Conn {
|
||||||
|
conn, err := db.Acquire(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
type Conn struct {
|
type Conn struct {
|
||||||
*pgxpool.Conn
|
*pgxpool.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Text(ctx context.Context, def string, sql string, args ...interface{}) string {
|
func (c *Conn) MustGetText(ctx context.Context, def string, sql string, args ...interface{}) string {
|
||||||
var result string
|
var result string
|
||||||
if err := c.Conn.QueryRow(ctx, sql, args...).Scan(&result); err != nil {
|
if err := c.Conn.QueryRow(ctx, sql, args...).Scan(&result); err != nil {
|
||||||
if err == pgx.ErrNoRows {
|
if err == pgx.ErrNoRows {
|
||||||
|
@ -74,7 +82,7 @@ func (c *Conn) Text(ctx context.Context, def string, sql string, args ...interfa
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) Exec(ctx context.Context, sql string, args ...interface{}) {
|
func (c *Conn) MustExec(ctx context.Context, sql string, args ...interface{}) {
|
||||||
if _, err := c.Conn.Exec(ctx, sql, args...); err != nil {
|
if _, err := c.Conn.Exec(ctx, sql, args...); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import (
|
||||||
const contextLocaleKey = "numerus-locale"
|
const contextLocaleKey = "numerus-locale"
|
||||||
|
|
||||||
func Locale(db *Db, next http.Handler) http.Handler {
|
func Locale(db *Db, next http.Handler) http.Handler {
|
||||||
availableLanguages := getAvailableLanguages(db)
|
availableLanguages := mustGetAvailableLanguages(db)
|
||||||
var matcher = language.NewMatcher(availableLanguages)
|
var matcher = language.NewMatcher(availableLanguages)
|
||||||
|
|
||||||
locales := map[language.Tag]*gotext.Locale{}
|
locales := map[language.Tag]*gotext.Locale{}
|
||||||
|
@ -24,14 +24,18 @@ func Locale(db *Db, next http.Handler) http.Handler {
|
||||||
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
var locale *gotext.Locale
|
var locale *gotext.Locale
|
||||||
t, _, err := language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))
|
user := getUser(r)
|
||||||
if err == nil {
|
locale = locales[user.Language]
|
||||||
tag, _, _ := matcher.Match(t...)
|
if locale == nil {
|
||||||
var ok bool
|
t, _, err := language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))
|
||||||
locale, ok = locales[tag]
|
if err == nil {
|
||||||
for !ok && !tag.IsRoot() {
|
tag, _, _ := matcher.Match(t...)
|
||||||
tag = tag.Parent()
|
var ok bool
|
||||||
locale, ok = locales[tag]
|
locale, ok = locales[tag]
|
||||||
|
for !ok && !tag.IsRoot() {
|
||||||
|
tag = tag.Parent()
|
||||||
|
locale, ok = locales[tag]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if locale == nil {
|
if locale == nil {
|
||||||
|
@ -46,7 +50,11 @@ func getLocale(r *http.Request) *gotext.Locale {
|
||||||
return r.Context().Value(contextLocaleKey).(*gotext.Locale)
|
return r.Context().Value(contextLocaleKey).(*gotext.Locale)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAvailableLanguages(db *Db) []language.Tag {
|
func pgettext(context string, str string, locale *gotext.Locale) string {
|
||||||
|
return locale.GetC(str, context)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustGetAvailableLanguages(db *Db) []language.Tag {
|
||||||
rows, err := db.Query(context.Background(), "select lang_tag from language where selectable")
|
rows, err := db.Query(context.Background(), "select lang_tag from language where selectable")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
20
pkg/login.go
20
pkg/login.go
|
@ -5,6 +5,8 @@ import (
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"golang.org/x/text/language"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -25,6 +27,7 @@ type AppUser struct {
|
||||||
Email string
|
Email string
|
||||||
LoggedIn bool
|
LoggedIn bool
|
||||||
Role string
|
Role string
|
||||||
|
Language language.Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoginHandler() http.Handler {
|
func LoginHandler() http.Handler {
|
||||||
|
@ -41,7 +44,7 @@ func LoginHandler() http.Handler {
|
||||||
}
|
}
|
||||||
if r.Method == "POST" {
|
if r.Method == "POST" {
|
||||||
conn := getConn(r)
|
conn := getConn(r)
|
||||||
cookie := conn.Text(r.Context(), "", "select login($1, $2, $3)", page.Email, page.Password, remoteAddr(r))
|
cookie := conn.MustGetText(r.Context(), "", "select login($1, $2, $3)", page.Email, page.Password, remoteAddr(r))
|
||||||
if cookie != "" {
|
if cookie != "" {
|
||||||
http.SetCookie(w, createSessionCookie(cookie, 8766*24*time.Hour))
|
http.SetCookie(w, createSessionCookie(cookie, 8766*24*time.Hour))
|
||||||
http.Redirect(w, r, "/", http.StatusSeeOther)
|
http.Redirect(w, r, "/", http.StatusSeeOther)
|
||||||
|
@ -52,7 +55,7 @@ func LoginHandler() http.Handler {
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
renderTemplate(w, r, "login.html", page)
|
mustRenderWebTemplate(w, r, "login.html", page)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,7 +64,7 @@ func LogoutHandler() http.Handler {
|
||||||
user := getUser(r)
|
user := getUser(r)
|
||||||
if user.LoggedIn {
|
if user.LoggedIn {
|
||||||
conn := getConn(r)
|
conn := getConn(r)
|
||||||
conn.Exec(r.Context(), "select logout()")
|
conn.MustExec(r.Context(), "select logout()")
|
||||||
http.SetCookie(w, createSessionCookie("", -24*time.Hour))
|
http.SetCookie(w, createSessionCookie("", -24*time.Hour))
|
||||||
}
|
}
|
||||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
|
@ -94,10 +97,7 @@ func CheckLogin(db *Db, next http.Handler) http.Handler {
|
||||||
ctx = context.WithValue(ctx, ContextCookieKey, cookie.Value)
|
ctx = context.WithValue(ctx, ContextCookieKey, cookie.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
conn, err := db.Acquire(ctx)
|
conn := db.MustAcquire(ctx)
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
defer conn.Release()
|
defer conn.Release()
|
||||||
ctx = context.WithValue(ctx, ContextConnKey, conn)
|
ctx = context.WithValue(ctx, ContextConnKey, conn)
|
||||||
|
|
||||||
|
@ -106,11 +106,13 @@ func CheckLogin(db *Db, next http.Handler) http.Handler {
|
||||||
LoggedIn: false,
|
LoggedIn: false,
|
||||||
Role: defaultRole,
|
Role: defaultRole,
|
||||||
}
|
}
|
||||||
row := conn.QueryRow(ctx, "select current_setting('request.user.email', true), current_user")
|
row := conn.QueryRow(ctx, "select coalesce(email, ''), role, lang_tag from user_profile")
|
||||||
if err := row.Scan(&user.Email, &user.Role); err != nil {
|
var langTag string
|
||||||
|
if err := row.Scan(&user.Email, &user.Role, &langTag); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
user.LoggedIn = user.Email != ""
|
user.LoggedIn = user.Email != ""
|
||||||
|
user.Language, _ = language.Parse(langTag)
|
||||||
ctx = context.WithValue(ctx, ContextUserKey, user)
|
ctx = context.WithValue(ctx, ContextUserKey, user)
|
||||||
|
|
||||||
next.ServeHTTP(w, r.WithContext(ctx))
|
next.ServeHTTP(w, r.WithContext(ctx))
|
||||||
|
|
|
@ -11,6 +11,7 @@ type LanguageOption struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProfilePage struct {
|
type ProfilePage struct {
|
||||||
|
Title string
|
||||||
Name string
|
Name string
|
||||||
Email string
|
Email string
|
||||||
Password string
|
Password string
|
||||||
|
@ -27,9 +28,11 @@ func ProfileHandler() http.Handler {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
conn := getConn(r)
|
conn := getConn(r)
|
||||||
|
locale := getLocale(r)
|
||||||
page := ProfilePage{
|
page := ProfilePage{
|
||||||
|
Title: pgettext("title", "User Settings", locale),
|
||||||
Email: user.Email,
|
Email: user.Email,
|
||||||
Languages: getLanguageOptions(r.Context(), conn),
|
Languages: mustGetLanguageOptions(r.Context(), conn),
|
||||||
}
|
}
|
||||||
if r.Method == "POST" {
|
if r.Method == "POST" {
|
||||||
r.ParseForm()
|
r.ParseForm()
|
||||||
|
@ -38,17 +41,19 @@ func ProfileHandler() http.Handler {
|
||||||
page.Password = r.FormValue("password")
|
page.Password = r.FormValue("password")
|
||||||
page.PasswordConfirm = r.FormValue("password_confirm")
|
page.PasswordConfirm = r.FormValue("password_confirm")
|
||||||
page.Language = r.FormValue("language")
|
page.Language = r.FormValue("language")
|
||||||
conn.Exec(r.Context(), "update user_profile set name = $1, email = $2, lang_tag = $3", page.Name, page.Email, page.Language);
|
conn.MustExec(r.Context(), "update user_profile set name = $1, email = $2, lang_tag = $3", page.Name, page.Email, page.Language)
|
||||||
|
http.Redirect(w, r, "/profile", http.StatusSeeOther);
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
if err := conn.QueryRow(r.Context(), "select name, lang_tag from user_profile").Scan(&page.Name, &page.Language); err != nil {
|
if err := conn.QueryRow(r.Context(), "select name, lang_tag from user_profile").Scan(&page.Name, &page.Language); err != nil {
|
||||||
panic(nil)
|
panic(nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
renderTemplate(w, r, "profile.html", page)
|
mustRenderAppTemplate(w, r, "profile.html", page)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLanguageOptions(ctx context.Context, conn *Conn) []LanguageOption {
|
func mustGetLanguageOptions(ctx context.Context, conn *Conn) []LanguageOption {
|
||||||
rows, err := conn.Query(ctx, "select lang_tag, endonym from language where selectable")
|
rows, err := conn.Query(ctx, "select lang_tag, endonym from language where selectable")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package pkg
|
package pkg
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"runtime"
|
"runtime"
|
||||||
"log"
|
|
||||||
"fmt"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func Recoverer(next http.Handler) http.Handler {
|
func Recoverer(next http.Handler) http.Handler {
|
||||||
|
@ -12,18 +12,18 @@ func Recoverer(next http.Handler) http.Handler {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
if r == http.ErrAbortHandler {
|
if r == http.ErrAbortHandler {
|
||||||
panic(r);
|
panic(r)
|
||||||
}
|
}
|
||||||
err, ok := r.(error)
|
err, ok := r.(error)
|
||||||
if ! ok {
|
if !ok {
|
||||||
err = fmt.Errorf("%v", r);
|
err = fmt.Errorf("%v", r)
|
||||||
}
|
}
|
||||||
stack := make([]byte, 4 << 10);
|
stack := make([]byte, 4<<10)
|
||||||
length := runtime.Stack(stack, true)
|
length := runtime.Stack(stack, true)
|
||||||
log.Printf("PANIC - %v %s", err, stack[:length])
|
log.Printf("PANIC - %v %s", err, stack[:length])
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
}();
|
}()
|
||||||
next.ServeHTTP(w, r);
|
next.ServeHTTP(w, r)
|
||||||
});
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ func NewRouter(db *Db) http.Handler {
|
||||||
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
user := getUser(r)
|
user := getUser(r)
|
||||||
if user.LoggedIn {
|
if user.LoggedIn {
|
||||||
renderTemplate(w, r, "index.html", nil)
|
mustRenderAppTemplate(w, r, "dashboard.html", nil)
|
||||||
} else {
|
} else {
|
||||||
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
http.Redirect(w, r, "/login", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,17 +6,29 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
func renderTemplate(wr io.Writer, r *http.Request, filename string, data interface{}) {
|
func templateFile (name string) string {
|
||||||
|
return "web/template/" + name
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustRenderTemplate(wr io.Writer, r *http.Request, layout string, filename string, data interface{}) {
|
||||||
locale := getLocale(r)
|
locale := getLocale(r)
|
||||||
t := template.New(filename)
|
t := template.New(filename)
|
||||||
t.Funcs(template.FuncMap{
|
t.Funcs(template.FuncMap{
|
||||||
"gettext": locale.Get,
|
"gettext": locale.Get,
|
||||||
"pgettext": locale.GetC,
|
"pgettext": locale.GetC,
|
||||||
})
|
})
|
||||||
if _, err := t.ParseFiles("web/template/" + filename); err != nil {
|
if _, err := t.ParseFiles(templateFile(filename), templateFile(layout)); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
if err := t.Execute(wr, data); err != nil {
|
if err := t.ExecuteTemplate(wr, layout, data); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func mustRenderAppTemplate(w io.Writer, r *http.Request, filename string, data interface{}) {
|
||||||
|
mustRenderTemplate(w, r, "app.html", filename, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustRenderWebTemplate(w io.Writer, r *http.Request, filename string, data interface{}) {
|
||||||
|
mustRenderTemplate(w, r, "web.html", filename, data);
|
||||||
|
}
|
||||||
|
|
48
po/ca.po
48
po/ca.po
|
@ -8,7 +8,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: numerus\n"
|
"Project-Id-Version: numerus\n"
|
||||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||||
"POT-Creation-Date: 2023-01-22 02:20+0100\n"
|
"POT-Creation-Date: 2023-01-23 00:41+0100\n"
|
||||||
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
|
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
|
||||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||||
"Language-Team: Catalan <ca@dodds.net>\n"
|
"Language-Team: Catalan <ca@dodds.net>\n"
|
||||||
|
@ -17,75 +17,75 @@ msgstr ""
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
|
||||||
#: web/template/login.html:6 web/template/login.html:19
|
#: web/template/web.html:6 web/template/login.html:9
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Entrada"
|
msgstr "Entrada"
|
||||||
|
|
||||||
#: web/template/login.html:14
|
#: web/template/login.html:5
|
||||||
msgid "Invalid user or password"
|
msgid "Invalid user or password"
|
||||||
msgstr "Nom d’usuari o contrasenya incorrectes"
|
msgstr "Nom d’usuari o contrasenya incorrectes"
|
||||||
|
|
||||||
#: web/template/login.html:21 web/template/profile.html:29
|
#: web/template/login.html:13 web/template/profile.html:14
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr "Correu electrònic"
|
msgstr "Correu-e"
|
||||||
|
|
||||||
#: web/template/login.html:24 web/template/profile.html:35
|
#: web/template/login.html:18 web/template/profile.html:22
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr "Contrasenya"
|
msgstr "Contrasenya"
|
||||||
|
|
||||||
#: web/template/login.html:27
|
#: web/template/login.html:21
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Entra"
|
msgstr "Entra"
|
||||||
|
|
||||||
#: web/template/profile.html:6 web/template/profile.html:21
|
#: web/template/profile.html:2 pkg/profile.go:33
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "User Settings"
|
msgid "User Settings"
|
||||||
msgstr "Configuració usuari"
|
msgstr "Configuració usuari"
|
||||||
|
|
||||||
#: web/template/profile.html:15 web/template/index.html:15
|
#: web/template/profile.html:5
|
||||||
msgid "Account"
|
|
||||||
msgstr "Compte"
|
|
||||||
|
|
||||||
#: web/template/profile.html:16 web/template/index.html:16
|
|
||||||
msgctxt "action"
|
|
||||||
msgid "Logout"
|
|
||||||
msgstr "Surt"
|
|
||||||
|
|
||||||
#: web/template/profile.html:24
|
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "User Access Data"
|
msgid "User Access Data"
|
||||||
msgstr "Dades accés usuari"
|
msgstr "Dades accés usuari"
|
||||||
|
|
||||||
#: web/template/profile.html:26
|
#: web/template/profile.html:9
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "User name"
|
msgid "User name"
|
||||||
msgstr "Nom d’usuari"
|
msgstr "Nom d’usuari"
|
||||||
|
|
||||||
#: web/template/profile.html:33
|
#: web/template/profile.html:18
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Password Change"
|
msgid "Password Change"
|
||||||
msgstr "Canvi contrasenya"
|
msgstr "Canvi contrasenya"
|
||||||
|
|
||||||
#: web/template/profile.html:38
|
#: web/template/profile.html:27
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Password Confirmation"
|
msgid "Password Confirmation"
|
||||||
msgstr "Confirmació contrasenya"
|
msgstr "Confirmació contrasenya"
|
||||||
|
|
||||||
#: web/template/profile.html:42
|
#: web/template/profile.html:31
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
msgstr "Idioma"
|
msgstr "Idioma"
|
||||||
|
|
||||||
#: web/template/profile.html:44
|
#: web/template/profile.html:33
|
||||||
msgctxt "language option"
|
msgctxt "language option"
|
||||||
msgid "Automatic"
|
msgid "Automatic"
|
||||||
msgstr "Automàtic"
|
msgstr "Automàtic"
|
||||||
|
|
||||||
#: web/template/profile.html:49
|
#: web/template/profile.html:38
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Save changes"
|
msgid "Save changes"
|
||||||
msgstr "Desa canvis"
|
msgstr "Desa canvis"
|
||||||
|
|
||||||
|
#: web/template/app.html:16
|
||||||
|
msgid "Account"
|
||||||
|
msgstr "Compte"
|
||||||
|
|
||||||
|
#: web/template/app.html:19
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "Logout"
|
||||||
|
msgstr "Surt"
|
||||||
|
|
48
po/es.po
48
po/es.po
|
@ -7,7 +7,7 @@ msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: numerus\n"
|
"Project-Id-Version: numerus\n"
|
||||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||||
"POT-Creation-Date: 2023-01-22 02:20+0100\n"
|
"POT-Creation-Date: 2023-01-23 00:41+0100\n"
|
||||||
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
|
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
|
||||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||||
"Language-Team: Spanish <es@tp.org.es>\n"
|
"Language-Team: Spanish <es@tp.org.es>\n"
|
||||||
|
@ -17,75 +17,75 @@ msgstr ""
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
#: web/template/login.html:6 web/template/login.html:19
|
#: web/template/web.html:6 web/template/login.html:9
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Entrada"
|
msgstr "Entrada"
|
||||||
|
|
||||||
#: web/template/login.html:14
|
#: web/template/login.html:5
|
||||||
msgid "Invalid user or password"
|
msgid "Invalid user or password"
|
||||||
msgstr "Nombre de usuario o contraseña inválido"
|
msgstr "Nombre de usuario o contraseña inválido"
|
||||||
|
|
||||||
#: web/template/login.html:21 web/template/profile.html:29
|
#: web/template/login.html:13 web/template/profile.html:14
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Email"
|
msgid "Email"
|
||||||
msgstr "Correo electrónico"
|
msgstr "Correo-e"
|
||||||
|
|
||||||
#: web/template/login.html:24 web/template/profile.html:35
|
#: web/template/login.html:18 web/template/profile.html:22
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Password"
|
msgid "Password"
|
||||||
msgstr "Contraseña"
|
msgstr "Contraseña"
|
||||||
|
|
||||||
#: web/template/login.html:27
|
#: web/template/login.html:21
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Login"
|
msgid "Login"
|
||||||
msgstr "Entrar"
|
msgstr "Entrar"
|
||||||
|
|
||||||
#: web/template/profile.html:6 web/template/profile.html:21
|
#: web/template/profile.html:2 pkg/profile.go:33
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "User Settings"
|
msgid "User Settings"
|
||||||
msgstr "Configuración usuario"
|
msgstr "Configuración usuario"
|
||||||
|
|
||||||
#: web/template/profile.html:15 web/template/index.html:15
|
#: web/template/profile.html:5
|
||||||
msgid "Account"
|
|
||||||
msgstr "Cuenta"
|
|
||||||
|
|
||||||
#: web/template/profile.html:16 web/template/index.html:16
|
|
||||||
msgctxt "action"
|
|
||||||
msgid "Logout"
|
|
||||||
msgstr "Salir"
|
|
||||||
|
|
||||||
#: web/template/profile.html:24
|
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "User Access Data"
|
msgid "User Access Data"
|
||||||
msgstr "Datos acceso usuario"
|
msgstr "Datos acceso usuario"
|
||||||
|
|
||||||
#: web/template/profile.html:26
|
#: web/template/profile.html:9
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "User name"
|
msgid "User name"
|
||||||
msgstr "Nombre de usuario"
|
msgstr "Nombre de usuario"
|
||||||
|
|
||||||
#: web/template/profile.html:33
|
#: web/template/profile.html:18
|
||||||
msgctxt "title"
|
msgctxt "title"
|
||||||
msgid "Password Change"
|
msgid "Password Change"
|
||||||
msgstr "Cambio de contraseña"
|
msgstr "Cambio de contraseña"
|
||||||
|
|
||||||
#: web/template/profile.html:38
|
#: web/template/profile.html:27
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Password Confirmation"
|
msgid "Password Confirmation"
|
||||||
msgstr "Confirmación contrasenya"
|
msgstr "Confirmación contrasenya"
|
||||||
|
|
||||||
#: web/template/profile.html:42
|
#: web/template/profile.html:31
|
||||||
msgctxt "input"
|
msgctxt "input"
|
||||||
msgid "Language"
|
msgid "Language"
|
||||||
msgstr "Idioma"
|
msgstr "Idioma"
|
||||||
|
|
||||||
#: web/template/profile.html:44
|
#: web/template/profile.html:33
|
||||||
msgctxt "language option"
|
msgctxt "language option"
|
||||||
msgid "Automatic"
|
msgid "Automatic"
|
||||||
msgstr "Automático"
|
msgstr "Automático"
|
||||||
|
|
||||||
#: web/template/profile.html:49
|
#: web/template/profile.html:38
|
||||||
msgctxt "action"
|
msgctxt "action"
|
||||||
msgid "Save changes"
|
msgid "Save changes"
|
||||||
msgstr "Guardar cambios"
|
msgstr "Guardar cambios"
|
||||||
|
|
||||||
|
#: web/template/app.html:16
|
||||||
|
msgid "Account"
|
||||||
|
msgstr "Cuenta"
|
||||||
|
|
||||||
|
#: web/template/app.html:19
|
||||||
|
msgctxt "action"
|
||||||
|
msgid "Logout"
|
||||||
|
msgstr "Salir"
|
||||||
|
|
|
@ -2,6 +2,6 @@
|
||||||
|
|
||||||
begin;
|
begin;
|
||||||
|
|
||||||
delete from numerus.language;
|
delete from public.language;
|
||||||
|
|
||||||
commit;
|
commit;
|
||||||
|
|
|
@ -16,8 +16,8 @@ encrypt_password [schema_auth user extension_pgcrypto] 2023-01-13T00:14:30Z jord
|
||||||
login_attempt [schema_auth] 2023-01-17T14:05:49Z jordi fita mas <jordi@tandem.blog> # Add table to log login attempts
|
login_attempt [schema_auth] 2023-01-17T14:05:49Z jordi fita mas <jordi@tandem.blog> # Add table to log login attempts
|
||||||
login [roles schema_numerus schema_auth extension_pgcrypto email user login_attempt] 2023-01-13T00:32:32Z jordi fita mas <jordi@tandem.blog> # Add function to login
|
login [roles schema_numerus schema_auth extension_pgcrypto email user login_attempt] 2023-01-13T00:32:32Z jordi fita mas <jordi@tandem.blog> # Add function to login
|
||||||
check_cookie [schema_public user] 2023-01-17T17:48:49Z jordi fita mas <jordi@tandem.blog> # Add function to check if a user cookie is valid
|
check_cookie [schema_public user] 2023-01-17T17:48:49Z jordi fita mas <jordi@tandem.blog> # Add function to check if a user cookie is valid
|
||||||
logout [schema_auth user] 2023-01-17T19:10:21Z jordi fita mas <jordi@tandem.blog> # Add function to logout
|
|
||||||
set_cookie [schema_public check_cookie] 2023-01-19T11:00:22Z jordi fita mas <jordi@tandem.blog> # Add function to set the role based on the cookie
|
|
||||||
current_app_user [schema_numerus] 2023-01-21T20:16:28Z jordi fita mas <jordi@tandem.blog> # Add function to get the ID of the current Numerus’ user
|
current_app_user [schema_numerus] 2023-01-21T20:16:28Z jordi fita mas <jordi@tandem.blog> # Add function to get the ID of the current Numerus’ user
|
||||||
|
logout [schema_auth current_app_user user] 2023-01-17T19:10:21Z jordi fita mas <jordi@tandem.blog> # Add function to logout
|
||||||
|
set_cookie [schema_public check_cookie] 2023-01-19T11:00:22Z jordi fita mas <jordi@tandem.blog> # Add function to set the role based on the cookie
|
||||||
available_languages [schema_numerus language] 2023-01-21T21:11:08Z jordi fita mas <jordi@tandem.blog> # Add the initial available languages
|
available_languages [schema_numerus language] 2023-01-21T21:11:08Z jordi fita mas <jordi@tandem.blog> # Add the initial available languages
|
||||||
user_profile [schema_numerus user current_app_user] 2023-01-21T23:18:20Z jordi fita mas <jordi@tandem.blog> # Add view for user profile
|
user_profile [schema_numerus user current_app_user] 2023-01-21T23:18:20Z jordi fita mas <jordi@tandem.blog> # Add view for user profile
|
||||||
|
|
|
@ -12,7 +12,7 @@ set search_path to numerus, public;
|
||||||
select has_domain('email');
|
select has_domain('email');
|
||||||
select domain_type_is('email', 'citext');
|
select domain_type_is('email', 'citext');
|
||||||
|
|
||||||
select lives_ok($$ SELECT 'test@tandem.com'::email $$, 'Should be able to cast strings to email');
|
select lives_ok($$ select 'test@tandem.com'::email $$, 'Should be able to cast strings to email');
|
||||||
|
|
||||||
select throws_ok(
|
select throws_ok(
|
||||||
$$ SELECT 'test@tandem,,co.uk'::email $$,
|
$$ SELECT 'test@tandem,,co.uk'::email $$,
|
||||||
|
|
|
@ -32,7 +32,7 @@ prepare user_cookies as
|
||||||
select cookie, cookie_expires_at from "user" order by user_id
|
select cookie, cookie_expires_at from "user" order by user_id
|
||||||
;
|
;
|
||||||
|
|
||||||
select set_config('request.user.id', '0', false);
|
select set_config('request.user.cookie', '', false);
|
||||||
select lives_ok( $$ select * from logout() $$, 'Can logout “nobody”' );
|
select lives_ok( $$ select * from logout() $$, 'Can logout “nobody”' );
|
||||||
|
|
||||||
select results_eq(
|
select results_eq(
|
||||||
|
@ -43,7 +43,7 @@ select results_eq(
|
||||||
'Nothing changed'
|
'Nothing changed'
|
||||||
);
|
);
|
||||||
|
|
||||||
select set_config('request.user.id', '1', false);
|
select set_config('request.user.cookie', '8c23d4a8d777775f8fc507676a0d99d3dfa54b03b1b257c838', false);
|
||||||
select lives_ok( $$ select * from logout() $$, 'Can logout the first user' );
|
select lives_ok( $$ select * from logout() $$, 'Can logout the first user' );
|
||||||
|
|
||||||
select results_eq(
|
select results_eq(
|
||||||
|
@ -54,7 +54,7 @@ select results_eq(
|
||||||
'The first user logged out'
|
'The first user logged out'
|
||||||
);
|
);
|
||||||
|
|
||||||
select set_config('request.user.id', '12', false);
|
select set_config('request.user.cookie', '0169e5f668eec1e6749fd25388b057997358efa8dfd697961a', false);
|
||||||
select lives_ok( $$ select * from logout() $$, 'Can logout the second user' );
|
select lives_ok( $$ select * from logout() $$, 'Can logout the second user' );
|
||||||
|
|
||||||
select results_eq(
|
select results_eq(
|
||||||
|
|
|
@ -10,42 +10,42 @@ select plan(47);
|
||||||
set search_path to numerus, auth, public;
|
set search_path to numerus, auth, public;
|
||||||
|
|
||||||
select has_view('user_profile');
|
select has_view('user_profile');
|
||||||
select table_privs_are('user_profile', 'guest', array []::text[]);
|
select table_privs_are('user_profile', 'guest', array ['SELECT']);
|
||||||
select table_privs_are('user_profile', 'invoicer', array['SELECT']);
|
select table_privs_are('user_profile', 'invoicer', array['SELECT']);
|
||||||
select table_privs_are('user_profile', 'admin', array['SELECT']);
|
select table_privs_are('user_profile', 'admin', array['SELECT']);
|
||||||
select table_privs_are('user_profile', 'authenticator', array[]::text[]);
|
select table_privs_are('user_profile', 'authenticator', array[]::text[]);
|
||||||
|
|
||||||
select has_column('user_profile', 'user_id');
|
select has_column('user_profile', 'user_id');
|
||||||
select col_type_is('user_profile', 'user_id', 'integer');
|
select col_type_is('user_profile', 'user_id', 'integer');
|
||||||
select column_privs_are('user_profile', 'user_id', 'guest', array []::text[]);
|
select column_privs_are('user_profile', 'user_id', 'guest', array ['SELECT']);
|
||||||
select column_privs_are('user_profile', 'user_id', 'invoicer', array['SELECT']);
|
select column_privs_are('user_profile', 'user_id', 'invoicer', array['SELECT']);
|
||||||
select column_privs_are('user_profile', 'user_id', 'admin', array['SELECT']);
|
select column_privs_are('user_profile', 'user_id', 'admin', array['SELECT']);
|
||||||
select column_privs_are('user_profile', 'user_id', 'authenticator', array[]::text[]);
|
select column_privs_are('user_profile', 'user_id', 'authenticator', array[]::text[]);
|
||||||
|
|
||||||
select has_column('user_profile', 'email');
|
select has_column('user_profile', 'email');
|
||||||
select col_type_is('user_profile', 'email', 'email');
|
select col_type_is('user_profile', 'email', 'email');
|
||||||
select column_privs_are('user_profile', 'email', 'guest', array []::text[]);
|
select column_privs_are('user_profile', 'email', 'guest', array ['SELECT']);
|
||||||
select column_privs_are('user_profile', 'email', 'invoicer', array['SELECT', 'UPDATE']);
|
select column_privs_are('user_profile', 'email', 'invoicer', array['SELECT', 'UPDATE']);
|
||||||
select column_privs_are('user_profile', 'email', 'admin', array['SELECT', 'UPDATE']);
|
select column_privs_are('user_profile', 'email', 'admin', array['SELECT', 'UPDATE']);
|
||||||
select column_privs_are('user_profile', 'email', 'authenticator', array[]::text[]);
|
select column_privs_are('user_profile', 'email', 'authenticator', array[]::text[]);
|
||||||
|
|
||||||
select has_column('user_profile', 'name');
|
select has_column('user_profile', 'name');
|
||||||
select col_type_is('user_profile', 'name', 'text');
|
select col_type_is('user_profile', 'name', 'text');
|
||||||
select column_privs_are('user_profile', 'name', 'guest', array []::text[]);
|
select column_privs_are('user_profile', 'name', 'guest', array ['SELECT']);
|
||||||
select column_privs_are('user_profile', 'name', 'invoicer', array['SELECT', 'UPDATE']);
|
select column_privs_are('user_profile', 'name', 'invoicer', array['SELECT', 'UPDATE']);
|
||||||
select column_privs_are('user_profile', 'name', 'admin', array['SELECT', 'UPDATE']);
|
select column_privs_are('user_profile', 'name', 'admin', array['SELECT', 'UPDATE']);
|
||||||
select column_privs_are('user_profile', 'name', 'authenticator', array[]::text[]);
|
select column_privs_are('user_profile', 'name', 'authenticator', array[]::text[]);
|
||||||
|
|
||||||
select has_column('user_profile', 'role');
|
select has_column('user_profile', 'role');
|
||||||
select col_type_is('user_profile', 'role', 'name');
|
select col_type_is('user_profile', 'role', 'name');
|
||||||
select column_privs_are('user_profile', 'role', 'guest', array []::text[]);
|
select column_privs_are('user_profile', 'role', 'guest', array ['SELECT']);
|
||||||
select column_privs_are('user_profile', 'role', 'invoicer', array['SELECT']);
|
select column_privs_are('user_profile', 'role', 'invoicer', array['SELECT']);
|
||||||
select column_privs_are('user_profile', 'role', 'admin', array['SELECT']);
|
select column_privs_are('user_profile', 'role', 'admin', array['SELECT']);
|
||||||
select column_privs_are('user_profile', 'role', 'authenticator', array[]::text[]);
|
select column_privs_are('user_profile', 'role', 'authenticator', array[]::text[]);
|
||||||
|
|
||||||
select has_column('user_profile', 'lang_tag');
|
select has_column('user_profile', 'lang_tag');
|
||||||
select col_type_is('user_profile', 'lang_tag', 'text');
|
select col_type_is('user_profile', 'lang_tag', 'text');
|
||||||
select column_privs_are('user_profile', 'lang_tag', 'guest', array []::text[]);
|
select column_privs_are('user_profile', 'lang_tag', 'guest', array ['SELECT']);
|
||||||
select column_privs_are('user_profile', 'lang_tag', 'invoicer', array['SELECT', 'UPDATE']);
|
select column_privs_are('user_profile', 'lang_tag', 'invoicer', array['SELECT', 'UPDATE']);
|
||||||
select column_privs_are('user_profile', 'lang_tag', 'admin', array['SELECT', 'UPDATE']);
|
select column_privs_are('user_profile', 'lang_tag', 'admin', array['SELECT', 'UPDATE']);
|
||||||
select column_privs_are('user_profile', 'lang_tag', 'authenticator', array[]::text[]);
|
select column_privs_are('user_profile', 'lang_tag', 'authenticator', array[]::text[]);
|
||||||
|
@ -58,13 +58,20 @@ reset client_min_messages;
|
||||||
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at, lang_tag)
|
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at, lang_tag)
|
||||||
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month', 'ca')
|
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month', 'ca')
|
||||||
, (5, 'admin@tandem.blog', 'Admin', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month', 'es')
|
, (5, 'admin@tandem.blog', 'Admin', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month', 'es')
|
||||||
|
, (7, 'another@tandem.blog', 'Another Admin', 'test', 'admin', default, default, default)
|
||||||
;
|
;
|
||||||
|
|
||||||
prepare profile as
|
prepare profile as
|
||||||
select user_id, email, name, role, lang_tag
|
select user_id, email, name, role, lang_tag
|
||||||
from user_profile;
|
from user_profile;
|
||||||
|
|
||||||
select is_empty( 'profile', 'Should be empty when no user is logger in' );
|
select set_config('request.user.cookie', '', false);
|
||||||
|
|
||||||
|
select results_eq(
|
||||||
|
'profile',
|
||||||
|
$$ values (0, null::email, '', 'guest'::name, 'und') $$,
|
||||||
|
'Should be set up with the guest user when no user logged in yet.'
|
||||||
|
);
|
||||||
|
|
||||||
select set_cookie( '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog' );
|
select set_cookie( '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog' );
|
||||||
|
|
||||||
|
@ -144,6 +151,7 @@ select results_eq(
|
||||||
$$ select user_id, email, name, lang_tag from auth."user" order by user_id $$,
|
$$ select user_id, email, name, lang_tag from auth."user" order by user_id $$,
|
||||||
$$ values (1, 'demo+update@tandem.blog'::email, 'Demo Update', 'es')
|
$$ values (1, 'demo+update@tandem.blog'::email, 'Demo Update', 'es')
|
||||||
, (5, 'admin+update@tandem.blog'::email, 'Admin Update', 'ca')
|
, (5, 'admin+update@tandem.blog'::email, 'Admin Update', 'ca')
|
||||||
|
, (7, 'another@tandem.blog'::email, 'Another Admin', 'und')
|
||||||
$$,
|
$$,
|
||||||
'Should have updated the base table’s data'
|
'Should have updated the base table’s data'
|
||||||
);
|
);
|
||||||
|
|
|
@ -152,7 +152,7 @@
|
||||||
--numerus--color--hay: #ffe673;
|
--numerus--color--hay: #ffe673;
|
||||||
|
|
||||||
--numerus--text-color: var(--numerus--color--black);
|
--numerus--text-color: var(--numerus--color--black);
|
||||||
--numerus--background-color: var(--numerus--color-white);
|
--numerus--background-color: var(--numerus--color--white);
|
||||||
--numerus--font-family: 'JetBrains Mono';
|
--numerus--font-family: 'JetBrains Mono';
|
||||||
|
|
||||||
--numerus--header--background-color: #ede9e5;
|
--numerus--header--background-color: #ede9e5;
|
||||||
|
@ -164,11 +164,13 @@ html, body {
|
||||||
|
|
||||||
html {
|
html {
|
||||||
font-family: var(--numerus--font-family), monospace;
|
font-family: var(--numerus--font-family), monospace;
|
||||||
|
font-size: 62.5%;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
color: var(--numerus--text-color);
|
|
||||||
background-color: var(--numerus--background-color);
|
background-color: var(--numerus--background-color);
|
||||||
|
color: var(--numerus--text-color);
|
||||||
|
font-size: 1.6rem;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
-webkit-font-smoothing: antialiased;
|
-webkit-font-smoothing: antialiased;
|
||||||
}
|
}
|
||||||
|
@ -203,17 +205,19 @@ input[type="submit"]:active, button:active {
|
||||||
}
|
}
|
||||||
|
|
||||||
.web {
|
.web {
|
||||||
margin: 5.3125rem 2.5rem;
|
margin: 8.5rem 4rem;
|
||||||
background-color: var(--numerus--header--background-color);
|
background-color: var(--numerus--header--background-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
.web h1 {
|
.web h1 {
|
||||||
margin-bottom: 1.875em;
|
padding-bottom: .9375em;
|
||||||
|
border-bottom: 1px solid var(--numerus--color--black);
|
||||||
|
margin-bottom: .625em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#login {
|
#login {
|
||||||
background-color: var(--numerus--color--hay);
|
background-color: var(--numerus--color--hay);
|
||||||
padding: 1.5625em;
|
padding: 1.25em;
|
||||||
}
|
}
|
||||||
|
|
||||||
#login h2 {
|
#login h2 {
|
||||||
|
@ -221,7 +225,7 @@ input[type="submit"]:active, button:active {
|
||||||
}
|
}
|
||||||
|
|
||||||
div[role="alert"].error {
|
div[role="alert"].error {
|
||||||
padding: 1.3125em;
|
padding: 1.25em;
|
||||||
background-color: var(--numerus--color--red);
|
background-color: var(--numerus--color--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -230,16 +234,60 @@ header {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
background-color: var(--numerus--header--background-color);
|
background-color: var(--numerus--header--background-color);
|
||||||
padding: .625rem 1.875rem;
|
padding: 0rem 3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav {
|
main {
|
||||||
|
padding: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type="text"], input[type="password"], input[type="email"], select {
|
||||||
|
background-color: var(--numerus--background-color);
|
||||||
|
border: 1px solid var(--numerus--color--black);
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
min-width: 30rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input input::placeholder {
|
||||||
|
color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input label, .input input:focus ~ label {
|
||||||
|
position: absolute;
|
||||||
|
font-style: italic;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input input:placeholder-shown ~ label {
|
||||||
|
font-size: 1em;
|
||||||
|
background-color: initial;
|
||||||
|
left: 2rem;
|
||||||
|
top: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input label, .input input:focus ~ label {
|
||||||
|
background-color: var(--numerus--background-color);
|
||||||
|
top: -.9rem;
|
||||||
|
left: 1rem;
|
||||||
|
font-size: 0.8em;
|
||||||
|
padding: 0 .25rem;
|
||||||
|
transition: 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.relative {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav > button {
|
#profilebutton {
|
||||||
width: 4.375rem;
|
width: 7rem;
|
||||||
height: 4.375rem;
|
height: 7rem;
|
||||||
|
margin: 1rem 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -247,36 +295,69 @@ nav > button {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav button {
|
#profilebutton, #profilemenu button {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul {
|
#profilemenu {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: none;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: -1.875em;
|
right: -1.875em;
|
||||||
top: calc(100% + .625rem);
|
top: 100%;
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
background-color: var(--numerus--color--white);
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul button, nav ul a {
|
header div:hover #profilemenu {
|
||||||
height: 5.25rem;
|
opacity: 1;
|
||||||
width: 31.25rem;
|
display: initial;
|
||||||
padding: 0 1.875rem;
|
}
|
||||||
|
|
||||||
|
header .overlay {
|
||||||
|
background-color: var(--numerus--header--background-color);
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
opacity: 0;
|
||||||
|
display: none;
|
||||||
|
pointer-events: none;
|
||||||
|
mix-blend-mode: multiply;
|
||||||
|
}
|
||||||
|
|
||||||
|
header div:hover + .overlay {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
#profilemenu li + li {
|
||||||
|
border-top: 1px solid var(--numerus--color--black);
|
||||||
|
}
|
||||||
|
|
||||||
|
#profilemenu button, #profilemenu a {
|
||||||
|
font-size: 2rem;
|
||||||
|
font-style: italic;
|
||||||
|
height: 8rem;
|
||||||
|
width: 46rem;
|
||||||
|
padding-left: 2rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border: 0;
|
border: 0;
|
||||||
background-color: var(--numerus--color--white);
|
|
||||||
color: var(--numerus--text-color);
|
color: var(--numerus--text-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
text-transform: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul i[class^='ri-'] {
|
#profilemenu i[class^='ri-'] {
|
||||||
font-size: 1.25em;
|
margin-right: 2rem;
|
||||||
margin-right: 1.25rem;
|
color: var(--numerus--color--dark-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
nav ul button:hover, nav ul a:hover {
|
#profilemenu button:hover, #profilemenu a:hover {
|
||||||
background-color: var(--numerus--color--light-gray);
|
background-color: var(--numerus--color--light-gray);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{ .Title }} — Numerus</title>
|
||||||
|
<link rel="stylesheet" type="text/css" media="screen" href="/static/numerus.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1><img src="/static/numerus.svg" alt="Numerus" width="261" height="33"></h1>
|
||||||
|
<div class="relative">
|
||||||
|
<button id="profilebutton" aria-controls="profilemenu" aria-haspopup="true"><i class="ri-eye-close-line ri-3x"></i></button>
|
||||||
|
<ul id="profilemenu" role="menu" aria-labelledby="profilebutton">
|
||||||
|
<li role="presentation">
|
||||||
|
<a role="menuitem" href="/profile"><i class="ri-account-circle-line"></i> {{( gettext "Account" )}}</a>
|
||||||
|
</li>
|
||||||
|
<li role="presentation">
|
||||||
|
<form method="POST" action="/logout"><button type="submit" role="menuitem"><i class="ri-logout-circle-line"></i> {{( pgettext "Logout" "action" )}}</button></form>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="overlay"></div>
|
||||||
|
</header>
|
||||||
|
<main>
|
||||||
|
{{- template "content" . }}
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -0,0 +1,2 @@
|
||||||
|
{{ define "content" }}
|
||||||
|
{{- end }}
|
|
@ -1,23 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>Numerus</title>
|
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/static/numerus.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1><img src="/static/numerus.svg" alt="Numerus" width="261" height="33"></h1>
|
|
||||||
<nav role="navigation">
|
|
||||||
<button aria-haspopup="true"><i class="ri-eye-close-line ri-3x"></i></button>
|
|
||||||
<ul>
|
|
||||||
<li><a href="/profile"><i class="ri-account-circle-line"></i> {{( gettext "Account" )}}</a></li>
|
|
||||||
<li><form method="POST" action="/logout"><button type="submit"><i class="ri-logout-circle-line"></i> {{( pgettext "Logout" "action" )}}</button></form></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
</main>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,31 +1,24 @@
|
||||||
<!doctype html>
|
{{ define "content" }}
|
||||||
<html lang="en">
|
<h1><img src="/static/numerus.svg" alt="Numerus" width="620" height="77"></h1>
|
||||||
<head>
|
{{ if .LoginError -}}
|
||||||
<meta charset="utf-8">
|
<div class="error" role="alert">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<p>{{( gettext "Invalid user or password" )}}</p>
|
||||||
<title>{{( pgettext "Login" "title" )}} — Numerus</title>
|
</div>
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/static/numerus.css">
|
{{- end }}
|
||||||
</head>
|
<section id="login">
|
||||||
<body class="web">
|
<h2>{{( pgettext "Login" "title" )}}</h2>
|
||||||
<h1><img src="/static/numerus.svg" alt="Numerus" width="620" height="77"></h1>
|
<form method="POST" action="/login">
|
||||||
|
<div class="input">
|
||||||
{{ if .LoginError }}
|
<input id="user_email" type="email" required autofocus name="email" autocapitalize="none" value="{{ .Email }}" placeholder="{{( pgettext "Email" "input" )}}">
|
||||||
<div class="error" role="alert">
|
|
||||||
<p>{{( gettext "Invalid user or password" )}}</p>
|
|
||||||
</div>
|
|
||||||
{{ end }}
|
|
||||||
|
|
||||||
<section id="login">
|
|
||||||
<h2>{{( pgettext "Login" "title" )}}</h2>
|
|
||||||
<form method="POST" action="/login">
|
|
||||||
<label for="user_email">{{( pgettext "Email" "input" )}}</label>
|
<label for="user_email">{{( pgettext "Email" "input" )}}</label>
|
||||||
<input id="user_email" type="email" required autofocus name="email" autocapitalize="none" value="{{ .Email }}">
|
</input>
|
||||||
|
|
||||||
|
<div class="input">
|
||||||
|
<input id="user_password" type="password" required name="password" autocomplete="current-password" value="{{ .Password }}" placeholder="{{( pgettext "Password" "input" )}}">
|
||||||
<label for="user_password">{{( pgettext "Password" "input" )}}</label>
|
<label for="user_password">{{( pgettext "Password" "input" )}}</label>
|
||||||
<input id="user_password" type="password" required name="password" autocomplete="current-password" value="{{ .Password }}">
|
</div>
|
||||||
|
|
||||||
<button type="submit">{{( pgettext "Login" "action" )}}</button>
|
<button type="submit">{{( pgettext "Login" "action" )}}</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
</body>
|
{{- end }}
|
||||||
</html>
|
|
||||||
|
|
|
@ -1,53 +1,40 @@
|
||||||
<!doctype html>
|
{{ define "content" }}
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<title>{{(pgettext "User Settings" "title")}} — Numerus</title>
|
|
||||||
<link rel="stylesheet" type="text/css" media="screen" href="/static/numerus.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header>
|
|
||||||
<h1><img src="/static/numerus.svg" alt="Numerus" width="261" height="33"></h1>
|
|
||||||
<nav role="navigation">
|
|
||||||
<button aria-haspopup="true"><i class="ri-eye-close-line ri-3x"></i></button>
|
|
||||||
<ul>
|
|
||||||
<li><a href="/profile"><i class="ri-account-circle-line"></i> {{( gettext "Account" )}}</a></li>
|
|
||||||
<li><form method="POST" action="/logout"><button type="submit"><i class="ri-logout-circle-line"></i> {{( pgettext "Logout" "action" )}}</button></form></li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</header>
|
|
||||||
<main>
|
|
||||||
<h2>{{(pgettext "User Settings" "title")}}</h2>
|
<h2>{{(pgettext "User Settings" "title")}}</h2>
|
||||||
<form method="POST" action="/profile">
|
<form method="POST" action="/profile">
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{{( pgettext "User Access Data" "title" )}}</legend>
|
<legend>{{( pgettext "User Access Data" "title" )}}</legend>
|
||||||
|
|
||||||
<label for="name">{{( pgettext "User name" "input" )}}</label>
|
<div class="input">
|
||||||
<input type="text" name="name" id="name" required="required" value="{{ .Name }}">
|
<input type="text" name="name" id="name" required="required" value="{{ .Name }}" placeholder="{{( pgettext "User name" "input" )}}">
|
||||||
|
<label for="name">{{( pgettext "User name" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="email">{{( pgettext "Email" "input" )}}</label>
|
<div class="input">
|
||||||
<input type="email" name="email" id="email" required="required" value="{{ .Email }}">
|
<input type="email" name="email" id="email" required="required" value="{{ .Email }}" placeholder="{{( pgettext "Email" "input" )}}">
|
||||||
|
<label for="email">{{( pgettext "Email" "input" )}}</label>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>{{( pgettext "Password Change" "title" )}}</legend>
|
<legend>{{( pgettext "Password Change" "title" )}}</legend>
|
||||||
|
|
||||||
<label for="password">{{( pgettext "Password" "input" )}}</label>
|
<div class="input">
|
||||||
<input type="password" name="password" id="password" value="{{ .Password }}">
|
<input type="password" name="password" id="password" value="{{ .Password }}" placeholder="{{( pgettext "Password" "input" )}}">
|
||||||
|
<label for="password">{{( pgettext "Password" "input" )}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label for="password_confirm">{{( pgettext "Password Confirmation" "input" )}}</label>
|
<div class="input">
|
||||||
<input type="password" name="password_confirm" id="password_confirm" value="{{ .PasswordConfirm }}">
|
<input type="password" name="password_confirm" id="password_confirm" value="{{ .PasswordConfirm }}" placeholder="{{( pgettext "Password Confirmation" "input" )}}">
|
||||||
|
<label for="password_confirm">{{( pgettext "Password Confirmation" "input" )}}</label>
|
||||||
|
</div>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<label for="language">{{( pgettext "Language" "input" )}}</label>
|
<label for="language">{{( pgettext "Language" "input" )}}</label>
|
||||||
<select id="language" name="language">
|
<select id="language" name="language">
|
||||||
<option value="und">{{( pgettext "Automatic" "language option" )}}</option>
|
<option value="und">{{( pgettext "Automatic" "language option" )}}</option>
|
||||||
{{ range $language := .Languages }}
|
{{- range $language := .Languages }}
|
||||||
<option value="{{ .Tag }}" {{ if eq .Tag $.Language }}selected="selected"{{ end }}>{{ .Name }}</option>
|
<option value="{{ .Tag }}" {{ if eq .Tag $.Language }}selected="selected"{{ end }}>{{ .Name }}</option>
|
||||||
{{ end }}
|
{{- end }}
|
||||||
</select>
|
</select>
|
||||||
<button type="submit">{{( pgettext "Save changes" "action" )}}</button>
|
<button type="submit">{{( pgettext "Save changes" "action" )}}</button>
|
||||||
</form>
|
</form>
|
||||||
</main>
|
{{- end }}
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>{{( pgettext "Login" "title" )}} — Numerus</title>
|
||||||
|
<link rel="stylesheet" type="text/css" media="screen" href="/static/numerus.css">
|
||||||
|
</head>
|
||||||
|
<body class="web">
|
||||||
|
{{- template "content" . }}
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue