Progress on item sharing
This commit is contained in:
parent
2c9b2d4eb7
commit
77663735a4
10 changed files with 212 additions and 31 deletions
160
assets/static/css/guest.css
Normal file
160
assets/static/css/guest.css
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package main
|
||||
|
||||
type config struct {
|
||||
publicBaseURL string
|
||||
baseURL string
|
||||
httpPort int
|
||||
cookie struct {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -19,6 +19,8 @@ type ItemShare struct {
|
|||
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 {
|
||||
|
|
Loading…
Add table
Reference in a new issue