From bd4a475923a6a311e4e313fc269505b170e87e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Magnus=20=C3=85hall?= Date: Thu, 28 Nov 2024 18:11:14 +0100 Subject: [PATCH] wip --- authentication/pkg.go | 33 +++- file.go | 17 ++ main.go | 97 +++++++--- node.go | 111 +++++++++++ page.go | 34 ++++ sql/00002.sql | 46 +++++ static/css/login.css | 37 ++++ static/css/main.css | 2 + static/css/theme.css | 0 static/images/design.svg | 377 ++++++++++++++++++++++++++++++++++++ static/images/logo-orig.svg | 318 ++++++++++++++++-------------- static/images/logo.svg | 75 +++++++ static/js/api.mjs | 67 +++++++ static/js/app.mjs | 24 ++- static/less/login.less | 44 +++++ static/less/loop_make.sh | 16 +- static/less/main.less | 4 + static/less/theme.less | 3 + user.go | 24 +++ views/layouts/main.gotmpl | 8 +- views/pages/index.gotmpl | 3 - views/pages/login.gotmpl | 47 +++++ views/pages/notes2.gotmpl | 22 +++ 23 files changed, 1217 insertions(+), 192 deletions(-) create mode 100644 file.go create mode 100644 node.go create mode 100644 page.go create mode 100644 sql/00002.sql create mode 100644 static/css/login.css create mode 100644 static/css/theme.css create mode 100644 static/images/design.svg create mode 100644 static/images/logo.svg create mode 100644 static/js/api.mjs create mode 100644 static/less/login.less create mode 100644 static/less/theme.less create mode 100644 user.go delete mode 100644 views/pages/index.gotmpl create mode 100644 views/pages/login.gotmpl create mode 100644 views/pages/notes2.gotmpl diff --git a/authentication/pkg.go b/authentication/pkg.go index 915fd5b..2806114 100644 --- a/authentication/pkg.go +++ b/authentication/pkg.go @@ -51,6 +51,25 @@ func NewManager(db *sqlx.DB, log *slog.Logger, secret string, expireDays int) (m mngr.ExpireDays = expireDays return } // }}} +func validateTokenTimestamps(claims jwt.MapClaims) error { // {{{ + now := time.Now() + + if issuedAt, ok := claims["iat"].(float64); ok { + if now.Unix() < int64(issuedAt) { + return errors.New("Token is not valid yet") + } + } else { + return errors.New("Token is missing iat") + } + + if expires, ok := claims["exp"].(float64); ok { + if now.Unix() > int64(expires) { + return errors.New("Token has expired") + } + } + + return nil +} // }}} func (mngr *Manager) GenerateToken(data map[string]any) (string, error) { // {{{ // Create a new token object, specifying signing method and the claims @@ -64,7 +83,7 @@ func (mngr *Manager) GenerateToken(data map[string]any) (string, error) { // {{{ // Sign and get the complete encoded token as a string using the secret. return token.SignedString(mngr.secret) } // }}} -func (mngr *Manager) VerifyToken(tokenString string) (jwt.Claims, error) { // {{{ +func (mngr *Manager) VerifyToken(tokenString string) (jwt.MapClaims, error) { // {{{ // Parse takes the token string and a function for looking up the key. The latter is especially // useful if you use multiple keys for your application. The standard is to use 'kid' in the // head of the token to identify which key to use, but the parsed token (head and claims) is provided @@ -79,10 +98,15 @@ func (mngr *Manager) VerifyToken(tokenString string) (jwt.Claims, error) { // {{ return mngr.secret, nil }) if err != nil { - mngr.log.Error("jwt", "error", err) + mngr.log.Error("authentication", "error", err) + return nil, err } if claims, ok := token.Claims.(jwt.MapClaims); ok { + err = validateTokenTimestamps(claims) + if err != nil { + return nil, err + } return claims, nil } else { return nil, err @@ -97,6 +121,7 @@ func (mngr *Manager) AuthenticationHandler(w http.ResponseWriter, r *http.Reques body, _ := io.ReadAll(r.Body) err := json.Unmarshal(body, &request) if err != nil { + mngr.log.Debug("authentication", "error", err) httpError(w, err) return } @@ -104,11 +129,13 @@ func (mngr *Manager) AuthenticationHandler(w http.ResponseWriter, r *http.Reques // Verify username and password against the db user table. authenticated, user, err := mngr.Authenticate(request.Username, request.Password) if err != nil { + mngr.log.Error("authentication", "error", err) httpError(w, err) return } if !authenticated { + mngr.log.Info("authentication", "username", request.Username, "status", "failed") httpError(w, errors.New("Authentication failed")) return } @@ -121,10 +148,12 @@ func (mngr *Manager) AuthenticationHandler(w http.ResponseWriter, r *http.Reques data["name"] = user.Name token, err = mngr.GenerateToken(data) if err != nil { + mngr.log.Error("authentication", "error", err) httpError(w, err) return } + mngr.log.Info("authentication", "username", request.Username, "status", "accepted") j, _ := json.Marshal(struct { OK bool User User diff --git a/file.go b/file.go new file mode 100644 index 0000000..c5bca8c --- /dev/null +++ b/file.go @@ -0,0 +1,17 @@ +package main + +import ( + // Standard + "time" +) + +type File struct { + ID int + UserID int `db:"user_id"` + NodeID int `db:"node_id"` + Filename string + Size int64 + MIME string + MD5 string + Uploaded time.Time +} diff --git a/main.go b/main.go index 8ee157e..4fc380e 100644 --- a/main.go +++ b/main.go @@ -1,8 +1,6 @@ package main import ( - // External - // Internal "notes2/authentication" "notes2/html_template" @@ -10,17 +8,21 @@ import ( // Standard "bufio" + "context" "embed" + "encoding/json" "flag" "fmt" "log/slog" "net/http" "path" + "regexp" "strings" "text/template" ) const VERSION = "v1" +const CONTEXT_USER = 1 var ( FlagDev bool @@ -31,6 +33,7 @@ var ( config Config Log *slog.Logger AuthManager authentication.Manager + RxpBearerToken *regexp.Regexp //go:embed views ViewFS embed.FS @@ -55,6 +58,8 @@ func init() { // {{{ flag.StringVar(&FlagCreateUser, "create-user", "", "Username for creating a new user") flag.StringVar(&FlagChangePassword, "change-password", "", "Change the password for the given username") flag.Parse() + + RxpBearerToken = regexp.MustCompile("(?i)^\\s*Bearer\\s+(.*?)\\s*$") } // }}} func initLog() { // {{{ opts := slog.HandlerOptions{} @@ -91,7 +96,6 @@ func main() { // {{{ return } - // The webengine takes layouts, pages and components and renders them into HTML. Webengine, err = HTMLTemplate.NewEngine(ViewFS, StaticFS, FlagDev) if err != nil { @@ -99,23 +103,67 @@ func main() { // {{{ } http.HandleFunc("/", rootHandler) + http.HandleFunc("/notes2", pageNotes2) http.HandleFunc("/login", pageLogin) - http.HandleFunc("/authenticate", AuthManager.AuthenticationHandler) + + http.HandleFunc("/user/authenticate", AuthManager.AuthenticationHandler) + + http.HandleFunc("/node/tree", authenticated(actionNodeTree)) + http.HandleFunc("/service_worker.js", pageServiceWorker) + listen := fmt.Sprintf("%s:%d", config.Network.Address, config.Network.Port) + Log.Info("webserver", "listen_address", listen) http.ListenAndServe(listen, nil) } // }}} +func authenticated(fn func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { // {{{ + return func(w http.ResponseWriter, r *http.Request) { + failed := func(err error) { + j, _ := json.Marshal(struct { + OK bool + Error string + AuthFailed bool + }{false, err.Error(), true}) + w.Write(j) + } + + // The Bearer token is extracted. + authHeader := r.Header.Get("Authorization") + authParts := RxpBearerToken.FindStringSubmatch(authHeader) + if len(authParts) != 2 { + failed(fmt.Errorf("Authorization missing or invalid")) + return + } + token := authParts[1] + + // Token signature is verified with the application secret key. + claims, err := AuthManager.VerifyToken(token) + if err != nil { + failed(err) + return + } + + // User object is added to the context for the next handler. + user := NewUser(claims) + r = r.WithContext(context.WithValue(r.Context(), CONTEXT_USER, user)) + + Log.Info("webserver", "op", "request", "method", r.Method, "url", r.URL.String(), "username", user.Username) + fn(w, r) + } +} // }}} + func rootHandler(w http.ResponseWriter, r *http.Request) { // {{{ // All URLs not specifically handled are routed to this function. // Everything going here should be a static resource. - if r.URL.Path != "/" { - Webengine.StaticResource(w, r) + if r.URL.Path == "/" { + http.Redirect(w, r, "/notes2", http.StatusSeeOther) return } - pageIndex(w, r) + Webengine.StaticResource(w, r) } // }}} + func pageServiceWorker(w http.ResponseWriter, r *http.Request) { // {{{ w.Header().Add("Content-Type", "text/javascript; charset=utf-8") @@ -137,20 +185,6 @@ func pageServiceWorker(w http.ResponseWriter, r *http.Request) { // {{{ return } } // }}} - -func pageIndex(w http.ResponseWriter, r *http.Request) { // {{{ - page := HTMLTemplate.SimplePage{ - Layout: "main", - Page: "index", - Version: VERSION, - } - - err := Webengine.Render(page, w, r) - if err != nil { - w.Write([]byte(err.Error())) - return - } -} // }}} func pageLogin(w http.ResponseWriter, r *http.Request) { // {{{ page := HTMLTemplate.SimplePage{ Layout: "main", @@ -164,14 +198,27 @@ func pageLogin(w http.ResponseWriter, r *http.Request) { // {{{ return } } // }}} +func pageNotes2(w http.ResponseWriter, r *http.Request) { // {{{ + page := NewPage("notes2") -func sessionFilter(fn func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { // {{{ - return func(w http.ResponseWriter, r *http.Request) { - fmt.Printf("Filtered ") - fn(w, r) + err := Webengine.Render(page, w, r) + if err != nil { + w.Write([]byte(err.Error())) + return } } // }}} +func actionNodeTree(w http.ResponseWriter, r *http.Request) { // {{{ + user, _ := r.Context().Value(CONTEXT_USER).(User) + + j, _ := json.Marshal(struct { + OK bool + Foo string + User User + }{true, "FOO", user}) + w.Write(j) +} // }}} + func createNewUser(username string) { // {{{ reader := bufio.NewReader(os.Stdin) diff --git a/node.go b/node.go new file mode 100644 index 0000000..e1e815c --- /dev/null +++ b/node.go @@ -0,0 +1,111 @@ +package main + +import ( + // External + "github.com/jmoiron/sqlx" + + // Standard + "time" +) + +type ChecklistItem struct { + ID int + GroupID int `db:"checklist_group_id"` + Order int + Label string + Checked bool +} + +type ChecklistGroup struct { + ID int + NodeID int `db:"node_id"` + Order int + Label string + Items []ChecklistItem +} + +type Node struct { + ID int + UserID int `db:"user_id"` + ParentID int `db:"parent_id"` + CryptoKeyID int `db:"crypto_key_id"` + Name string + Content string + Updated time.Time + Children []Node + Crumbs []Node + Files []File + Complete bool + Level int + + ChecklistGroups []ChecklistGroup + + ContentEncrypted string `db:"content_encrypted" json:"-"` + Markdown bool +} + +func NodeTree(userID, startNodeID int) (nodes []Node, err error) { // {{{ + var rows *sqlx.Rows + rows, err = db.Queryx(` + WITH RECURSIVE nodetree AS ( + SELECT + *, + array[name::text] AS path, + 0 AS level + FROM node + WHERE + user_id = $1 AND + CASE $2::int + WHEN 0 THEN parent_id IS NULL + ELSE parent_id = $2 + END + + UNION ALL + + SELECT + n.*, + path||n.name::text AS path, + nt.level + 1 AS level + FROM node n + INNER JOIN nodetree nt ON n.parent_id = nt.id + ) + + SELECT + id, + user_id, + COALESCE(parent_id, 0) AS parent_id, + name, + updated, + level + FROM nodetree + ORDER BY + path ASC + `, + userID, + startNodeID, + ) + if err != nil { + return + } + defer rows.Close() + + type resultRow struct { + Node + Level int + } + + nodes = []Node{} + for rows.Next() { + node := Node{} + node.Complete = false + node.Crumbs = []Node{} + node.Children = []Node{} + node.Files = []File{} + if err = rows.StructScan(&node); err != nil { + return + } + nodes = append(nodes, node) + } + + return +} // }}} diff --git a/page.go b/page.go new file mode 100644 index 0000000..ee5c255 --- /dev/null +++ b/page.go @@ -0,0 +1,34 @@ +package main + +import ( + // Internal + "notes2/html_template" +) + +type Page struct { + HTMLTemplate.SimplePage + Data map[string]any +} + +func NewPage(page string) (p Page) { + p.Page = page + p.Data = make(map[string]any) + return +} + +func (p Page) GetVersion() string { + return VERSION +} + +func (p Page) GetLayout() string { + if p.Layout == "" { + return "main" + } + return p.Layout +} + +func (p Page) GetData() any { + p.Data["_dev"] = FlagDev + p.Data["GRIS"] = "foo" + return p.Data +} diff --git a/sql/00002.sql b/sql/00002.sql new file mode 100644 index 0000000..cf148a7 --- /dev/null +++ b/sql/00002.sql @@ -0,0 +1,46 @@ +CREATE EXTENSION pg_trgm; + +CREATE TABLE public.crypto_key ( + id serial4 NOT NULL, + user_id int4 NOT NULL, + description varchar(255) DEFAULT ''::character varying NOT NULL, + "key" bpchar(144) NOT NULL, + CONSTRAINT crypto_key_pk PRIMARY KEY (id), + CONSTRAINT crypto_user_description_un UNIQUE (user_id, description), + CONSTRAINT crypto_key_user_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE RESTRICT ON UPDATE RESTRICT +); + +CREATE TABLE public.node ( + id serial4 NOT NULL, + user_id int4 NOT NULL, + parent_id int4 NULL, + "name" varchar(256) DEFAULT ''::character varying NOT NULL, + "content" text DEFAULT ''::text NOT NULL, + updated timestamptz DEFAULT now() NOT NULL, + crypto_key_id int4 NULL, + content_encrypted text DEFAULT ''::text NOT NULL, + markdown bool DEFAULT false NOT NULL, + CONSTRAINT name_length CHECK ((length(TRIM(BOTH FROM name)) > 0)), + CONSTRAINT node_pk PRIMARY KEY (id), + CONSTRAINT crypto_key_fk FOREIGN KEY (crypto_key_id) REFERENCES public.crypto_key(id) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT node_fk FOREIGN KEY (parent_id) REFERENCES public.node(id) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT node_user_fk FOREIGN KEY (user_id) REFERENCES public."user"(id) ON DELETE RESTRICT ON UPDATE RESTRICT +); +CREATE INDEX node_search_index ON public.node USING gin (name gin_trgm_ops, content gin_trgm_ops); + + +CREATE OR REPLACE FUNCTION node_update_timestamp() +RETURNS TRIGGER +LANGUAGE PLPGSQL +AS $$ +BEGIN + IF NEW.updated = OLD.updated THEN + UPDATE node SET updated = NOW() WHERE id=NEW.id; + END IF; + RETURN NEW; +END; +$$; + +CREATE OR REPLACE TRIGGER node_update AFTER UPDATE ON node +FOR EACH ROW +EXECUTE PROCEDURE node_update_timestamp() diff --git a/static/css/login.css b/static/css/login.css new file mode 100644 index 0000000..88a9140 --- /dev/null +++ b/static/css/login.css @@ -0,0 +1,37 @@ +#app { + display: grid; + justify-items: center; + margin-top: 128px; +} +#logo { + margin-bottom: 48px; +} +#box { + display: grid; + grid-gap: 16px 0; + justify-items: center; + width: 300px; + padding: 48px 0px; + background-color: #fff; + box-shadow: 0px 20px 52px -33px rgba(0, 0, 0, 0.75); + border-left: 8px solid #666; +} +#box input { + padding: 4px 8px; + font-size: 1em; + width: calc(100% - 64px); + border: 1px solid #aaa; + border-radius: 4px; +} +#box button { + padding: 6px 16px; + font-size: 1em; + border-radius: 4px; + border: none; + background-color: #fe5f55; + color: #fff; +} +#box #error { + color: #c33; + margin-top: 16px; +} diff --git a/static/css/main.css b/static/css/main.css index 1888e5b..05539e5 100644 --- a/static/css/main.css +++ b/static/css/main.css @@ -1,5 +1,7 @@ html { box-sizing: border-box; + background: #efede8; + font-size: 14pt; } *, *:before, diff --git a/static/css/theme.css b/static/css/theme.css new file mode 100644 index 0000000..e69de29 diff --git a/static/images/design.svg b/static/images/design.svg new file mode 100644 index 0000000..4fe5ef9 --- /dev/null +++ b/static/images/design.svg @@ -0,0 +1,377 @@ + + + +image/svg+xmlKBKunderNätverk GustafsbergStartArbete diff --git a/static/images/logo-orig.svg b/static/images/logo-orig.svg index 94d53ac..c014403 100644 --- a/static/images/logo-orig.svg +++ b/static/images/logo-orig.svg @@ -7,7 +7,7 @@ viewBox="0 0 210 297" version="1.1" id="svg1" - inkscape:version="1.3.2 (091e20e, 2023-11-25)" + inkscape:version="1.3.2 (1:1.3.2+202311252150+091e20ef0f)" sodipodi:docname="logo-orig.svg" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" @@ -15,24 +15,26 @@ xmlns:svg="http://www.w3.org/2000/svg"> + inkscape:zoom="1" + inkscape:cx="491.5" + inkscape:cy="542" + inkscape:window-width="2190" + inkscape:window-height="1404" + inkscape:window-x="1463" + inkscape:window-y="16" + inkscape:window-maximized="0" + inkscape:current-layer="layer1" + showgrid="false" + showborder="false"> + id="defs1"> + + + + + style="color:#000000;overflow:visible;mix-blend-mode:normal;fill:#020000;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:0.767057;paint-order:markers stroke fill;filter:url(#filter4)" + id="rect4" + width="55.739491" + height="51.341488" + x="53.515308" + y="30.794798" + transform="matrix(1.2491076,0,0,2.3812885,-6.9841439,-25.028378)" /> - - - - Notes - 2 - + x="54.010353" + y="23.504112" /> + x="40.694885" + y="224.53336" /> + x="14.955561" + y="224.53336" /> - - + x="67.040604" + y="224.53336" /> + + id="g17" + transform="translate(5.3143021)"> + Notes + 2 + + + + + Username + + + + Password + + + + Login + + - - - Notes - 2 - - diff --git a/static/images/logo.svg b/static/images/logo.svg new file mode 100644 index 0000000..3b5efa4 --- /dev/null +++ b/static/images/logo.svg @@ -0,0 +1,75 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + diff --git a/static/js/api.mjs b/static/js/api.mjs new file mode 100644 index 0000000..3fff10a --- /dev/null +++ b/static/js/api.mjs @@ -0,0 +1,67 @@ +export class API { + // query resolves into the JSON data produced by the application, or an exception with 'type' and 'error' properties. + static async query(method, path, request) { + return new Promise((resolve, reject) => { + const body = JSON.stringify(request) + const headers = {} + + // Authentication is done with a bearer token. + // Here provided to the backend if set. + const token = localStorage.getItem('token') + if (token) { + headers.Authorization = `Bearer ${token}` + } + + fetch(path, { method, headers, body }) + .then(response => { + // An HTTP communication level error occured. + if (!response.ok || response.status != 200) + return reject({ + type: 'http', + error: response, + }) + return response.json() + }) + .then(json => { + // Application level response are handled here. + if (!json.OK) + return reject({ + type: 'application', + error: json.Error, + application: json, + }) + resolve(json) + }) + .catch(err => + // Catch any other errors from fetch. + reject({ + type: 'http', + error: err, + })) + }) + } + + static hasAuthenticationToken() {//{{{ + const token = localStorage.getItem('token') + return token !== null && token !== '' + }//}}} + static authenticate(username, password) {//{{{ + return new Promise((resolve, reject) => { + const req = { username, password } + API.query('POST', '/user/authenticate', req) + .then(response => { + localStorage.setItem('token', response.Token) + localStorage.setItem('user', JSON.stringify(response.User)) + resolve(response.User) + }) + .catch(e => { + console.log(e.type, e.error) + reject(e.error) + }) + }) + }//}}} + static logout() {//{{{ + localStorage.removeItem('token') + location.href = '/' + }//}}} +} diff --git a/static/js/app.mjs b/static/js/app.mjs index 4694c89..1e4cb22 100644 --- a/static/js/app.mjs +++ b/static/js/app.mjs @@ -1 +1,23 @@ -console.log('app.mjs') +import { h, Component, createRef } from 'preact' +import htm from 'htm' +import { API } from 'api' +const html = htm.bind(h) + +export class Notes2 { + constructor() {//{{{ + }//}}} + render() {//{{{ + return html` + + ` + }//}}} + + treeGet() { + const req = {} + API.query('POST', '/tree/get', req) + .then(response => { + console.log(response) + }) + .catch(e => console.log(e.type, e.error)) + } +} diff --git a/static/less/login.less b/static/less/login.less new file mode 100644 index 0000000..a07aec4 --- /dev/null +++ b/static/less/login.less @@ -0,0 +1,44 @@ +@import "theme.less"; + +#app { + display: grid; + justify-items: center; + margin-top: 128px; +} + +#logo { + margin-bottom: 48px; +} + +#box { + display: grid; + grid-gap: 16px 0; + justify-items: center; + width: 300px; + padding: 48px 0px; + background-color: #fff; + box-shadow: 0px 20px 52px -33px rgba(0,0,0,0.75); + border-left: 8px solid @color3; + + input { + padding: 4px 8px; + font-size: 1em; + width: calc(100% - 64px); + border: 1px solid #aaa; + border-radius: 4px; + } + + button { + padding: 6px 16px; + font-size: 1em; + border-radius: 4px; + border: none; + background-color: @color1; + color: #fff; + } + + #error { + color: #c33; + margin-top: 16px; + } +} diff --git a/static/less/loop_make.sh b/static/less/loop_make.sh index e6cabf6..5acd94b 100755 --- a/static/less/loop_make.sh +++ b/static/less/loop_make.sh @@ -6,14 +6,10 @@ do inotifywait -q -e MODIFY *less #sleep 0.5 clear - - for THEME in $(ls theme-*.less | sed -e 's/^theme-\(.*\)\.less$/\1/'); do - export THEME=$THEME - make -j12 - #curl -s http://notes.lan:1371/_ws/css_update - done - echo -e "\n\e[32;1mOK!\e[0m" - sleep 1 - clear - + if make -j12; then + echo -e "\n\e[32;1mOK!\e[0m" + curl -s http://notes.lan:1371/css_updated + sleep 1 + clear + fi done diff --git a/static/less/main.less b/static/less/main.less index af533a1..d2b9e2c 100644 --- a/static/less/main.less +++ b/static/less/main.less @@ -1,5 +1,9 @@ +@import "theme.less"; + html { box-sizing: border-box; + background: @color2; + font-size: 14pt; } *, diff --git a/static/less/theme.less b/static/less/theme.less new file mode 100644 index 0000000..255a49c --- /dev/null +++ b/static/less/theme.less @@ -0,0 +1,3 @@ +@color1: #fe5f55; +@color2: #efede8; +@color3: #666; diff --git a/user.go b/user.go new file mode 100644 index 0000000..fcd1cb9 --- /dev/null +++ b/user.go @@ -0,0 +1,24 @@ +package main + +import ( + // External + "github.com/golang-jwt/jwt/v5" +) + +type User struct { + ID int + Username string + Password string + Name string +} + +func NewUser(claims jwt.MapClaims) (u User) { + uid, _ := claims["uid"].(float64) + name, _ := claims["name"].(string) + username, _ := claims["login"].(string) + + u.ID = int(uid) + u.Username = username + u.Name = name + return +} diff --git a/views/layouts/main.gotmpl b/views/layouts/main.gotmpl index d38e88f..d1ca4d7 100644 --- a/views/layouts/main.gotmpl +++ b/views/layouts/main.gotmpl @@ -10,11 +10,9 @@ if (navigator.serviceWorker) navigator.serviceWorker.register('/service_worker.js') - diff --git a/views/pages/index.gotmpl b/views/pages/index.gotmpl deleted file mode 100644 index b31a9be..0000000 --- a/views/pages/index.gotmpl +++ /dev/null @@ -1,3 +0,0 @@ -{{ define "page" }} - -{{ end }} diff --git a/views/pages/login.gotmpl b/views/pages/login.gotmpl new file mode 100644 index 0000000..3f4406e --- /dev/null +++ b/views/pages/login.gotmpl @@ -0,0 +1,47 @@ +{{ define "page" }} + +
+ + + + +
+
+ + +{{ end }} diff --git a/views/pages/notes2.gotmpl b/views/pages/notes2.gotmpl new file mode 100644 index 0000000..bcbde8c --- /dev/null +++ b/views/pages/notes2.gotmpl @@ -0,0 +1,22 @@ +{{ define "page" }} + + + +{{ end }}