Progress on item sharing

This commit is contained in:
roberto 2025-04-16 18:04:28 +02:00
parent 2c9b2d4eb7
commit 77663735a4
10 changed files with 212 additions and 31 deletions

160
assets/static/css/guest.css Normal file
View file

@ -0,0 +1,160 @@
/* Global variables. */
:root,
::backdrop {
/* Set sans-serif & mono fonts */
--sans-font: -apple-system, BlinkMacSystemFont, "Avenir Next", Avenir,
"Nimbus Sans L", Roboto, "Noto Sans", "Segoe UI", Arial, Helvetica,
"Helvetica Neue", sans-serif;
--mono-font: Consolas, Menlo, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
--standard-border-radius: 5px;
/* Default (light) theme */
--bg: #fff;
--primary-color: #2b5797;
}
/* Reset */
/* Box sizing rules */
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Prevent font size inflation */
html {
-moz-text-size-adjust: none;
-webkit-text-size-adjust: none;
text-size-adjust: none;
}
/* Remove default margin in favour of better control in authored CSS */
body, h1, h2, h3, h4, p,
figure, blockquote, dl, dd {
margin-block-end: 0;
}
/* Remove list styles on ul, ol elements with a list role, which suggests default styling will be removed */
ul[role='list'],
ol[role='list'] {
list-style: none;
}
/* Set core body defaults */
body {
margin: 0;
min-height: 100vh;
line-height: 1.5;
}
/* Set shorter line heights on headings and interactive elements */
h1, h2, h3, h4,
input, label {
line-height: 1.1;
}
/* Balance text wrapping on headings */
h1, h2,
h3, h4 {
text-wrap: balance;
}
/* A elements that don't have a class get default styles */
a:not([class]) {
text-decoration-skip-ink: auto;
color: currentColor;
}
/* Inherit fonts for inputs and buttons */
input, button,
textarea, select {
font-family: inherit;
font-size: inherit;
}
/* Anything that has been anchored to should have extra scroll margin */
:target {
scroll-margin-block: 5ex;
}
h1,h2,h3,h4,h5,h6, form {
margin: 0;
padding: 0;
}
a {
text-decoration: none;
color: var(--primary-color);
}
/* End reset */
html {
font-family: var(--sans-font), sans-serif;
scroll-behavior: smooth;
overflow-x: hidden;
box-sizing: border-box;
}
.logo {
width: 32px;
float: left;
padding-right: 10px;
}
#page-top-bar {
display: flex;
align-items: center;
z-index: 1;
height: 50px;
background-color: var(--primary-color);
position: fixed;
width: 100%;
}
#app-title {
height: 50px;
background-color: var(--primary-color);
color: #ffffff;
font-size: 22px;
font-weight: bold;
padding-left: 8px;
display: flex;
align-items: center;
}
#app-title h5 {
color: #ffffff;
font-size: 22px;
font-weight: bold;
}
#page-title {
color: #ffffff;
font-size: 20px;
}
#page-content {
padding-top: 60px;
padding-left: 4px;
padding-right: 4px;
}
.item-detail dt {
font-weight: bold;
}
.show-large {
display: none;
}
/* Large */
@media (min-width:992px) {
.show-large {
display: inline !important;
}
#app-title {
width: 200px;
}
}

View file

@ -104,11 +104,6 @@ textarea, select {
font-size: inherit;
}
/* Make sure textareas without a rows attribute are not tiny */
/*textarea:not([rows]) {
min-height: 10em;
}*/
/* Anything that has been anchored to should have extra scroll margin */
:target {
scroll-margin-block: 5ex;

View file

@ -6,7 +6,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="manifest" href="/static/manifest.json">
<link rel="stylesheet" href="/static/bootstrap-icons/font/bootstrap-icons.min.css" />
<link rel="stylesheet" href="/static/css/main.css" />
<link rel="stylesheet" href="/static/css/guest.css" />
<link rel="icon" type="image/x-icon" href="/static/img/brainminder-icon.svg">
{{block "page:meta" . }}
@ -15,8 +15,15 @@
}}
</head>
<body class="guest">
Title
<div id="full-main-container">
<div id="page-top-bar">
<div id="app-title">
<img src="/static/img/brainminder.svg" alt="BrainMinder" class="logo" /><h5 class="show-large">BrainMinder</h5>
</div>
<div id="page-title-container">
<div id="page-title">Shared item : {{ .itemShare.ItemTitle}}</div>
</div>
</div>
<div id="guest-main-container">
<div id="page-content">{{template "page:content" .}}</div>
</div>
</body>

View file

@ -26,7 +26,7 @@
<p style="margin-right: 6px">
<label for="share-token">Share Url</label>
<div class="input-container">
<span class="prefix">{{ .baseUrl }}/item/share/</span>
<span class="prefix">{{ .publicBaseUrl }}/item/share/</span>
<input name="Token" id="share-token" type="url" readonly="readonly" value="{{ .shareToken }}" />
</div>
</p>

View file

@ -26,7 +26,7 @@
<p style="margin-right: 6px">
<label for="share-token">Share Url</label>
<div class="input-container">
<span class="prefix">{{ .baseUrl }}/item/share/</span>
<span class="prefix">{{ .publicBaseUrl }}/item/share/</span>
<input name="Token" id="share-token" type="url" readonly="readonly" value="{{ .itemShare.Token }}" />
</div>
</p>

View file

@ -1,4 +1,8 @@
{{define "page:content"}}
Shared item FOUND
{{ .itemShare.ItemTitle}}
<dl class="item-detail">
<dt>Summary</dt>
<dd>{{ .itemShare.ItemSummaryRendered | safeHTML}}</dd>
<dt>Description</dt>
<dd>{{ .itemShare.ItemDescriptionRendered | safeHTML}}</dd>
</dl>
{{end}}

View file

@ -1,9 +1,10 @@
package main
type config struct {
baseURL string
httpPort int
cookie struct {
publicBaseURL string
baseURL string
httpPort int
cookie struct {
secretKey string
}
db struct {

View file

@ -1,6 +1,7 @@
package main
import (
"brainminder.speedtech.it/internal/password"
"bytes"
"fmt"
"github.com/justinas/nosurf"
@ -993,7 +994,7 @@ func (app *application) itemShare(w http.ResponseWriter, r *http.Request) {
data := app.newTemplateData(r)
data["item"] = item
data["baseUrl"] = app.config.baseURL
data["publicBaseUrl"] = app.config.publicBaseURL
switch r.Method {
case http.MethodGet:
@ -1023,10 +1024,14 @@ func (app *application) itemShare(w http.ResponseWriter, r *http.Request) {
return
}
hashedPassword := ""
if itemShareFromForm.Password != "" {
hashedPassword, _ = password.Hash(itemShareFromForm.Password)
}
itemShare := &models.ItemShare{
ItemId: itemId,
Token: itemShareFromForm.Token,
Password: itemShareFromForm.Password,
Password: hashedPassword,
PermissionEdit: itemShareFromForm.PermissionEdit,
StartDatetime: itemShareFromForm.StartDatetime,
EndDatetime: itemShareFromForm.EndDatetime,
@ -1065,7 +1070,7 @@ func (app *application) itemShareEdit(w http.ResponseWriter, r *http.Request) {
data := app.newTemplateData(r)
data["itemShare"] = itemShare
data["baseUrl"] = app.config.baseURL
data["publicBaseUrl"] = app.config.publicBaseURL
switch r.Method {
case http.MethodGet:
@ -1093,9 +1098,13 @@ func (app *application) itemShareEdit(w http.ResponseWriter, r *http.Request) {
return
}
hashedPassword := ""
if itemShareFromForm.Password != "" {
hashedPassword, _ = password.Hash(itemShareFromForm.Password)
}
itemShare := &models.ItemShare{
Token: itemShareFromForm.Token,
Password: itemShareFromForm.Password,
Password: hashedPassword,
PermissionEdit: itemShareFromForm.PermissionEdit,
StartDatetime: itemShareFromForm.StartDatetime,
EndDatetime: itemShareFromForm.EndDatetime,

View file

@ -33,7 +33,8 @@ func main() {
func run(logger *slog.Logger) error {
var cfg config
cfg.baseURL = env.GetString("BASE_URL", "http://localhost:4445")
cfg.publicBaseURL = env.GetString("PUBLIC_BASE_URL", "http://localhost:4445")
cfg.baseURL = env.GetString("BASE_URL", "http://localhost")
cfg.httpPort = env.GetInt("HTTP_PORT", 4445)
cfg.cookie.secretKey = env.GetString("COOKIE_SECRET_KEY", `CHANGE_THIS_COOKIE_KEY`)
cfg.db.dsn = env.GetString("DB_DSN", "db.sqlite")

View file

@ -11,14 +11,16 @@ type ItemShareModel struct {
}
type ItemShare struct {
Id int64 `db:"id"`
Token string `db:"token"`
ItemId int64 `db:"item_id"`
PermissionEdit int `db:"permission_edit"`
StartDatetime string `db:"start_datetime"`
EndDatetime string `db:"end_datetime"`
Password string `db:"password"`
ItemTitle string `db:"item_title"`
Id int64 `db:"id"`
Token string `db:"token"`
ItemId int64 `db:"item_id"`
PermissionEdit int `db:"permission_edit"`
StartDatetime string `db:"start_datetime"`
EndDatetime string `db:"end_datetime"`
Password string `db:"password"`
ItemTitle string `db:"item_title"`
ItemSummaryRendered string `db:"item_summary_rendered"`
ItemDescriptionRendered string `db:"item_description_rendered"`
}
func (model *ItemShareModel) One(id int64) (*ItemShare, bool, error) {
@ -27,7 +29,8 @@ func (model *ItemShareModel) One(id int64) (*ItemShare, bool, error) {
var row ItemShare
query := `SELECT bmi.title AS item_title, bmis.* FROM bm_item_shares bmis
query := `SELECT bmi.title AS item_title, bmi.summary_rendered as item_summary_rendered, bmi.description_rendered as item_description_rendered,
bmis.* FROM bm_item_shares bmis
INNER JOIN bm_item bmi ON bmis.item_id=bmi.id WHERE bmis.id = $1`
err := model.DB.GetContext(ctx, &row, query, id)
@ -44,7 +47,8 @@ func (model *ItemShareModel) OneByToken(token string) (*ItemShare, bool, error)
var row ItemShare
query := `SELECT bmi.title AS item_title, bmis.* FROM bm_item_shares bmis
query := `SELECT bmi.title AS item_title, bmi.summary_rendered as item_summary_rendered, bmi.description_rendered as item_description_rendered,
bmis.* FROM bm_item_shares bmis
INNER JOIN bm_item bmi ON bmis.item_id=bmi.id WHERE bmis.token = $1`
err := model.DB.GetContext(ctx, &row, query, token)
@ -78,7 +82,7 @@ func (model *ItemShareModel) Update(ItemShare *ItemShare) error {
ctx, cancel := database.GetContext()
defer cancel()
query := `UPDATE bm_item_shares SET token=:token, permission_edit=:permission_edit, start_datetime=:start_datetime, end_datetime=:end_datetime WHERE id = :id`
query := `UPDATE bm_item_shares SET token=:token, permission_edit=:permission_edit, start_datetime=:start_datetime, end_datetime=:end_datetime, password=:password WHERE id = :id`
_, err := model.DB.NamedExecContext(ctx, query, ItemShare)
if err != nil {