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.
This commit is contained in:
parent
052c9c8caa
commit
ea9e830a75
|
@ -10,13 +10,19 @@ Copyright:
|
|||
License: AGPL-3.0-only
|
||||
|
||||
Files:
|
||||
web/static/fonts/*
|
||||
web/static/fonts/JetBrainsMono*
|
||||
Copyright:
|
||||
2020 Philipp Nurullin
|
||||
2020 Konstantin Bulenkov
|
||||
2020 The JetBrains Mono Project Authors
|
||||
License: OFL-1.1
|
||||
|
||||
Files:
|
||||
web/static/fonts/remixicon*
|
||||
Copyright:
|
||||
2020 RemixIcon.com
|
||||
License: Apache-2.0
|
||||
|
||||
License: OFL-1.1
|
||||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
.
|
||||
|
@ -111,3 +117,181 @@ License: OFL-1.1
|
|||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
|
||||
License: Apache-2.0
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
.
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
.
|
||||
1. Definitions.
|
||||
.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
.
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
.
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
.
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
.
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
.
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
.
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
.
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
.
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
.
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
.
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
.
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
.
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
.
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
.
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
.
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
.
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
.
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
.
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
.
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
.
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
.
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
.
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
-- Deploy numerus:available_languages to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: language
|
||||
|
||||
begin;
|
||||
|
||||
insert into public.language (lang_tag, name, endonym, selectable)
|
||||
values ('und', 'Undefined', 'Undefined', false)
|
||||
, ('ca', 'Catalan', 'català', true)
|
||||
, ('es', 'Spanish', 'español', true)
|
||||
;
|
||||
|
||||
commit;
|
|
@ -12,9 +12,10 @@ declare
|
|||
uid text;
|
||||
user_email text;
|
||||
user_role name;
|
||||
user_cookie text;
|
||||
begin
|
||||
select user_id::text, email::text, role
|
||||
into uid, user_email, user_role
|
||||
select user_id::text, email::text, role, cookie
|
||||
into uid, user_email, user_role, user_cookie
|
||||
from "user"
|
||||
where email = split_part(input_cookie, '/', 2)
|
||||
and cookie_expires_at > current_timestamp
|
||||
|
@ -24,10 +25,12 @@ begin
|
|||
if user_role is null then
|
||||
uid := '0';
|
||||
user_email := '';
|
||||
user_cookie := '';
|
||||
user_role := 'guest'::name;
|
||||
end if;
|
||||
perform set_config('request.user.id', uid, false);
|
||||
perform set_config('request.user.email', user_email, false);
|
||||
perform set_config('request.user.cookie', user_cookie, false);
|
||||
return user_role;
|
||||
end;
|
||||
$$
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
-- Deploy numerus:current_app_user to pg
|
||||
-- requires: schema_numerus
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus;
|
||||
|
||||
create or replace function current_app_user() returns text as
|
||||
$$
|
||||
select current_setting('request.user.cookie', true);
|
||||
$$
|
||||
language sql
|
||||
stable;
|
||||
|
||||
comment on function current_app_user() is
|
||||
'Returns the ID of the current Numerus user';
|
||||
|
||||
revoke execute on function current_app_user() from public;
|
||||
grant execute on function current_app_user() to guest;
|
||||
grant execute on function current_app_user() to invoicer;
|
||||
grant execute on function current_app_user() to admin;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,29 @@
|
|||
-- Deploy numerus:language to pg
|
||||
-- requires: schema_numerus
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to public;
|
||||
|
||||
create table language (
|
||||
lang_tag text primary key check (length(lang_tag) < 36), -- RFC5646 recommends 35 at least
|
||||
name text not null,
|
||||
endonym text not null,
|
||||
selectable boolean not null
|
||||
);
|
||||
|
||||
grant select on table language to guest;
|
||||
grant select on table language to invoicer;
|
||||
grant select on table language to admin;
|
||||
grant select on table language to authenticator;
|
||||
|
||||
comment on table language is
|
||||
'Languages/locales available in Numerus';
|
||||
|
||||
comment on column language.lang_tag is
|
||||
'BCP-47 language tag';
|
||||
|
||||
comment on column language.selectable is
|
||||
'Whether the language should be a option in a user-facing select control.';
|
||||
|
||||
commit;
|
|
@ -2,10 +2,11 @@
|
|||
-- requires: roles
|
||||
-- requires: schema_auth
|
||||
-- requires: email
|
||||
-- requires: language
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to auth, numerus;
|
||||
set search_path to auth, numerus, public;
|
||||
|
||||
create table "user" (
|
||||
user_id serial primary key,
|
||||
|
@ -13,6 +14,7 @@ create table "user" (
|
|||
name text not null,
|
||||
password text not null check (length(password) < 512),
|
||||
role name not null check (length(role) < 512),
|
||||
lang_tag text not null default 'und' references language,
|
||||
cookie text not null default '',
|
||||
cookie_expires_at timestamptz not null default '-infinity'::timestamp,
|
||||
created_at timestamptz not null default current_timestamp
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
-- Deploy numerus:user_profile to pg
|
||||
-- requires: schema_numerus
|
||||
-- requires: user
|
||||
-- requires: current_app_user
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, public;
|
||||
|
||||
create or replace view user_profile
|
||||
with (security_barrier)
|
||||
as
|
||||
select user_id
|
||||
, email
|
||||
, name
|
||||
, role
|
||||
, lang_tag
|
||||
from auth."user"
|
||||
where cookie = current_app_user()
|
||||
;
|
||||
|
||||
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;
|
||||
|
||||
commit;
|
|
@ -10,15 +10,12 @@ import (
|
|||
|
||||
const contextLocaleKey = "numerus-locale"
|
||||
|
||||
func Locale(next http.Handler) http.Handler {
|
||||
supportedLanguages := []language.Tag{
|
||||
language.Catalan,
|
||||
language.Spanish,
|
||||
}
|
||||
var matcher = language.NewMatcher(supportedLanguages)
|
||||
func Locale(db *Db, next http.Handler) http.Handler {
|
||||
availableLanguages := getAvailableLanguages(db)
|
||||
var matcher = language.NewMatcher(availableLanguages)
|
||||
|
||||
locales := map[language.Tag]*gotext.Locale{}
|
||||
for _, lang := range supportedLanguages {
|
||||
for _, lang := range availableLanguages {
|
||||
locale := gotext.NewLocale("locales", lang.String())
|
||||
locale.AddDomain("numerus")
|
||||
locales[lang] = locale
|
||||
|
@ -48,3 +45,26 @@ func Locale(next http.Handler) http.Handler {
|
|||
func getLocale(r *http.Request) *gotext.Locale {
|
||||
return r.Context().Value(contextLocaleKey).(*gotext.Locale)
|
||||
}
|
||||
|
||||
func getAvailableLanguages(db *Db) []language.Tag {
|
||||
rows, err := db.Query(context.Background(), "select lang_tag from language where selectable")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var langs []language.Tag
|
||||
for rows.Next() {
|
||||
var lang_tag string
|
||||
err = rows.Scan(&lang_tag)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
langs = append(langs, language.MustParse(lang_tag))
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
panic(rows.Err())
|
||||
}
|
||||
|
||||
return langs
|
||||
}
|
||||
|
|
18
pkg/login.go
18
pkg/login.go
|
@ -8,11 +8,11 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
ContextUserKey = "numerus-user"
|
||||
ContextUserKey = "numerus-user"
|
||||
ContextCookieKey = "numerus-cookie"
|
||||
ContextConnKey = "numerus-database"
|
||||
sessionCookie = "numerus-session"
|
||||
defaultRole = "guest"
|
||||
ContextConnKey = "numerus-database"
|
||||
sessionCookie = "numerus-session"
|
||||
defaultRole = "guest"
|
||||
)
|
||||
|
||||
type LoginPage struct {
|
||||
|
@ -89,16 +89,16 @@ func createSessionCookie(value string, duration time.Duration) *http.Cookie {
|
|||
|
||||
func CheckLogin(db *Db, next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var ctx = r.Context();
|
||||
var ctx = r.Context()
|
||||
if cookie, err := r.Cookie(sessionCookie); err == nil {
|
||||
ctx = context.WithValue(ctx, ContextCookieKey, cookie.Value);
|
||||
ctx = context.WithValue(ctx, ContextCookieKey, cookie.Value)
|
||||
}
|
||||
|
||||
conn, err := db.Acquire(ctx)
|
||||
if err != nil {
|
||||
panic(err);
|
||||
panic(err)
|
||||
}
|
||||
defer conn.Release();
|
||||
defer conn.Release()
|
||||
ctx = context.WithValue(ctx, ContextConnKey, conn)
|
||||
|
||||
user := &AppUser{
|
||||
|
@ -108,7 +108,7 @@ func CheckLogin(db *Db, next http.Handler) http.Handler {
|
|||
}
|
||||
row := conn.QueryRow(ctx, "select current_setting('request.user.email', true), current_user")
|
||||
if err := row.Scan(&user.Email, &user.Role); err != nil {
|
||||
panic(err)
|
||||
panic(err)
|
||||
}
|
||||
user.LoggedIn = user.Email != ""
|
||||
ctx = context.WithValue(ctx, ContextUserKey, user)
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
package pkg
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type LanguageOption struct {
|
||||
Tag string
|
||||
Name string
|
||||
}
|
||||
|
||||
type ProfilePage struct {
|
||||
Name string
|
||||
Email string
|
||||
Password string
|
||||
PasswordConfirm string
|
||||
Language string
|
||||
Languages []LanguageOption
|
||||
}
|
||||
|
||||
func ProfileHandler() http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
user := getUser(r)
|
||||
if !user.LoggedIn {
|
||||
http.Redirect(w, r, "/login", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
conn := getConn(r)
|
||||
page := ProfilePage{
|
||||
Email: user.Email,
|
||||
Languages: getLanguageOptions(r.Context(), conn),
|
||||
}
|
||||
if r.Method == "POST" {
|
||||
r.ParseForm()
|
||||
page.Email = r.FormValue("email")
|
||||
page.Name = r.FormValue("name")
|
||||
page.Password = r.FormValue("password")
|
||||
page.PasswordConfirm = r.FormValue("password_confirm")
|
||||
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);
|
||||
} else {
|
||||
if err := conn.QueryRow(r.Context(), "select name, lang_tag from user_profile").Scan(&page.Name, &page.Language); err != nil {
|
||||
panic(nil)
|
||||
}
|
||||
}
|
||||
renderTemplate(w, r, "profile.html", page)
|
||||
})
|
||||
}
|
||||
|
||||
func getLanguageOptions(ctx context.Context, conn *Conn) []LanguageOption {
|
||||
rows, err := conn.Query(ctx, "select lang_tag, endonym from language where selectable")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var langs []LanguageOption
|
||||
for rows.Next() {
|
||||
var lang LanguageOption
|
||||
err = rows.Scan(&lang.Tag, &lang.Name)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
langs = append(langs, lang)
|
||||
}
|
||||
if rows.Err() != nil {
|
||||
panic(rows.Err())
|
||||
}
|
||||
|
||||
return langs
|
||||
}
|
|
@ -9,6 +9,7 @@ func NewRouter(db *Db) http.Handler {
|
|||
router.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("web/static"))))
|
||||
router.Handle("/login", LoginHandler())
|
||||
router.Handle("/logout", LogoutHandler())
|
||||
router.Handle("/profile", ProfileHandler())
|
||||
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
user := getUser(r)
|
||||
if user.LoggedIn {
|
||||
|
@ -18,7 +19,7 @@ func NewRouter(db *Db) http.Handler {
|
|||
}
|
||||
})
|
||||
var handler http.Handler = router
|
||||
handler = Locale(handler)
|
||||
handler = Locale(db, handler)
|
||||
handler = CheckLogin(db, handler)
|
||||
handler = Recoverer(handler)
|
||||
handler = Logger(handler)
|
||||
|
|
52
po/ca.po
52
po/ca.po
|
@ -8,7 +8,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: numerus\n"
|
||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||
"POT-Creation-Date: 2023-01-18 17:54+0100\n"
|
||||
"POT-Creation-Date: 2023-01-22 02:20+0100\n"
|
||||
"PO-Revision-Date: 2023-01-18 17:08+0100\n"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Catalan <ca@dodds.net>\n"
|
||||
|
@ -26,12 +26,12 @@ msgstr "Entrada"
|
|||
msgid "Invalid user or password"
|
||||
msgstr "Nom d’usuari o contrasenya incorrectes"
|
||||
|
||||
#: web/template/login.html:21
|
||||
#: web/template/login.html:21 web/template/profile.html:29
|
||||
msgctxt "input"
|
||||
msgid "Email"
|
||||
msgstr "Correu electrònic"
|
||||
|
||||
#: web/template/login.html:24
|
||||
#: web/template/login.html:24 web/template/profile.html:35
|
||||
msgctxt "input"
|
||||
msgid "Password"
|
||||
msgstr "Contrasenya"
|
||||
|
@ -41,7 +41,51 @@ msgctxt "action"
|
|||
msgid "Login"
|
||||
msgstr "Entra"
|
||||
|
||||
#: web/template/index.html:16
|
||||
#: web/template/profile.html:6 web/template/profile.html:21
|
||||
msgctxt "title"
|
||||
msgid "User Settings"
|
||||
msgstr "Configuració usuari"
|
||||
|
||||
#: web/template/profile.html:15 web/template/index.html:15
|
||||
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"
|
||||
msgid "User Access Data"
|
||||
msgstr "Dades accés usuari"
|
||||
|
||||
#: web/template/profile.html:26
|
||||
msgctxt "input"
|
||||
msgid "User name"
|
||||
msgstr "Nom d’usuari"
|
||||
|
||||
#: web/template/profile.html:33
|
||||
msgctxt "title"
|
||||
msgid "Password Change"
|
||||
msgstr "Canvi contrasenya"
|
||||
|
||||
#: web/template/profile.html:38
|
||||
msgctxt "input"
|
||||
msgid "Password Confirmation"
|
||||
msgstr "Confirmació contrasenya"
|
||||
|
||||
#: web/template/profile.html:42
|
||||
msgctxt "input"
|
||||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: web/template/profile.html:44
|
||||
msgctxt "language option"
|
||||
msgid "Automatic"
|
||||
msgstr "Automàtic"
|
||||
|
||||
#: web/template/profile.html:49
|
||||
msgctxt "action"
|
||||
msgid "Save changes"
|
||||
msgstr "Desa canvis"
|
||||
|
|
52
po/es.po
52
po/es.po
|
@ -7,7 +7,7 @@ msgid ""
|
|||
msgstr ""
|
||||
"Project-Id-Version: numerus\n"
|
||||
"Report-Msgid-Bugs-To: jordi@tandem.blog\n"
|
||||
"POT-Creation-Date: 2023-01-18 17:54+0100\n"
|
||||
"POT-Creation-Date: 2023-01-22 02:20+0100\n"
|
||||
"PO-Revision-Date: 2023-01-18 17:45+0100\n"
|
||||
"Last-Translator: jordi fita mas <jordi@tandem.blog>\n"
|
||||
"Language-Team: Spanish <es@tp.org.es>\n"
|
||||
|
@ -26,12 +26,12 @@ msgstr "Entrada"
|
|||
msgid "Invalid user or password"
|
||||
msgstr "Nombre de usuario o contraseña inválido"
|
||||
|
||||
#: web/template/login.html:21
|
||||
#: web/template/login.html:21 web/template/profile.html:29
|
||||
msgctxt "input"
|
||||
msgid "Email"
|
||||
msgstr "Correo electrónico"
|
||||
|
||||
#: web/template/login.html:24
|
||||
#: web/template/login.html:24 web/template/profile.html:35
|
||||
msgctxt "input"
|
||||
msgid "Password"
|
||||
msgstr "Contraseña"
|
||||
|
@ -41,7 +41,51 @@ msgctxt "action"
|
|||
msgid "Login"
|
||||
msgstr "Entrar"
|
||||
|
||||
#: web/template/index.html:16
|
||||
#: web/template/profile.html:6 web/template/profile.html:21
|
||||
msgctxt "title"
|
||||
msgid "User Settings"
|
||||
msgstr "Configuración usuario"
|
||||
|
||||
#: web/template/profile.html:15 web/template/index.html:15
|
||||
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"
|
||||
msgid "User Access Data"
|
||||
msgstr "Datos acceso usuario"
|
||||
|
||||
#: web/template/profile.html:26
|
||||
msgctxt "input"
|
||||
msgid "User name"
|
||||
msgstr "Nombre de usuario"
|
||||
|
||||
#: web/template/profile.html:33
|
||||
msgctxt "title"
|
||||
msgid "Password Change"
|
||||
msgstr "Cambio de contraseña"
|
||||
|
||||
#: web/template/profile.html:38
|
||||
msgctxt "input"
|
||||
msgid "Password Confirmation"
|
||||
msgstr "Confirmación contrasenya"
|
||||
|
||||
#: web/template/profile.html:42
|
||||
msgctxt "input"
|
||||
msgid "Language"
|
||||
msgstr "Idioma"
|
||||
|
||||
#: web/template/profile.html:44
|
||||
msgctxt "language option"
|
||||
msgid "Automatic"
|
||||
msgstr "Automático"
|
||||
|
||||
#: web/template/profile.html:49
|
||||
msgctxt "action"
|
||||
msgid "Save changes"
|
||||
msgstr "Guardar cambios"
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:available_languages from pg
|
||||
|
||||
begin;
|
||||
|
||||
delete from numerus.language;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:current_app_user from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop function if exists numerus.current_app_user();
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:language from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop table if exists public.language;
|
||||
|
||||
commit;
|
|
@ -0,0 +1,7 @@
|
|||
-- Revert numerus:user_profile from pg
|
||||
|
||||
begin;
|
||||
|
||||
drop view if exists numerus.user_profile;
|
||||
|
||||
commit;
|
|
@ -8,7 +8,8 @@ schema_public [roles] 2023-01-12T19:24:29Z jordi fita mas <jordi@tandem.blog> #
|
|||
schema_numerus [roles] 2023-01-12T22:57:22Z jordi fita mas <jordi@tandem.blog> # Add application schema
|
||||
extension_citext [schema_public] 2023-01-12T23:03:33Z jordi fita mas <jordi@tandem.blog> # Add citext extension
|
||||
email [schema_numerus extension_citext] 2023-01-12T23:09:59Z jordi fita mas <jordi@tandem.blog> # Add email domain
|
||||
user [roles schema_auth email] 2023-01-12T23:44:03Z jordi fita mas <jordi@tandem.blog> # Create user table
|
||||
language [schema_numerus] 2023-01-21T20:55:49Z jordi fita mas <jordi@tandem.blog> # Add relation of available languages
|
||||
user [roles schema_auth email language] 2023-01-12T23:44:03Z jordi fita mas <jordi@tandem.blog> # Create user table
|
||||
ensure_role_exists [schema_auth user] 2023-01-12T23:57:59Z jordi fita mas <jordi@tandem.blog> # Add trigger to ensure the user’s role exists
|
||||
extension_pgcrypto [schema_auth] 2023-01-13T00:11:50Z jordi fita mas <jordi@tandem.blog> # Add pgcrypto extension
|
||||
encrypt_password [schema_auth user extension_pgcrypto] 2023-01-13T00:14:30Z jordi fita mas <jordi@tandem.blog> # Add trigger to encrypt user’s password
|
||||
|
@ -17,3 +18,6 @@ login [roles schema_numerus schema_auth extension_pgcrypto email user login_atte
|
|||
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
|
||||
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
|
||||
|
|
|
@ -29,7 +29,10 @@ values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4b
|
|||
;
|
||||
|
||||
prepare user_info as
|
||||
select current_setting('request.user.id', true)::integer, current_setting('request.user.email', true);
|
||||
select current_setting('request.user.id', true)::integer
|
||||
, current_setting('request.user.email', true)
|
||||
, current_setting('request.user.cookie', true)
|
||||
;
|
||||
|
||||
select is (
|
||||
check_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog'),
|
||||
|
@ -39,7 +42,7 @@ select is (
|
|||
|
||||
select results_eq (
|
||||
'user_info',
|
||||
$$ values (1, 'demo@tandem.blog') $$,
|
||||
$$ values (1, 'demo@tandem.blog', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e') $$,
|
||||
'Should have updated the settings with the user info'
|
||||
);
|
||||
|
||||
|
@ -51,7 +54,7 @@ select is (
|
|||
|
||||
select results_eq (
|
||||
'user_info',
|
||||
$$ values (9, 'admin@tandem.blog') $$,
|
||||
$$ values (9, 'admin@tandem.blog', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524') $$,
|
||||
'Should have updated the settings with the other user info'
|
||||
);
|
||||
|
||||
|
@ -63,7 +66,7 @@ select is (
|
|||
|
||||
select results_eq (
|
||||
'user_info',
|
||||
$$ values (0, '') $$,
|
||||
$$ values (0, '', '') $$,
|
||||
'Should have updated the settings with a guest user'
|
||||
);
|
||||
|
||||
|
@ -75,7 +78,7 @@ select is (
|
|||
|
||||
select results_eq (
|
||||
'user_info',
|
||||
$$ values (0, '') $$,
|
||||
$$ values (0, '', '') $$,
|
||||
'Should have left the settings with a guest user'
|
||||
);
|
||||
|
||||
|
@ -89,7 +92,7 @@ select is (
|
|||
|
||||
select results_eq (
|
||||
'user_info',
|
||||
$$ values (0, '') $$,
|
||||
$$ values (0, '', '') $$,
|
||||
'Should have left the settings with a guest user'
|
||||
);
|
||||
|
||||
|
@ -101,7 +104,7 @@ select is (
|
|||
|
||||
select results_eq (
|
||||
'user_info',
|
||||
$$ values (0, '') $$,
|
||||
$$ values (0, '', '') $$,
|
||||
'Should have left the settings with a guest user'
|
||||
);
|
||||
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
-- Test current_app_user
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
select plan(15);
|
||||
|
||||
select has_function('numerus', 'current_app_user', array []::name[]);
|
||||
select function_lang_is('numerus', 'current_app_user', array []::name[], 'sql');
|
||||
select function_returns('numerus', 'current_app_user', array []::name[], 'text');
|
||||
select isnt_definer('numerus', 'current_app_user', array []::name[]);
|
||||
select volatility_is('numerus', 'current_app_user', array []::name[], 'stable');
|
||||
select function_privs_are('numerus', 'current_app_user', array []::name[], 'guest', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'current_app_user', array []::name[], 'invoicer', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'current_app_user', array []::name[], 'admin', array ['EXECUTE']);
|
||||
select function_privs_are('numerus', 'current_app_user', array []::name[], 'authenticator', array []::text []);
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate auth."user" cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
insert into auth."user" (user_id, email, name, password, role, cookie, cookie_expires_at)
|
||||
values (1, 'demo@tandem.blog', 'Demo', 'test', 'invoicer', '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', current_timestamp + interval '1 month')
|
||||
, (5, 'admin@tandem.blog', 'Demo', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month')
|
||||
;
|
||||
|
||||
select lives_ok(
|
||||
$$ select set_cookie('44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog') $$,
|
||||
'Should change ok for the first user'
|
||||
);
|
||||
|
||||
select is(current_app_user(), '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e', 'Should be running as the first user');
|
||||
|
||||
reset role;
|
||||
|
||||
|
||||
select lives_ok(
|
||||
$$ select set_cookie('12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog') $$,
|
||||
'Should change ok for the second user'
|
||||
);
|
||||
|
||||
select is(current_app_user(), '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', 'Should be running as the second user');
|
||||
|
||||
reset role;
|
||||
|
||||
select lives_ok(
|
||||
$$ select set_cookie('') $$,
|
||||
'Should change ok for a guest user'
|
||||
);
|
||||
|
||||
select is(current_app_user(), '', 'Should be running as the first user');
|
||||
|
||||
reset role;
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,43 @@
|
|||
-- Test language
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to public;
|
||||
|
||||
select plan(23);
|
||||
|
||||
select has_table('language');
|
||||
select has_pk('language');
|
||||
select table_privs_are('language', 'guest', array ['SELECT']);
|
||||
select table_privs_are('language', 'invoicer', array ['SELECT']);
|
||||
select table_privs_are('language', 'admin', array ['SELECT']);
|
||||
select table_privs_are('language', 'authenticator', array ['SELECT']::text[]);
|
||||
|
||||
select has_column('language', 'lang_tag');
|
||||
select col_is_pk('language', 'lang_tag');
|
||||
select col_type_is('language', 'lang_tag', 'text');
|
||||
select col_not_null('language', 'lang_tag');
|
||||
select col_hasnt_default('language', 'lang_tag');
|
||||
|
||||
select has_column('language', 'name');
|
||||
select col_type_is('language', 'name', 'text');
|
||||
select col_not_null('language', 'name');
|
||||
select col_hasnt_default('language', 'name');
|
||||
|
||||
select has_column('language', 'endonym');
|
||||
select col_type_is('language', 'endonym', 'text');
|
||||
select col_not_null('language', 'endonym');
|
||||
select col_hasnt_default('language', 'endonym');
|
||||
|
||||
select has_column('language', 'selectable');
|
||||
select col_type_is('language', 'selectable', 'boolean');
|
||||
select col_not_null('language', 'selectable');
|
||||
select col_hasnt_default('language', 'selectable');
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -5,7 +5,7 @@ reset client_min_messages;
|
|||
|
||||
begin;
|
||||
|
||||
select plan(44);
|
||||
select plan(51);
|
||||
|
||||
set search_path to auth, public;
|
||||
|
||||
|
@ -44,6 +44,14 @@ select col_type_is('user', 'role', 'name');
|
|||
select col_not_null('user', 'role');
|
||||
select col_hasnt_default('user', 'role');
|
||||
|
||||
select has_column('user', 'lang_tag');
|
||||
select col_is_fk('user', 'lang_tag');
|
||||
select fk_ok('user', 'lang_tag', 'language', 'lang_tag');
|
||||
select col_type_is('user', 'lang_tag', 'text');
|
||||
select col_not_null('user', 'lang_tag');
|
||||
select col_has_default('user', 'lang_tag');
|
||||
select col_default_is('user', 'lang_tag', 'und');
|
||||
|
||||
select has_column('user', 'cookie');
|
||||
select col_type_is('user', 'cookie', 'text');
|
||||
select col_not_null('user', 'cookie');
|
||||
|
|
|
@ -0,0 +1,154 @@
|
|||
-- Test user_profile
|
||||
set client_min_messages to warning;
|
||||
create extension if not exists pgtap;
|
||||
reset client_min_messages;
|
||||
|
||||
begin;
|
||||
|
||||
select plan(47);
|
||||
|
||||
set search_path to numerus, auth, public;
|
||||
|
||||
select has_view('user_profile');
|
||||
select table_privs_are('user_profile', 'guest', array []::text[]);
|
||||
select table_privs_are('user_profile', 'invoicer', array['SELECT']);
|
||||
select table_privs_are('user_profile', 'admin', array['SELECT']);
|
||||
select table_privs_are('user_profile', 'authenticator', array[]::text[]);
|
||||
|
||||
select has_column('user_profile', 'user_id');
|
||||
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', 'invoicer', 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 has_column('user_profile', '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', 'invoicer', 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 has_column('user_profile', 'name');
|
||||
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', 'invoicer', 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 has_column('user_profile', 'role');
|
||||
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', 'invoicer', array['SELECT']);
|
||||
select column_privs_are('user_profile', 'role', 'admin', array['SELECT']);
|
||||
select column_privs_are('user_profile', 'role', 'authenticator', array[]::text[]);
|
||||
|
||||
select has_column('user_profile', 'lang_tag');
|
||||
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', 'invoicer', 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[]);
|
||||
|
||||
|
||||
set client_min_messages to warning;
|
||||
truncate auth."user" cascade;
|
||||
reset client_min_messages;
|
||||
|
||||
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')
|
||||
, (5, 'admin@tandem.blog', 'Admin', 'test', 'admin', '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524', current_timestamp + interval '1 month', 'es')
|
||||
;
|
||||
|
||||
prepare profile as
|
||||
select user_id, email, name, role, lang_tag
|
||||
from user_profile;
|
||||
|
||||
select is_empty( 'profile', 'Should be empty when no user is logger in' );
|
||||
|
||||
select set_cookie( '44facbb30d8a419dfd4bfbc44a4b5539d4970148dfc84bed0e/demo@tandem.blog' );
|
||||
|
||||
select results_eq(
|
||||
'profile',
|
||||
$$ values (1, 'demo@tandem.blog'::email, 'Demo', 'invoicer'::name, 'ca') $$,
|
||||
'Should only see the profile of the first user'
|
||||
);
|
||||
|
||||
select lives_ok( $$
|
||||
update user_profile
|
||||
set email = 'demo+update@tandem.blog'
|
||||
, name = 'Demo Update'
|
||||
, lang_tag = 'es';
|
||||
$$,
|
||||
'Should be able to update the first profile'
|
||||
);
|
||||
|
||||
select throws_ok(
|
||||
$$ update user_profile set user_id = 123 $$,
|
||||
'42501', 'permission denied for view user_profile',
|
||||
'Should not be able to change the ID'
|
||||
);
|
||||
|
||||
select throws_ok(
|
||||
$$ update user_profile set role = 'admin' $$,
|
||||
'42501', 'permission denied for view user_profile',
|
||||
'Should not be able to change the ID'
|
||||
);
|
||||
|
||||
select results_eq(
|
||||
'profile',
|
||||
$$ values (1, 'demo+update@tandem.blog'::email, 'Demo Update', 'invoicer'::name, 'es') $$,
|
||||
'Should see the changed profile of the first user'
|
||||
);
|
||||
|
||||
reset role;
|
||||
|
||||
select set_cookie( '12af4c88b528c2ad4222e3740496ecbc58e76e26f087657524/admin@tandem.blog' );
|
||||
|
||||
select results_eq(
|
||||
'profile',
|
||||
$$ values (5, 'admin@tandem.blog'::email, 'Admin', 'admin'::name, 'es') $$,
|
||||
'Should only see the profile of the second user'
|
||||
);
|
||||
|
||||
select lives_ok( $$
|
||||
update user_profile
|
||||
set email = 'admin+update@tandem.blog'
|
||||
, name = 'Admin Update'
|
||||
, lang_tag = 'ca';
|
||||
$$,
|
||||
'Should be able to update the second profile'
|
||||
);
|
||||
|
||||
select throws_ok(
|
||||
$$ update user_profile set user_id = 123 $$,
|
||||
'42501', 'permission denied for view user_profile',
|
||||
'Should not be able to change the ID'
|
||||
);
|
||||
|
||||
select throws_ok(
|
||||
$$ update user_profile set role = 'invoicer' $$,
|
||||
'42501', 'permission denied for view user_profile',
|
||||
'Should not be able to change the ID'
|
||||
);
|
||||
|
||||
select results_eq(
|
||||
'profile',
|
||||
$$ values (5, 'admin+update@tandem.blog'::email, 'Admin Update', 'admin'::name, 'ca') $$,
|
||||
'Should see the changed profile of the first user'
|
||||
);
|
||||
|
||||
reset role;
|
||||
|
||||
select results_eq(
|
||||
$$ select user_id, email, name, lang_tag from auth."user" order by user_id $$,
|
||||
$$ values (1, 'demo+update@tandem.blog'::email, 'Demo Update', 'es')
|
||||
, (5, 'admin+update@tandem.blog'::email, 'Admin Update', 'ca')
|
||||
$$,
|
||||
'Should have updated the base table’s data'
|
||||
);
|
||||
|
||||
select *
|
||||
from finish();
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,28 @@
|
|||
-- Verify numerus:available_languages on pg
|
||||
|
||||
begin;
|
||||
|
||||
set search_path to public;
|
||||
|
||||
select 1 / count(*)
|
||||
from language
|
||||
where lang_tag = 'und'
|
||||
and name = 'Undefined'
|
||||
and endonym = 'Undefined'
|
||||
and not selectable;
|
||||
|
||||
select 1 / count(*)
|
||||
from language
|
||||
where lang_tag = 'ca'
|
||||
and name = 'Catalan'
|
||||
and endonym = 'català'
|
||||
and selectable;
|
||||
|
||||
select 1 / count(*)
|
||||
from language
|
||||
where lang_tag = 'es'
|
||||
and name = 'Spanish'
|
||||
and endonym = 'español'
|
||||
and selectable;
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,7 @@
|
|||
-- Verify numerus:current_app_user on pg
|
||||
|
||||
begin;
|
||||
|
||||
select has_function_privilege('numerus.current_app_user()', 'execute');
|
||||
|
||||
rollback;
|
|
@ -0,0 +1,12 @@
|
|||
-- Verify numerus:language on pg
|
||||
|
||||
begin;
|
||||
|
||||
select lang_tag
|
||||
, name
|
||||
, endonym
|
||||
, selectable
|
||||
from public.language
|
||||
where false;
|
||||
|
||||
rollback;
|
|
@ -8,6 +8,7 @@ select
|
|||
, name
|
||||
, password
|
||||
, role
|
||||
, lang_tag
|
||||
, cookie
|
||||
, cookie_expires_at
|
||||
, created_at
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
-- Verify numerus:user_profile on pg
|
||||
|
||||
begin;
|
||||
|
||||
select
|
||||
user_id
|
||||
, email
|
||||
, name
|
||||
, role
|
||||
, lang_tag
|
||||
from numerus.user_profile
|
||||
where false;
|
||||
|
||||
rollback;
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 1.1 MiB |
File diff suppressed because it is too large
Load Diff
After Width: | Height: | Size: 877 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
|
@ -10,10 +10,10 @@
|
|||
<header>
|
||||
<h1><img src="/static/numerus.svg" alt="Numerus" width="261" height="33"></h1>
|
||||
<nav role="navigation">
|
||||
<button aria-haspopup="true">
|
||||
<svg role="image" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="50" height="50"><path fill="none" d="M0 0h24v24H0z"/><path d="M9.342 18.782l-1.931-.518.787-2.939a10.988 10.988 0 0 1-3.237-1.872l-2.153 2.154-1.415-1.415 2.154-2.153a10.957 10.957 0 0 1-2.371-5.07l1.968-.359C3.903 10.812 7.579 14 12 14c4.42 0 8.097-3.188 8.856-7.39l1.968.358a10.957 10.957 0 0 1-2.37 5.071l2.153 2.153-1.415 1.415-2.153-2.154a10.988 10.988 0 0 1-3.237 1.872l.787 2.94-1.931.517-.788-2.94a11.072 11.072 0 0 1-3.74 0l-.788 2.94z"/></svg></button>
|
||||
<button aria-haspopup="true"><i class="ri-eye-close-line ri-3x"></i></button>
|
||||
<ul>
|
||||
<li><form method="POST" action="/logout"><button type="submit">{{( pgettext "Logout" "action" )}}</button></form></li>
|
||||
<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>
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
<!doctype html>
|
||||
<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>
|
||||
<form method="POST" action="/profile">
|
||||
<fieldset>
|
||||
<legend>{{( pgettext "User Access Data" "title" )}}</legend>
|
||||
|
||||
<label for="name">{{( pgettext "User name" "input" )}}</label>
|
||||
<input type="text" name="name" id="name" required="required" value="{{ .Name }}">
|
||||
|
||||
<label for="email">{{( pgettext "Email" "input" )}}</label>
|
||||
<input type="email" name="email" id="email" required="required" value="{{ .Email }}">
|
||||
</fieldset>
|
||||
<fieldset>
|
||||
<legend>{{( pgettext "Password Change" "title" )}}</legend>
|
||||
|
||||
<label for="password">{{( pgettext "Password" "input" )}}</label>
|
||||
<input type="password" name="password" id="password" value="{{ .Password }}">
|
||||
|
||||
<label for="password_confirm">{{( pgettext "Password Confirmation" "input" )}}</label>
|
||||
<input type="password" name="password_confirm" id="password_confirm" value="{{ .PasswordConfirm }}">
|
||||
</fieldset>
|
||||
|
||||
<label for="language">{{( pgettext "Language" "input" )}}</label>
|
||||
<select id="language" name="language">
|
||||
<option value="und">{{( pgettext "Automatic" "language option" )}}</option>
|
||||
{{ range $language := .Languages }}
|
||||
<option value="{{ .Tag }}" {{ if eq .Tag $.Language }}selected="selected"{{ end }}>{{ .Name }}</option>
|
||||
{{ end }}
|
||||
</select>
|
||||
<button type="submit">{{( pgettext "Save changes" "action" )}}</button>
|
||||
</form>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|