aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorSamuel Johnson <[email protected]>2025-11-24 13:53:18 -0500
committerSamuel Johnson <[email protected]>2025-11-24 13:53:18 -0500
commit368a462bc744d8e9084eacfaddeb9afcaf7f7133 (patch)
treec6e8f665d6cb9713b9226b10c4a341e60b8e91c2
parent4d4419f51557bef6b64dca8635ed61616d262a9b (diff)
Add session management
-rw-r--r--cmd/parser/main.go37
-rw-r--r--cmd/web/handlers/blog.go9
-rw-r--r--cmd/web/handlers/login.go170
-rw-r--r--cmd/web/handlers/routes.go13
-rw-r--r--cmd/web/main.go3
-rw-r--r--cmd/web/middleware/auth.go81
-rw-r--r--go.mod1
-rw-r--r--go.sum2
-rw-r--r--internal/dbmigrations.go51
-rw-r--r--internal/models/user.go9
-rw-r--r--static/app.css45
-rw-r--r--static/music_player.js12
-rw-r--r--ui/html/base.tmpl.html10
-rw-r--r--ui/html/login.tmpl.html19
-rw-r--r--ui/html/pages/index.tmpl.html21
15 files changed, 432 insertions, 51 deletions
diff --git a/cmd/parser/main.go b/cmd/parser/main.go
index 6d3c3c4..3d0b132 100644
--- a/cmd/parser/main.go
+++ b/cmd/parser/main.go
@@ -16,7 +16,7 @@ import (
"github.com/yuin/goldmark/extension"
"github.com/yuin/goldmark/parser"
"github.com/yuin/goldmark/renderer/html"
- "golang.org/x/crypto/bcrypt"
+ "paterissa.net/mblog/internal"
_ "github.com/jackc/pgx/v5/stdlib"
)
@@ -121,40 +121,7 @@ func main() {
}
defer db.Close()
- _, table_check := db.Query("SELECT * FROM posts;")
- if table_check != nil {
- _, err = db.Exec("CREATE TABLE posts (id SERIAL PRIMARY KEY, name VARCHAR(50) NOT NULL, time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, content TEXT NOT NULL);")
- if err != nil {
- fmt.Fprintf(os.Stderr, "Unable to create posts table: %v\n", err)
- os.Exit(1)
- }
- }
-
- webmaster := os.Getenv("webmaster")
- passOne := os.Getenv("blog_pass1")
- passTwo := os.Getenv("blog_pass2")
-
- _, table_check = db.Query("SELECT * FROM logins;")
- if table_check != nil {
- _, err = db.Exec("CREATE TABLE logins (id SERIAL PRIMARY KEY, name VARCHAR(50) NOT NULL, time DATE DEFAULT CURRENT_DATE, pass_one TEXT NOT NULL, pass_two TEXT NOT NULL);")
- if err != nil {
- fmt.Fprintf(os.Stderr, "Unable to create logins table: %v\n", err)
- os.Exit(1)
- }
-
- hashOne, errHashOne := bcrypt.GenerateFromPassword([]byte(passOne), 12)
- hashTwo, errHashTwo := bcrypt.GenerateFromPassword([]byte(passTwo), 12)
- if errHashOne != nil || errHashTwo != nil {
- fmt.Fprintf(os.Stderr, "Failed to hash password")
- os.Exit(1)
- }
-
- _, err = db.Exec(fmt.Sprintf("INSERT INTO logins (name, pass_one, pass_two) VALUES ('%s', '%s', '%s');", webmaster, hashOne, hashTwo))
- if err != nil {
- fmt.Fprintf(os.Stderr, "Unable to add webmaster to logins table: %v\n", err)
- os.Exit(1)
- }
- }
+ internal.Migrate(db, os.Getenv("webmaster"), os.Getenv("blog_pass1"), os.Getenv("blog_pass2"))
router := http.NewServeMux()
router.HandleFunc("/", mdServe)
diff --git a/cmd/web/handlers/blog.go b/cmd/web/handlers/blog.go
index bd2d97f..573e52f 100644
--- a/cmd/web/handlers/blog.go
+++ b/cmd/web/handlers/blog.go
@@ -15,22 +15,29 @@ type blogContext struct {
Rows []models.Post
Name string
+ IsAuth bool
}
func (ctx *blogContext) index(w http.ResponseWriter, r *http.Request) {
ctx.Rows = []models.Post{}
+ ctx.IsAuth = false
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
+
+ _, err := r.Cookie("paterissa_session_token")
+ if err == nil {
+ ctx.IsAuth = true;
+ }
offset := r.URL.Query().Get("offset")
if offset == "" {
offset = "20"
}
- rows, err := ctx.db.Query("SELECT * FROM posts WHERE id < " + offset + " ORDER BY id DESC LIMIT 20;")
+ rows, err := ctx.db.Query("SELECT p.id, u.name, p.time, p.content FROM posts p INNER JOIN logins u ON p.user_id = u.id WHERE p.id < " + offset + " ORDER BY p.id DESC LIMIT 20;")
if err != nil {
ctx.err.Print(err.Error())
http.Error(w, "Internal Server Error", 500)
diff --git a/cmd/web/handlers/login.go b/cmd/web/handlers/login.go
new file mode 100644
index 0000000..e867e07
--- /dev/null
+++ b/cmd/web/handlers/login.go
@@ -0,0 +1,170 @@
+package handlers
+
+import (
+ "database/sql"
+ "html/template"
+ "log"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/google/uuid"
+ "golang.org/x/crypto/bcrypt"
+ "paterissa.net/mblog/internal/models"
+)
+
+type loginContext struct {
+ err *log.Logger
+ db *sql.DB
+}
+
+func (ctx *loginContext) index(w http.ResponseWriter, r *http.Request) {
+ files := []string{
+ "ui/html/base.tmpl.html",
+ "ui/html/login.tmpl.html",
+ "ui/html/music_player.tmpl.html",
+ }
+
+ compiled, err := template.ParseFiles(files...)
+ if err != nil {
+ w.WriteHeader(500)
+ w.Write([]byte("Internal Server Error"))
+ ctx.err.Printf("Failed to parse templates: %v\n", err)
+ return
+ }
+
+ err = compiled.ExecuteTemplate(w, "base", ctx)
+ if err != nil {
+ w.WriteHeader(500)
+ w.Write([]byte("Internal Server Error"))
+ ctx.err.Printf("Failed to parse templates: %v\n", err)
+ return
+ }
+
+ return
+}
+
+func (ctx *loginContext) login(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ w.WriteHeader(405)
+ w.Write([]byte("Method Not Allowed"))
+ return
+ }
+
+ err := r.ParseForm()
+ if err != nil {
+ w.WriteHeader(500)
+ w.Write([]byte("Internal Error"))
+ ctx.err.Printf("Failed to retrieve form data: %v\n", err)
+ return
+ }
+
+ user := r.PostForm.Get("user")
+ passOne := r.PostForm.Get("pass_one")
+ passTwo := r.PostForm.Get("pass_two")
+
+ stmt, err := ctx.db.Prepare("SELECT * FROM logins WHERE name = $1;")
+ if err != nil {
+ w.WriteHeader(500)
+ w.Write([]byte("Internal Error"))
+ ctx.err.Printf("Failed to retrieve form data: %v\n", err)
+ return
+ }
+ defer stmt.Close()
+
+ var u models.User
+
+ row := stmt.QueryRow(user)
+ err = row.Scan(&u.Id, &u.Name, &u.Time, &u.PassOne, &u.PassTwo)
+ if err != nil {
+ w.WriteHeader(401)
+ w.Write([]byte("Unauthorized"))
+ ctx.err.Printf("Failed to retrieve user info from DB: %v\n", err)
+ return
+ }
+
+ passOneErr := bcrypt.CompareHashAndPassword([]byte(u.PassOne), []byte(passOne))
+ passTwoErr := bcrypt.CompareHashAndPassword([]byte(u.PassTwo), []byte(passTwo))
+ if passOneErr != nil || passTwoErr != nil {
+ w.WriteHeader(401)
+ w.Write([]byte("Failed to login - not authorized"))
+ return
+ }
+
+ cookie := http.Cookie{
+ Name: "paterissa_session_token",
+ Value: uuid.New().String(),
+ Expires: time.Now().AddDate(0, 0, 1),
+ Path: "/",
+ Domain: os.Getenv("serv"),
+ HttpOnly: true,
+ Secure: true,
+ }
+
+ commit, err := ctx.db.Prepare("INSERT INTO cookies (content, user_id, expiration) VALUES ($1, $2, $3);")
+ if err != nil {
+ w.WriteHeader(500)
+ w.Write([]byte("Internal Error"))
+ ctx.err.Printf("Failed to prepare DB statement: %v\n", err)
+ return
+ }
+
+ _, err = commit.Exec(cookie.Value, u.Id, cookie.Expires)
+ if err != nil {
+ w.WriteHeader(500)
+ w.Write([]byte("Internal Error"))
+ ctx.err.Printf("Failed to prepare DB statement: %v\n", err)
+ }
+
+ http.SetCookie(w, &cookie)
+ http.Redirect(w, r, "/", http.StatusFound)
+ return
+}
+
+func (ctx *loginContext) logout(w http.ResponseWriter, r *http.Request) {
+ cookie, err := r.Cookie("paterissa_session_token")
+ if err != nil {
+ w.WriteHeader(405)
+ w.Write([]byte("Unauthorized"))
+ return
+ }
+
+ stmt, err := ctx.db.Prepare("UPDATE cookies SET expiration = $1 WHERE content = $2;")
+ if err != nil {
+ w.WriteHeader(500)
+ w.Write([]byte("Internal Error"))
+ ctx.err.Printf("Could not prepare DB statement: %v\n", err)
+ return
+ }
+ defer stmt.Close()
+
+ _, err = stmt.Exec(time.Now(), cookie.Value)
+ if err != nil {
+ w.WriteHeader(500)
+ w.Write([]byte("Internal Error"))
+ ctx.err.Printf("Could not execute DB statement: %v\n", err)
+ return
+ }
+
+ cookie = &http.Cookie{
+ Name: "paterissa_session_token",
+ Value: "",
+ Path: "/",
+ MaxAge: -1,
+ HttpOnly: true,
+ }
+
+ http.SetCookie(w, cookie)
+ http.Redirect(w, r, "/", http.StatusFound)
+ return
+}
+
+func (ctx *loginContext) handle(w http.ResponseWriter, r *http.Request) {
+ if r.Method != "POST" {
+ ctx.index(w, r)
+ return
+ } else {
+ ctx.login(w, r)
+ return
+ }
+}
diff --git a/cmd/web/handlers/routes.go b/cmd/web/handlers/routes.go
index e9fd0f5..0196331 100644
--- a/cmd/web/handlers/routes.go
+++ b/cmd/web/handlers/routes.go
@@ -4,15 +4,25 @@ import (
"database/sql"
"net/http"
+ "paterissa.net/mblog/cmd/web/middleware"
"paterissa.net/mblog/cmd/web/types"
)
func RegisterEndpoints(app types.Application, db *sql.DB) *http.ServeMux {
+ auth := middleware.AuthMiddleware{
+ Err: app.Err,
+ Db: db,
+ }
+
blog := blogContext{
err: app.Err,
db: db,
Name: app.Env.Webmaster,
}
+ login := loginContext{
+ err: app.Err,
+ db: db,
+ }
audio := fsContext{
err: app.Err,
path: app.AudioDir,
@@ -26,6 +36,9 @@ func RegisterEndpoints(app types.Application, db *sql.DB) *http.ServeMux {
blogRouter := http.NewServeMux()
blogRouter.HandleFunc("/", blog.index)
+ blogRouter.HandleFunc("/login", login.handle)
+ blogRouter.HandleFunc("/logout", auth.Resolve(login.logout))
+
blogRouter.HandleFunc("/audio", audio.readdir)
blogRouter.HandleFunc("/audio/get", audio.get)
diff --git a/cmd/web/main.go b/cmd/web/main.go
index 42b672a..0b35ff2 100644
--- a/cmd/web/main.go
+++ b/cmd/web/main.go
@@ -14,6 +14,7 @@ import (
"paterissa.net/mblog/cmd/web/handlers"
"paterissa.net/mblog/cmd/web/types"
+ "paterissa.net/mblog/internal"
)
func main() {
@@ -57,6 +58,8 @@ func main() {
}
defer db.Close()
+ internal.Migrate(db, os.Getenv("webmaster"), os.Getenv("blog_pass1"), os.Getenv("blog_pass2"))
+
router := handlers.RegisterEndpoints(app, db)
srv := &http.Server{
Addr: fmt.Sprintf(":%d", app.Env.AppPort),
diff --git a/cmd/web/middleware/auth.go b/cmd/web/middleware/auth.go
new file mode 100644
index 0000000..b53980a
--- /dev/null
+++ b/cmd/web/middleware/auth.go
@@ -0,0 +1,81 @@
+package middleware
+
+import (
+ "database/sql"
+ "log"
+ "net/http"
+ "time"
+)
+
+type AuthMiddleware struct {
+ Err *log.Logger
+ Db *sql.DB
+}
+
+func (auth *AuthMiddleware) Resolve(next http.HandlerFunc) http.HandlerFunc {
+ return http.HandlerFunc(
+ func (w http.ResponseWriter, r *http.Request) {
+ cookie, err := r.Cookie("paterissa_session_token")
+ if err != nil {
+ w.WriteHeader(401)
+ w.Write([]byte("Unauthorized"))
+ return
+ }
+
+ stmt, err := auth.Db.Prepare("SELECT * FROM cookies WHERE content = $1;")
+ if err != nil {
+ cookie = &http.Cookie{
+ Name: "paterissa_session_token",
+ Value: "",
+ Path: "/",
+ MaxAge: -1,
+ HttpOnly: true,
+ }
+ http.SetCookie(w, cookie)
+
+ w.Write([]byte("Unauthorized"))
+ auth.Err.Printf("Could not retrieve cookie from DB: %v\n", err)
+ return
+ }
+ defer stmt.Close()
+
+ var id int
+ var content string
+ var userId int
+ var expiration time.Time
+
+ row := stmt.QueryRow(cookie.Value)
+ err = row.Scan(&id, &content, &userId, &expiration)
+ if err != nil {
+ cookie = &http.Cookie{
+ Name: "paterissa_session_token",
+ Value: "",
+ Path: "/",
+ MaxAge: -1,
+ HttpOnly: true,
+ }
+ http.SetCookie(w, cookie)
+
+ w.Write([]byte("Unauthorized"))
+ auth.Err.Printf("Could not retrieve cookie from DB: %v\n", err)
+ return
+ }
+
+ if time.Now().After(expiration) {
+ cookie = &http.Cookie{
+ Name: "paterissa_session_token",
+ Value: "",
+ Path: "/",
+ MaxAge: -1,
+ HttpOnly: true,
+ }
+ http.SetCookie(w, cookie)
+
+ w.Write([]byte("Expired"))
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ return
+ })
+}
diff --git a/go.mod b/go.mod
index 82a8fd3..f6a9fb6 100644
--- a/go.mod
+++ b/go.mod
@@ -8,6 +8,7 @@ require (
)
require (
+ github.com/google/uuid v1.6.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.7.6 // indirect
diff --git a/go.sum b/go.sum
index 1a6445d..6c14cd2 100644
--- a/go.sum
+++ b/go.sum
@@ -1,4 +1,6 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
diff --git a/internal/dbmigrations.go b/internal/dbmigrations.go
new file mode 100644
index 0000000..e6c11ff
--- /dev/null
+++ b/internal/dbmigrations.go
@@ -0,0 +1,51 @@
+package internal
+
+import (
+ "database/sql"
+ "fmt"
+ "os"
+
+ "golang.org/x/crypto/bcrypt"
+)
+
+func Migrate (db *sql.DB, webmaster string, passOne string, passTwo string) {
+ _, table_check := db.Query("SELECT * FROM logins;")
+ if table_check != nil {
+ _, err := db.Exec("CREATE TABLE logins (id SERIAL PRIMARY KEY, name VARCHAR(50) NOT NULL, time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, pass_one TEXT NOT NULL, pass_two TEXT NOT NULL);")
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Unable to create logins table: %v\n", err)
+ os.Exit(1)
+ }
+
+ hashOne, errHashOne := bcrypt.GenerateFromPassword([]byte(passOne), 12)
+ hashTwo, errHashTwo := bcrypt.GenerateFromPassword([]byte(passTwo), 12)
+ if errHashOne != nil || errHashTwo != nil {
+ fmt.Fprintf(os.Stderr, "Failed to hash password")
+ os.Exit(1)
+ }
+
+ _, err = db.Exec(fmt.Sprintf("INSERT INTO logins (name, pass_one, pass_two) VALUES ('%s', '%s', '%s');", webmaster, hashOne, hashTwo))
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Unable to add webmaster to logins table: %v\n", err)
+ os.Exit(1)
+ }
+ }
+
+ _, table_check = db.Query("SELECT * FROM posts;")
+ if table_check != nil {
+ _, err := db.Exec("CREATE TABLE posts (id SERIAL PRIMARY KEY, user_id INTEGER REFERENCES logins(id), time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, content TEXT NOT NULL);")
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Unable to create posts table: %v\n", err)
+ os.Exit(1)
+ }
+ }
+
+
+ _, table_check = db.Query("SELECT * FROM cookies;")
+ if table_check != nil {
+ _, err := db.Exec("CREATE TABLE cookies (id SERIAL PRIMARY KEY, content VARCHAR(255) NOT NULL, user_id INTEGER REFERENCES logins(id), expiration TIMESTAMP);")
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Unable to create cookies table: %v\n", err)
+ }
+ }
+}
diff --git a/internal/models/user.go b/internal/models/user.go
new file mode 100644
index 0000000..8753810
--- /dev/null
+++ b/internal/models/user.go
@@ -0,0 +1,9 @@
+package models
+
+type User struct {
+ Id int
+ Name string
+ Time string
+ PassOne string
+ PassTwo string
+}
diff --git a/static/app.css b/static/app.css
index 0148d24..7b2d083 100644
--- a/static/app.css
+++ b/static/app.css
@@ -8,6 +8,7 @@ header {
header > h1 {
padding: 0.5rem;
+ font-size: 2rem;
}
header > nav {
@@ -16,8 +17,27 @@ header > nav {
margin-left: 2em;
}
-a {
+.flex-between {
+ display: flex;
+ flex-direction: row;
+}
+
+.spacer {
+ visibility: hidden;
+ flex-grow: 1;
+}
+
+.nav_tag {
margin: 0.5em;
+}
+
+.right {
+ float: right;
+ margin: auto;
+ margin-right: 2em;
+}
+
+a {
color: inherit;
position: relative;
display: inline-block;
@@ -50,7 +70,7 @@ a:hover::after {
.pane {
margin: auto;
- padding: 4px 10px;
+ padding: 0.2em 0.5em;
width: 75%;
background-color: black;
@@ -66,6 +86,7 @@ a:hover::after {
flex-direction: row;
justify-content: center;
align-content: center;
+ margin: 2em;
}
.topline > p {
@@ -74,6 +95,16 @@ a:hover::after {
margin-left: 2em;
}
+@media only screen and (max-width: 768px) {
+ header > h1 {
+ font-size: 1em;
+ }
+
+ .topline > p {
+ display: none;
+ }
+}
+
.card {
border-top: 1px solid #fff;
@@ -126,6 +157,16 @@ a:hover::after {
right: 0;
}
+.margin_2rem {
+ margin: 2rem;
+}
+
+.login_form {
+ margin-top: 2rem;
+ margin-bottom: 2rem;
+ padding: 0.5rem;
+}
+
.vert_align {
display: inline-block;
vertical-align: middle;
diff --git a/static/music_player.js b/static/music_player.js
index 8fe89c2..33063d1 100644
--- a/static/music_player.js
+++ b/static/music_player.js
@@ -45,7 +45,7 @@ function mpSeek () {
audio.currentTime = goal;
}
-async function mpUpdate () {
+async function mpInit () {
clearInterval(tick);
const trackSelector = document.getElementById("mp_tracks");
@@ -61,6 +61,10 @@ async function mpUpdate () {
ppButton.innerHTML = "<img src='/static/get?file=images/play.svg' alt='Play'>";
tick = setInterval(mpTick, 1000);
+}
+
+async function mpUpdate () {
+ mpInit();
mpPpTrack();
}
@@ -90,7 +94,6 @@ function mpNext () {
}
mpUpdate();
- mpPpTrack();
}
function mpPrevious () {
@@ -103,7 +106,6 @@ function mpPrevious () {
}
mpUpdate();
- mpPpTrack();
}
const trackList = await mpFetchAsync("/audio");
@@ -113,7 +115,7 @@ trackList.forEach((track) => {
let option = document.createElement("option");
option.text = track;
- trackSelector.add(option, 0);
+ trackSelector.append(option)
});
document.getElementById("mp_tracks").addEventListener("change", mpUpdate, false);
@@ -122,4 +124,4 @@ document.getElementById("mp_pp_track").addEventListener("click", mpPpTrack, fals
document.getElementById("mp_prev_track").addEventListener("click", mpPrevious, false);
document.getElementById("mp_next_track").addEventListener("click", mpNext, false);
-mpUpdate();
+mpInit();
diff --git a/ui/html/base.tmpl.html b/ui/html/base.tmpl.html
index dda949b..014868e 100644
--- a/ui/html/base.tmpl.html
+++ b/ui/html/base.tmpl.html
@@ -3,7 +3,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta property="og:site_name" content="paterissa" />
<meta property="og:type" content="object" />
<meta property="og:title" content="Paterissa - thehinterlander's blog" />
@@ -16,11 +16,11 @@
<img src="/static/get?file=images/bg.png" id="bg-image">
<div class="pane">
<header>
- <h1>Paterissa</h1>
+ <h1><a href="https://paterissa.net">Paterissa</a></h1>
<nav>
- <a href="https://git.paterissa.net">Git</a>
- <a href="https://starless.paterissa.net">Starless</a>
- <a href="https://charts.paterissa.net">Parliament Builder</a>
+ <a href="https://git.paterissa.net" class="nav_tag">Git</a>
+ <a href="https://starless.paterissa.net" class="nav_tag">Starless</a>
+ <a href="https://charts.paterissa.net" class="nav_tag">Parliament Builder</a>
</nav>
</header>
<hr>
diff --git a/ui/html/login.tmpl.html b/ui/html/login.tmpl.html
new file mode 100644
index 0000000..6377dc0
--- /dev/null
+++ b/ui/html/login.tmpl.html
@@ -0,0 +1,19 @@
+{{define "title"}}Login{{end}}
+
+{{define "main"}}
+ <div class="topline">
+ <h2>Log In</h2>
+ </div>
+ <div class="card">
+ <form class="login_form center" action="/login" method="POST">
+ <label for="user">Username:</label>
+ <input type="text" id="user" name="user">
+ <label for="pass_one">Password:</label>
+ <input type="text" id="pass_one" name="pass_one">
+ <label for="pass_two">Password:</label>
+ <input type="text" id="pass_two" name="pass_two">
+ <br>
+ <input type="submit" value="Submit">
+ </form>
+ </div>
+{{end}}
diff --git a/ui/html/pages/index.tmpl.html b/ui/html/pages/index.tmpl.html
index 78c2bb2..011f964 100644
--- a/ui/html/pages/index.tmpl.html
+++ b/ui/html/pages/index.tmpl.html
@@ -1,10 +1,25 @@
{{define "title"}}Blog{{end}}
{{define "main"}}
- <div class="topline">
- <h2>Blog</h2>
- <p>Home of {{.Name}}</p>
+ <div class="flex-between">
+ <div class="topline">
+ <h2>Blog</h2>
+ <p>Home of {{.Name}}</p>
+ </div>
+ <div class="spacer"></div>
+ {{if .IsAuth}}
+ <a href="/logout" class="nav_tag right">Logout</a>
+ {{else}}
+ <a href="/login" class="nav_tag right">Login</a>
+ {{end}}
</div>
+ {{if .IsAuth}}
+ <div class="card">
+ <div class="header">
+ <h4><b>New</b></h4>
+ </div>
+ </div>
+ {{end}}
{{range .Rows}}
<div class="card">
<div class="header">