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"
|
||||
)
|
||||
|
||||
// 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
|
||||
const (
|
||||
BcryptCost = 14
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package photo
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.nonshy.com/nonshy/website/pkg/config"
|
||||
|
@ -28,6 +29,30 @@ func VisibleAvatarURL(user, currentUser *models.User) string {
|
|||
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.
|
||||
func SignedPhotoURL(user *models.User, filename string) string {
|
||||
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).
|
||||
"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="card block">
|
||||
<header class="card-header has-background-link">
|
||||
<p class="card-header-title has-text-light">
|
||||
About Me
|
||||
</p>
|
||||
<div class="card-header-title">
|
||||
<div class="columns is-mobile is-gapless nonshy-fullwidth">
|
||||
<div class="column">
|
||||
About Me
|
||||
</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>
|
||||
|
||||
<div class="card-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 class="card block">
|
||||
<header class="card-header has-background-link">
|
||||
<p class="card-header-title has-text-light">
|
||||
My Interests
|
||||
</p>
|
||||
<div class="card-header-title">
|
||||
<div class="columns is-mobile is-gapless nonshy-fullwidth">
|
||||
<div class="column">
|
||||
My Interests
|
||||
</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>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
{{or (ToMarkdown (.User.GetProfileField "interests")) "n/a"}}
|
||||
{{or (ReSignPhotoLinks (ToMarkdown (.User.GetProfileField "interests"))) "n/a"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card block">
|
||||
<header class="card-header has-background-link">
|
||||
<p class="card-header-title has-text-light">
|
||||
Music/Bands/Movies
|
||||
</p>
|
||||
<div class="card-header-title">
|
||||
<div class="columns is-mobile is-gapless nonshy-fullwidth">
|
||||
<div class="column">
|
||||
Music/Bands/Movies
|
||||
</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>
|
||||
|
||||
<div class="card-content">
|
||||
<div class="content">
|
||||
{{or (ToMarkdown (.User.GetProfileField "music_movies")) "n/a"}}
|
||||
{{or (ReSignPhotoLinks (ToMarkdown (.User.GetProfileField "music_movies"))) "n/a"}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -307,7 +307,7 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="field" id="profile/about_me">
|
||||
<label class="label" for="about_me">About Me</label>
|
||||
<textarea class="textarea" cols="60" rows="4"
|
||||
id="about_me"
|
||||
|
@ -318,7 +318,7 @@
|
|||
</p>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="field" id="profile/interests">
|
||||
<label class="label" for="interests">My Interests</label>
|
||||
<textarea class="textarea" cols="60" rows="4"
|
||||
id="interests"
|
||||
|
@ -326,7 +326,7 @@
|
|||
placeholder="What kinds of things make you curious?">{{$User.GetProfileField "interests"}}</textarea>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<div class="field" id="profile/music_movies">
|
||||
<label class="label" for="music_movies">Music/Bands/Movies</label>
|
||||
<textarea class="textarea" cols="60" rows="4"
|
||||
id="music_movies"
|
||||
|
@ -1421,9 +1421,10 @@ window.addEventListener("DOMContentLoaded", (event) => {
|
|||
// Global function to toggle the active tab.
|
||||
const showTab = (name) => {
|
||||
name = name.replace(/\.$/, '');
|
||||
if (!name) name = "profile";
|
||||
let tabName = name.split('/')[0]; // "#profile/about_me"
|
||||
if (!tabName) name = "profile";
|
||||
$activeTab.style.display = 'none';
|
||||
switch (name) {
|
||||
switch (tabName) {
|
||||
case "look":
|
||||
$activeTab = $look;
|
||||
break;
|
||||
|
@ -1486,9 +1487,9 @@ window.addEventListener("DOMContentLoaded", (event) => {
|
|||
|
||||
// Show the requested tab on first page load.
|
||||
showTab(window.location.hash.replace(/^#/, ''));
|
||||
window.requestAnimationFrame(() => {
|
||||
window.scrollTo(0, 0);
|
||||
});
|
||||
// window.requestAnimationFrame(() => {
|
||||
// window.scrollTo(0, 0);
|
||||
// });
|
||||
});
|
||||
|
||||
// Look & Feel tab scripts.
|
||||
|
|
Loading…
Reference in New Issue
Block a user