Function to re-sign photo URLs on profile pages
* With the new JWT signatures on photo URLs, it was no longer possible for creative users to embed their gallery photos on their profile page. * Add a function to ReSignPhotoLinks that finds/replaces (on the server side) all references to paths under "/static/photos/" and gives them a fresh ?jwt= query string signature. * Note: only applies to the profile page essays, ReSignPhotoLinks is a template func that must be opted-in on a per page basis. Other miscellaneous fixes * Add "Edit" buttons in the corners of profile cards, when the current user looks at their profile page. They link to URIs like "/settings#profile/about_me" which will now: 1. Select the "Profile settings" tab like #profile 2. Scroll and focus the profile essay field that the user clicked to edit.
This commit is contained in:
parent
cb37934935
commit
b7bee75e1f
|
@ -25,6 +25,10 @@ const (
|
||||||
PhotoDiskPath = "./web/static/photos"
|
PhotoDiskPath = "./web/static/photos"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// PhotoURLRegexp describes an image path under "/static/photos" that can be parsed from Markdown or HTML input.
|
||||||
|
// It is used by e.g. the ReSignURLs function - if you move image URLs to a CDN this may need updating.
|
||||||
|
var PhotoURLRegexp = regexp.MustCompile(`(?:['"])(/static/photos/[^'"\s?]+(?:\?[^'"\s]*)?)(?:['"]|[^'"\s]*)`)
|
||||||
|
|
||||||
// Security
|
// Security
|
||||||
const (
|
const (
|
||||||
BcryptCost = 14
|
BcryptCost = 14
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package photo
|
package photo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"code.nonshy.com/nonshy/website/pkg/config"
|
"code.nonshy.com/nonshy/website/pkg/config"
|
||||||
|
@ -28,6 +29,30 @@ func VisibleAvatarURL(user, currentUser *models.User) string {
|
||||||
return "/static/img/shy.png"
|
return "/static/img/shy.png"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReSignPhotoLinks will search a blob of text for photo gallery links ("/static/photos/*") and re-sign
|
||||||
|
// their JWT security tokens.
|
||||||
|
func ReSignPhotoLinks(currentUser *models.User, text string) string {
|
||||||
|
var matches = config.PhotoURLRegexp.FindAllStringSubmatch(text, -1)
|
||||||
|
for _, m := range matches {
|
||||||
|
var (
|
||||||
|
origString = m[0]
|
||||||
|
url = m[1]
|
||||||
|
filename string
|
||||||
|
)
|
||||||
|
log.Error("ReSignPhotoLinks: got [%s] url [%s]", origString, url)
|
||||||
|
|
||||||
|
// Trim the /static/photos/ prefix off to get the URL down to its base filename.
|
||||||
|
filename = strings.Split(url, "?")[0]
|
||||||
|
filename = strings.TrimPrefix(filename, config.PhotoWebPath)
|
||||||
|
filename = strings.TrimPrefix(filename, "/")
|
||||||
|
|
||||||
|
// Sign the URL and replace the original.
|
||||||
|
signed := SignedPhotoURL(currentUser, filename)
|
||||||
|
text = strings.ReplaceAll(text, origString, signed)
|
||||||
|
}
|
||||||
|
return text
|
||||||
|
}
|
||||||
|
|
||||||
// SignedPhotoURL returns a URL path to a photo's filename, signed for the current user only.
|
// SignedPhotoURL returns a URL path to a photo's filename, signed for the current user only.
|
||||||
func SignedPhotoURL(user *models.User, filename string) string {
|
func SignedPhotoURL(user *models.User, filename string) string {
|
||||||
return createSignedPhotoURL(user.ID, user.Username, filename, false)
|
return createSignedPhotoURL(user.ID, user.Username, filename, false)
|
||||||
|
|
|
@ -71,6 +71,14 @@ func TemplateFuncs(r *http.Request) template.FuncMap {
|
||||||
|
|
||||||
// Get a description for an admin scope (e.g. for transparency page).
|
// Get a description for an admin scope (e.g. for transparency page).
|
||||||
"AdminScopeDescription": config.AdminScopeDescription,
|
"AdminScopeDescription": config.AdminScopeDescription,
|
||||||
|
|
||||||
|
// "ReSignPhotoLinks": photo.ReSignPhotoLinks,
|
||||||
|
"ReSignPhotoLinks": func(s template.HTML) template.HTML {
|
||||||
|
if currentUser, err := session.CurrentUser(r); err == nil {
|
||||||
|
return template.HTML(photo.ReSignPhotoLinks(currentUser, string(s)))
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -314,42 +314,75 @@
|
||||||
<div class="column is-two-thirds">
|
<div class="column is-two-thirds">
|
||||||
<div class="card block">
|
<div class="card block">
|
||||||
<header class="card-header has-background-link">
|
<header class="card-header has-background-link">
|
||||||
<p class="card-header-title has-text-light">
|
<div class="card-header-title">
|
||||||
|
<div class="columns is-mobile is-gapless nonshy-fullwidth">
|
||||||
|
<div class="column">
|
||||||
About Me
|
About Me
|
||||||
</p>
|
</div>
|
||||||
|
{{if eq .CurrentUser.ID .User.ID}}
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<a href="/settings#profile/about_me" class="button is-outlined is-small">
|
||||||
|
<i class="fa fa-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{or (ToMarkdown (.User.GetProfileField "about_me")) "n/a"}}
|
{{or (ReSignPhotoLinks (ToMarkdown (.User.GetProfileField "about_me"))) "n/a"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card block">
|
<div class="card block">
|
||||||
<header class="card-header has-background-link">
|
<header class="card-header has-background-link">
|
||||||
<p class="card-header-title has-text-light">
|
<div class="card-header-title">
|
||||||
|
<div class="columns is-mobile is-gapless nonshy-fullwidth">
|
||||||
|
<div class="column">
|
||||||
My Interests
|
My Interests
|
||||||
</p>
|
</div>
|
||||||
|
{{if eq .CurrentUser.ID .User.ID}}
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<a href="/settings#profile/interests" class="button is-outlined is-small">
|
||||||
|
<i class="fa fa-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{or (ToMarkdown (.User.GetProfileField "interests")) "n/a"}}
|
{{or (ReSignPhotoLinks (ToMarkdown (.User.GetProfileField "interests"))) "n/a"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card block">
|
<div class="card block">
|
||||||
<header class="card-header has-background-link">
|
<header class="card-header has-background-link">
|
||||||
<p class="card-header-title has-text-light">
|
<div class="card-header-title">
|
||||||
|
<div class="columns is-mobile is-gapless nonshy-fullwidth">
|
||||||
|
<div class="column">
|
||||||
Music/Bands/Movies
|
Music/Bands/Movies
|
||||||
</p>
|
</div>
|
||||||
|
{{if eq .CurrentUser.ID .User.ID}}
|
||||||
|
<div class="column is-narrow">
|
||||||
|
<a href="/settings#profile/music_movies" class="button is-outlined is-small">
|
||||||
|
<i class="fa fa-pencil"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{{or (ToMarkdown (.User.GetProfileField "music_movies")) "n/a"}}
|
{{or (ReSignPhotoLinks (ToMarkdown (.User.GetProfileField "music_movies"))) "n/a"}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -307,7 +307,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field" id="profile/about_me">
|
||||||
<label class="label" for="about_me">About Me</label>
|
<label class="label" for="about_me">About Me</label>
|
||||||
<textarea class="textarea" cols="60" rows="4"
|
<textarea class="textarea" cols="60" rows="4"
|
||||||
id="about_me"
|
id="about_me"
|
||||||
|
@ -318,7 +318,7 @@
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field" id="profile/interests">
|
||||||
<label class="label" for="interests">My Interests</label>
|
<label class="label" for="interests">My Interests</label>
|
||||||
<textarea class="textarea" cols="60" rows="4"
|
<textarea class="textarea" cols="60" rows="4"
|
||||||
id="interests"
|
id="interests"
|
||||||
|
@ -326,7 +326,7 @@
|
||||||
placeholder="What kinds of things make you curious?">{{$User.GetProfileField "interests"}}</textarea>
|
placeholder="What kinds of things make you curious?">{{$User.GetProfileField "interests"}}</textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="field">
|
<div class="field" id="profile/music_movies">
|
||||||
<label class="label" for="music_movies">Music/Bands/Movies</label>
|
<label class="label" for="music_movies">Music/Bands/Movies</label>
|
||||||
<textarea class="textarea" cols="60" rows="4"
|
<textarea class="textarea" cols="60" rows="4"
|
||||||
id="music_movies"
|
id="music_movies"
|
||||||
|
@ -1421,9 +1421,10 @@ window.addEventListener("DOMContentLoaded", (event) => {
|
||||||
// Global function to toggle the active tab.
|
// Global function to toggle the active tab.
|
||||||
const showTab = (name) => {
|
const showTab = (name) => {
|
||||||
name = name.replace(/\.$/, '');
|
name = name.replace(/\.$/, '');
|
||||||
if (!name) name = "profile";
|
let tabName = name.split('/')[0]; // "#profile/about_me"
|
||||||
|
if (!tabName) name = "profile";
|
||||||
$activeTab.style.display = 'none';
|
$activeTab.style.display = 'none';
|
||||||
switch (name) {
|
switch (tabName) {
|
||||||
case "look":
|
case "look":
|
||||||
$activeTab = $look;
|
$activeTab = $look;
|
||||||
break;
|
break;
|
||||||
|
@ -1486,9 +1487,9 @@ window.addEventListener("DOMContentLoaded", (event) => {
|
||||||
|
|
||||||
// Show the requested tab on first page load.
|
// Show the requested tab on first page load.
|
||||||
showTab(window.location.hash.replace(/^#/, ''));
|
showTab(window.location.hash.replace(/^#/, ''));
|
||||||
window.requestAnimationFrame(() => {
|
// window.requestAnimationFrame(() => {
|
||||||
window.scrollTo(0, 0);
|
// window.scrollTo(0, 0);
|
||||||
});
|
// });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Look & Feel tab scripts.
|
// Look & Feel tab scripts.
|
||||||
|
|
Loading…
Reference in New Issue
Block a user