Commit Graph

132 Commits

Author SHA1 Message Date
jordi fita mas 1456ac5341 Redirect from /login to /admin if user is already logged in 2023-08-06 03:59:52 +02:00
jordi fita mas e128680e9a Split templates and handlers into admin and public
I need to check that the user is an employee (or admin) in
administration handlers, but i do not want to do it for each handler,
because i am bound to forget it.  Thus, i added the /admin sub-path for
these resources.

The public-facing web is the rest of the resources outside /admin, but
for now there is only home, to test whether it works as expected or not.

The public-facing web can not relay on the user’s language settings, as
the guest user has no way to set that.  I would be happy to just use the
Accept-Language header for that, but apparently Google does not use that
header[0], and they give four alternatives: a country-specific domain,
a subdomain with a generic top-level domain (gTLD), subdirectories with
a gTLD, or URL parameters (e.g., site.com?loc=de).

Of the four, Google does not recommend URL parameters, and the customer
is already using subdirectories with the current site, therefor that’s
what i have chosen.

Google also tells me that it is a very good idea to have links between
localized version of the same resources, either with <link> elements,
Link HTTP response headers, or a sitemap file[1]; they are all
equivalent in the eyes of Google.

I have choosen the Link response headers way, because for that i can
simply “augment” ResponseHeader to automatically add these headers when
the response status is 2xx, otherwise i would need to pass down the
original URL path until it reaches the template.

Even though Camper is supposed to be a “generic”, multi-company
application, i think i will stick to the easiest route and write the
templates for just the “first” customer.

[0]: https://developers.google.com/search/docs/specialty/international/managing-multi-regional-sites
[1]: https://developers.google.com/search/docs/specialty/international/localized-versions
2023-08-05 03:42:37 +02:00
jordi fita mas 9349cda5f6 Take into account the confidence of language.Matcher
I had a weird but that sometimes the application would use Spanish as
the default language, even though the user had not passed Spanish as
value of the Accept-Language header and the default is hard-coded to
Catalan.

I learned that language.Matcher **always** returns one of its defined
locales, even if there is no way to match the passed list of languages
to Match.  In that case, it returns a confidence of “No”, meaning that
the match failed.

Usually go the default Catalan locale because most of the time this was
set as the first language to Matcher, but since Go randomizes maps,
there were times that Spanish was first.
2023-08-05 02:08:32 +02:00
jordi fita mas 1b923a9f65 Add add_campsite_type function and call it from Go with a proper form
This form has an “HTML field”, which is just a <textarea> but “improved”
with the use of Automattic’s isolated block editor[0], a repackaged
Gutenberg’s editor playground as full-featured multi-instance editor
that does not require WordPress.

I do not want to use Node to build this huge, over-engineered piece of …
software. Therefore, i downloaded the released “browser” package, and
added the required React bundle, like i do with HTMx.  This will hold
until i need a new custom block type; let’s hope i will not need it.

[0]: https://github.com/Automattic/isolated-block-editor
2023-08-04 19:59:58 +02:00
jordi fita mas 3994d77a78 Remove campsite_type’s booking_terms and rules
We are no longer going to “stitch up” a whole page based on ACF-like
field, but write the whole page with that information in description.

That change is due to wanting to serve the public-facing part of the web
with the same application.
2023-08-04 19:50:27 +02:00
jordi fita mas 0d2812acc5 Use HTTP Host to establish the request’s company
We made the decision that this application will also serve the public
pages to guests and customers, to avoid the overhead of having to
synchronize all data between this application and a bespoke WordPress
plugin.

That means that i no longer can have a /company/slug in the URL to know
which company the request is for, not only because it looks ugly but
because guest users do not have a “main company”—or any company
whatsoever.

Since the public-facing web is going to be served through a valid DNS
domain, and all companies are going to have a different domain, i
realized this is enough: i only had to add a relation of company and
their hosts.  The same company can have many hosts for staging servers
or to separate the administration and public parts, for instance.

With change, the company is already known from the first handler, and
can pass it down to all the others, not only the handlers under
/company/slug/whatever.  And i no longer need the companyURL function,
as there is no more explicit company in the URL.

Even though template technically does not need the template, as it only
contains the ID —the rest of the data is in a relation inaccessible to
guests for now—, but i left the parameter just in case later on i need
the decimal digits or currency symbol for whatever reason.
2023-08-03 20:21:21 +02:00
jordi fita mas f65110824e Add the company’s slug in the URL before company-dependent handlers
I really doubt that they are going to use more than a single company,
but the application is based on Numerus, that **does** have multiple
company, and followed the same architecture and philosophy: use the URL
to choose the company to manage, even if the user has a single company.

The reason i use the slug instead of the ID is because i do not want to
make the ID public in case the application is really used by employees
of many unrelated companies: they need not need to guess how many
companies there are based on the ID.

I validate this slug to be a valid UUID instead of relaying on the
query’s empty result because casting a string with a malformed value to
UUID results in an error other than data not found.  Not with that
select, but it would fail with a function parameter, and i want to add
that UUID check to all functions that do use slugs.

I based uuid.Valid function on Parse() from Google’s uuid package[0]
instead of using regular expression, as it was my first idea, because
that function is an order of magnitude faster in benchmarks:

  goos: linux
  goarch: amd64
  pkg: dev.tandem.ws/tandem/numerus/pkg
  cpu: Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz
  BenchmarkValidUuid-4            36946050                29.37 ns/op
  BenchmarkValidUuid_Re-4          3633169               306.70 ns/op

The regular expression used for the benchmark was:

  var re = regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")

And the input parameter for both functions was the following valid UUID,
because most of the time the passed UUID will be valid:

  "f47ac10b-58cc-0372-8567-0e02b2c3d479"

I did not use the uuid package as is, even though it is in Debian’s
repository, because i only need to check whether the value is valid,
not convert it to a byte array.  As far as i know, that package can not
do that.

Adding the Company struct into auth was not my intention, as it makes
little sense name-wise, but i need to have the Company when rendering
templates and the company package has templates to render, thus using
the company package for the Company struct would create a dependency
loop between template and company.  I’ve chosen the auth package only
because User is also there; User and Company are very much related in
this application, but not enough to include the company inside the user,
or vice versa, as the User comes from the cookie while the company from
the URL.

Finally, had to move methodNotAllowed to the http package, as an
exported function, because it is used now from other packages, namely
campsite.

[0]: https://github.com/google/uuid
2023-07-31 18:51:50 +02:00
jordi fita mas 5a2c8fea41 Use a constant slug for the demo company
Otherwise, we would not have persistent URLs when discussing issues.
2023-07-31 18:21:29 +02:00
jordi fita mas c33c6896d3 Add correct dependencies for camper-sqitch 2023-07-31 16:20:13 +02:00
jordi fita mas cfa330bce4 Add the mock-up form for new campsite type
It does nothing, but i need it to discuss with Oriol.

Now there are more than a single package that requires shiftPath, so
i moved it to http and an exported function, ShiftPath.

Part of #25.
2023-07-31 14:23:23 +02:00
jordi fita mas ab0cfb4100 Add the campsite_type relation
Part of #25.
2023-07-31 14:23:15 +02:00
jordi fita mas 4bf8b0bf0d Add company and company_user relations, along with their dependencies 2023-07-29 04:25:56 +02:00
jordi fita mas b03ef35a95 Allow users to update their profile images
I do not see the profile image as an “integral part” of the user (i.e.,
no need for constraints), hence i do not want to store it in the
database, as would do for the identification image during check-in.

By default, i store the avatars in /var/lib/camper/avatars, but it is a
variable to allow packagers change this value using the linker.

This is also served as a test bed for uploading files to the server,
that now has a better interface and uses less resources that what i did
to Numerus.

Now the profile handler needs to keep a variable to know the path to the
avatars’ directory, thus i had to change it to a struct nested in app,
much like the fileHandler does.  It still has to return the HandlerFunc,
however, as this function needs to close over the user and connection
variables.

Part of #7.
2023-07-28 20:15:09 +02:00
jordi fita mas bf0c86a796 Fix the login of the demo users 2023-07-27 20:35:34 +02:00
jordi fita mas 5c4ade15bc Add Debian package
Although it is way too soon to install the application on any server,
i use build.opensuse.org as a kind of CI/CD server: it runs the
migration and executes the test suite on each commit.  For that, i need
to build *some* package, and Debian suits be better because it has all
the Go packages i use; i would have to create the RPM for many libraries
if i were to use openSUSE, for instance.

According to the de facto project layout for Go[0], these files should
go into a `build/package` folder, but since i already broke the rules
with Sqitch’s folders, i do not see why i have to go against Debian’s
conventions of placing them into a `debian` subfolder of the root.

I have spit the package into the binary and the Sqitch migration files
because it is possible to want the PostgreSQL into a separate server,
and there is little point of having Sqitch and all its dependencies
installed on the front-end server where the Go program runs.

The demo package is probably harder to justify, as it is just a single
file, however i will not run out of packages, will i?

Lintian detects htmx@1.9.3.min.js as a “source-less” file, which is
practically true as nobody is ever going to edit a minified source.  I
did not want to include the source in the distribution package, that’s
why i included it in the “missing sources” file, even thought this is a
native debian package and, thus, can not have missing sources.

git-buildpackage creates a lot of extra files that have to be removed
to build it again, otherwise the process detects the new files in the
directory and refuses to build the tarball.  I was getting tired of
doing it manually and added a Makefile rule.

Closes #20
2023-07-27 20:00:08 +02:00
jordi fita mas 64d38f515f Add the SQL with demo data
Serves to prime up the database for development and for testing the
application on the staging server.
2023-07-27 18:52:01 +02:00
jordi fita mas f963f54839 Add profile form, inside a user menu
This is the first form that uses HTMx, and can not return a 400 error
code because otherwise HTMx does not render the content.

Since now there are pages that do not render the whole html, with header
and body, i need a different layout for these, and moved the common code
to handle forms and such a new template file that both layouts can use.
I also need the request in template.MustRender to check which layout i
should use.

Closes #7.
2023-07-26 20:46:09 +02:00
jordi fita mas 341520def3 Add favicon
Closes #8.
2023-07-26 18:59:17 +02:00
jordi fita mas cab949f8a6 Show request errors as a toast
Or at least they will once we add the required CSS, but there is at
least the JavaScript behaviour in place now.
2023-07-26 18:52:32 +02:00
jordi fita mas b0317f1051 Log the whole request path
Next handlers might cut up request’s URL.Path, so i need to keep a copy
before calling next to log the whole thing.
2023-07-26 13:50:55 +02:00
jordi fita mas ebe8217862 Add the logout button
Conceptually, to logout we have to “delete the session”, thus the best
HTTP verb would be `DELETE`.  However, there is no way to send a
`DELETE` request with a regular HTML form, and it seems that never will
be[0].

I could use a POST, optionally with a “method override” technique, but
i was planing to use HTMx anyway, so this was as good an opportunity to
include it as any.

In this application i am not concerned with people not having JavaScript
enabled, because it is for a customer that has a known environment, and
we do not have much time anyway.  Therefore, i opted to forgo
progressive enhancement in cases like this: if `DELETE` is needed, use
`hx-delete`.

Unfortunately, i can not use a <form> with a hidden <input> for the
CSRF token, because `DELETE` requests do not have body and the value
should be added as query parameters, like a form with GET method, but
HTMx does the incorrect thing here: sends the values in the request’s
body.  That’s why i have to use a custom header and the `hx-header`
directive to include the CSRF token.

Then, by default HTMx targets the triggered element for swap with the
response from the server, but after a logout i want to redirect the
user to the login form again.  I could set the hx-target to button to
replace the whole body, or tell the client to redirect to the new
location.  I actually do not know which one is “better”.  Maybe the
hx-target is best because then everything is handled by the client, but
in the case of logout, since it is possible that i might want to load
scripts only for logged-in users in the future, i opted for the full
page reload.

However, HTMx does not want to reload a page that return HTTP 401,
hence i had to include the GET method to /login in order to return the
login form with a response of HTTP 200, which also helps when
reloading in the browser after a failed login attempt.  I am not worried
with the HTTP 401 when attempting to load a page as guest, because
this request most probably comes from the browser, not HTMx, and it will
show the login form as intended—even though it is not compliant, since
it does not return the WWW-Authenticate header, but this is the best i
can do given that no cookie-based authentication method has been
accepted[1].

[0]: https://www.w3.org/Bugs/Public/show_bug.cgi?id=10671#c16
[1]: https://datatracker.ietf.org/doc/id/draft-broyer-http-cookie-auth-00.html
2023-07-26 13:49:47 +02:00
jordi fita mas 2f3fc8812d Include the locale inside the User struct
The locale is completely dependent on the user, as much as its email or
CSRF token, so it does not make much sense to have it in a separate
variable: for different users we might have to use different locales.
Also, this means one less variable to pass to handlers, that most of
them will need the user at some point or another (i.e., to render its
profile icon).

The thing is that i can not import `app.User` from the template package
because it would create an import cycle. I created the `auth` package
just because of that.

I thought that the login code would be better moved to the auth package
as well, but of course then i would reintroduce the import cycle between
auth and template.
2023-07-26 12:08:59 +02:00
jordi fita mas 1ef6dcc4cf Get user from database based on cookie and serve login if not logged in
To get the user from the database i have to set the cookie first, that
was already done in database.MustAcquire, but i thought they were too
far apart, even thought they are so related.  So, the cookie, and thus
the role, is set when getting the user, that is actually the first thing
to do once the connection is acquired.  However, that way the database
package has no knowledge of cookies, and the code that sets the cookie
and retrieves the user are next to each other.

I applied the same logic to the changes of locale.Match: it has not
business knowing that the accept language string comes from a request;
it only needs the actual string.  Also, the TODO comment about getting
the user’s locale made no sense, now, because app already knows that
locale, so there is no need to pass the user to the locale package.

Getting the locale is done after retrieving the user from the database,
for the same reason the connection is Acquired as far up as possible:
almost every request will need this value, together with the user and
the database connection.

I am a bit affraid that i will end up with functions that always expect
these three values.  Maybe i can put the locale inside user, as it is
the user’s locale, after all, no matter if it came from the database or
the user agent, but connection and user must be separate, i think.

We’ll see.
2023-07-26 01:50:39 +02:00
jordi fita mas 9fccd5f81d Acquire the database connection in App.ServeHTTP with cookie set
Almost all request will need the database connection, either because
they will perform queries or because they need to check if the user is
logged in, for instance, so it should be acquired as far above as
possible to avoid acquiring multiple connections.

This is specially true since i have to pass the cookie to the database
to switch role and set request.user.email and request.user.cookie
config variables.  I do not want to do that many times per request.
2023-07-26 00:48:58 +02:00
jordi fita mas 01526bff1a Convert the login variables to a struct with parsing and validation
It is a lot of code having to check the login variables inside the POST
handler, and i could not mark each input field individually as invalid
because the generic errors array i was using did no identify which field
had the error.

Thus, i use more or less the same technique as with Numerus: a struct
with the value and the error message.  This time the input field does
not have the label and extra attributes because i believe this belongs
to the template: if i want do reuse the same form template, i should
create a common template rather than defining everything in Go.

The name is a bit different, however, because it has meaning both to the
front and back ends, as it needs to be exactly the same.  Writing it
twice is error-prone, as with a rename i could easily forget to change
one or the other, and here i see value in having that in Go, because
it is also used in code.
2023-07-24 17:17:15 +02:00
jordi fita mas 7b1e17569e Add Gettext and GettextNoop to locale
xgettext does not recognize Get as marker for translatable strings, and
i can not add it as is because Go has many functions called Get, and it
would mark many strings as translatable that should not be, like HTTP
headers.

I believe that Gettext is unusual in Go, because the “correct” way to
spell that function in Go would be GetText, and would not interfere in
any other function.

GettextNoop is just a function that marks the string as translatable but
does not translate it at all.  I want this for error messages and such,
that i have to pass the string to a validator function, but it is only
necessary to translate it when the validation fails.  I doubt it makes
a difference in peformance, but still.
2023-07-24 17:09:43 +02:00
jordi fita mas 2300735030 Handle the login form
I now actually handle the /login URL and check whether the email and
password are valid, creating the session cookie if correct, but doing
nothing else with that cookie, for now.

The validation is done by hand for now, because i do not yet how i will
actually do it without so much duplication.
2023-07-23 20:49:26 +02:00
jordi fita mas 25bf186a83 Replace type="text/css" with media="screen"
The type is actually no necessary for rel="stylesheet", and this
stylesheet is only meant for screen interfaces, not the default “all”.
2023-07-23 20:44:06 +02:00
jordi fita mas 403c27f1e1 Add the skeleton of the web application
It does nothing more than to server a single page that does nothing
interesting.

This time i do not use a router.  Instead, i am trying out a technique
i have seen in an article[0] that i have tried in other, smaller,
projects and seems to work surprisingly well: it just “cuts off” the
URI path by path, passing the request from handler to handler until
it finds its way to a handler that actually serves the request.

That helps to loosen the coupling between the application and lower
handlers, and makes dependencies explicit, because i need to pass the
locale, company, etc. down instead of storing them in contexts.  Let’s
see if i do not regret it on a later date.

I also made a lot more packages that in Numerus.  In Numerus i actually
only have the single pkg package, and it works, kind of, but i notice
how i name my methods to avoid clashing instead of using packages for
that.  That is, instead of pkg.NewApp i now have app.New.

Initially i thought that Locale should be inside app, but then there was
a circular dependency between app and template.  That is why i created a
separate package, but now i am wondering if template should be inside
app too, but then i would have app.MustRenderTemplate instead of
template.MustRender.

The CSS is the most bare-bones file i could write because i am focusing
in markup right now; Oriol will fill in the file once the application is
working.

[0]: https://blog.merovius.de/posts/2017-06-18-how-not-to-use-an-http-router/
2023-07-23 00:11:00 +02:00
jordi fita mas 53347b0e80 Add Makefile to deploy with Sqitch and test with pg_prove
This is mainly intended for CI scripts, when building the distribution
package.
2023-07-22 23:56:16 +02:00
jordi fita mas 5d0861824a Add authentication relations, views, and functions for PostgreSQL
Most of them are exactly the same as we use for Numerus, but with the
main application schema changed to camper.

Closes #1
2023-07-22 01:59:12 +02:00
jordi fita mas 7171dee23e Add empty Sqitch configuration anq plan files 2023-07-20 23:43:23 +02:00