cmd
: command main packagesfreenote
: freenote CLI toolfreenoted
: freenote server
config
: configurationids
: ID helper functionsnotes
: note model and handlingpage
: pagination model and handlingrest
: REST API handler and helpersstats
: stats measurement for expvarstore
: backing store handlersusers
: user account model and handlingweb
: UI content files (HTML/CSS/JS)
Becuase the REST API is pretty straightforward, we don't use any third-party mux; in fact, we don't use any mux at all. We just use a cascading handler, wherein /users goes to the users collection handler; that looks for an ID, and if it finds one, passes control to the user handler; that looks for one of the user subroutes, and if it finds one, passes control to the appropriate handler; and so forth. The mux is entirely hard-coded, and therefore highly efficient. Were the API to get much more complicated, we'd likely want to move to a proper mux, as performance is not top priority for this server.
ServeHTTP
handles requests to begin with, establishing DB connectivity,
authenticating the user, authorizing the route, then routing the request. It
bundles up the request details into a requestContext
instance, which just
holds request-specific variables to avoid unnecessary re-parsing or excessive
argument counts in the handler methods. This is used in favor of Request.Context
for type safety.
auth.go contains handlers for authentication, authorization, and session handling. The only session data used is a session token to maintain authentication for web clients.
contenttype.go contains handlers for parsing requests of arbitrary content types and marshalling responses in arbitrary content types, based on the Content-Type and Accept headers, respectively.
hypermedia.go handles decorating response objects with link collections prior to marshaling, and includes helpers for generating the most common link relations.
notes.go and users.go contain helpers for decorating users and notes, respectively.
debug.go contains a stub handler for /debug
that always returns 404 and only
runs if neither the debug
nor dev
build tags are supplied. If either is
supplied, debug_dev.go will run instead, which routes handlers for expvar and
pprof.
Only two types of data are in the backing store, users and notes. These can be stored in the same database, or different databases. A backing store driver must fulfull the interfaces defined in store.go.
There are currently two backing stores implemented, an embedded database using BoltDB via Storm, and an external database using MongoDB. Further stores are planned for future versions.
Authentication is handled by the Password
type. Rather than including credentials
directly in the User
, all the details of password management are segregated out.
The Password
struct contains the password version, salt, and hash. The version
indicates what hash/salt implementation was used, allowing the security to be
updated at will while maintaining backward compatibility.
Authorization is handled by the simple AccessLevel enumeration, with a series of users levels, each with greater access than the one below it, allowing for access control by greater-than/less-than comparison against the constants. This is used primarily for route pattern authorization, and for administrative access (i.e. the ability to manage data owned by another user).
Lastly there is a recovery mode, whereby the system can generate a random password to an account with full administrative access, which is available for a limited time after the application starts. The password is printed in the log. This can be used for new installations (when no other administrative users exist yet) or for system recovery, when no administrative users are able to log in.
The web UI is a static HTML/CSS/JS site which interacts with the REST API. The
content files are embedded using github.com/aprice/embed
, and served directly by
the application. Given the dev
build tag, static files will be served from disk,
assuming the working directory is the github.com/aprice/freenote
root.
The UI uses no external libraries, frameworks, or polyfills. Built-in JavaScript functionality in modern browsers is not overly complex and provides all the needed functionality for the UI.
With all modern browsers auto-updating, we're free to target relatively recent versions of all mainstream browsers on desktop and mobile. Modern browsers are getting much better at standards compliance and cross-browser compatibility, which means we get the advantage of modern features. So the basic rule of thumb for any core functionality is any feature must be supported by Firefox, Chrome, and Safari versions at least 6 months old, without vendor prefixes. Any less broadly supported feature must degrade completely transparently when not available.
The UI design is fully responsive and adaptive based on state (identified by classes) and media. It's progressive, supporting offline access from IndexedDB and Web Cache, and uses a Service Worker to manage content caching.
The UI is relatively slim, coming in at under 250kB before minification and compression (under 200kB after), allowing for load times on the order of 500ms.
The UI is comprised of a few main components:
window.App
is the main application manager, which handles showing messages, errors, and confirmation panels, as well as the application startup process. It is defined inapp.js
.window.User
handles authentication and user (password) editing. It is defined inauth.js
.window.NoteList
maintains the note list, selecting notes, and creating notes. It is defined innotes.js
.window.NoteEditor
maints the note editor, saving notes, and deleting notes. It is defined innotes.js
.window.DataStore
is an opaque reference toIndexedDBDataStore
, orRESTDataStore
if IndexedDB is not available on the client. It is set indb.js
.IndexedDBDataStore
is a note store that acts as a proxy toRESTDataStore
, using IndexedDB for caching and offline access. It is defined indb.js
.RESTDataStore
is a note store backed by the REST API. It is defined indb.js
.- A service worker is defined in
worker.js
, responsible for caching static content for offline use.
utils.js
includes a handful of helper and utility functions, primarily
shortcuts to frequently-used calls.
The application starts with a call to App.init()
from the window's onload
handler. This then calls User.init()
, NoteList.init()
, and
NoteEditor.init()
. User.init()
loads the current user from localStorage
if available, and either way, refreshes the current session from the server.
NoteList.init()
establishes handlers and loads the note list from the data
store. NoteEditor.init()
just establishes event handlers.
When a note or note list is retrieved from the data store, assuming IndexedDB
is available, it first pulls from the local database to serve the request
quickly. It then makes a REST API call in the background, and when that call
returns, it fires a notefetched
(if a single note was retrieved in full) or
summaryfetched
(if a list of notes were retrieved with summaries) event on
the window; these events are handled by NoteList
and NoteEditor
and used to
refresh the display when fresh data is received from the server.
When a note is saved, it is put in IndexedDB with a pending
flag; if it is a
new note, it is also given a temporary ID. When a note is deleted, it is updated
in IndexedDB with a delete
flag. At regular intervals, the local database is
synced with the remote database, by:
- Sending any
pending
notes - Deleting any
delete
notes - Retrieving any notes with a modification time newer than the most recent currently in the local database
This allows us to continue operating while offline and sync up when connectivity is restored. It also allows for immediate responsiveness to user actions.