Private Profiles & Misc Improvements

* Add setting to mark profile as "private"
* If a profile is private you can't see their profile page or user photo
  gallery unless you are friends (or admin)
* The Site Gallery never shows pictures from private profiles.
* Add HTML5 drag/drop upload support for photo gallery.
* Suppress SQL logging except in debug mode.
* Clean up extra logs.
forums
Noah 2022-08-21 17:29:39 -07:00
parent 09d61aa5c7
commit 7f96edf95d
15 changed files with 163 additions and 42 deletions

View File

@ -145,8 +145,11 @@ func initdb(c *cli.Context) {
// Load the settings.json
config.LoadSettings()
var gormcfg = &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
var gormcfg = &gorm.Config{}
if c.Bool("debug") {
gormcfg = &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
}
}
// Initialize the database.

View File

@ -3,7 +3,6 @@ package account
import (
"net/http"
"git.kirsle.net/apps/gosocial/pkg/log"
"git.kirsle.net/apps/gosocial/pkg/templates"
)
@ -11,7 +10,6 @@ import (
func Dashboard() http.HandlerFunc {
tmpl := templates.Must("account/dashboard.html")
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Error("Dashboard called")
if err := tmpl.Execute(w, r, nil); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return

View File

@ -37,6 +37,8 @@ func Profile() http.HandlerFunc {
return
}
var isSelf = currentUser.ID == user.ID
// Banned or disabled? Only admin can view then.
if user.Status != models.UserStatusActive && !currentUser.IsAdmin {
templates.NotFoundPage(w, r)
@ -49,9 +51,16 @@ func Profile() http.HandlerFunc {
return
}
// Are they friends? And/or is this user private?
var (
isFriend = models.FriendStatus(currentUser.ID, user.ID)
isPrivate = !currentUser.IsAdmin && !isSelf && user.Visibility == models.UserVisibilityPrivate && isFriend != "approved"
)
vars := map[string]interface{}{
"User": user,
"IsFriend": models.FriendStatus(currentUser.ID, user.ID),
"User": user,
"IsFriend": isFriend,
"IsPrivate": isPrivate,
}
if err := tmpl.Execute(w, r, vars); err != nil {

View File

@ -93,9 +93,15 @@ func Settings() http.HandlerFunc {
case "preferences":
var (
explicit = r.PostFormValue("explicit") == "true"
private = r.PostFormValue("private") == "true"
)
user.Explicit = explicit
if private {
user.Visibility = models.UserVisibilityPrivate
} else {
user.Visibility = models.UserVisibilityPublic
}
if err := user.Save(); err != nil {
session.FlashError(w, r, "Failed to save user to database: %s", err)

View File

@ -5,7 +5,6 @@ import (
"regexp"
"git.kirsle.net/apps/gosocial/pkg/config"
"git.kirsle.net/apps/gosocial/pkg/log"
"git.kirsle.net/apps/gosocial/pkg/models"
"git.kirsle.net/apps/gosocial/pkg/session"
"git.kirsle.net/apps/gosocial/pkg/templates"
@ -52,6 +51,17 @@ func UserPhotos() http.HandlerFunc {
return
}
// Is this user private and we're not friends?
var (
areFriends = models.AreFriends(user.ID, currentUser.ID)
isPrivate = user.Visibility == models.UserVisibilityPrivate && !areFriends
)
if isPrivate && !currentUser.IsAdmin && !isOwnPhotos {
session.FlashError(w, r, "This user's profile page and photo gallery are private.")
templates.Redirect(w, "/u/"+user.Username)
return
}
// What set of visibilities to query?
visibility := []models.PhotoVisibility{models.PhotoPublic}
if isOwnPhotos || currentUser.IsAdmin {
@ -73,7 +83,6 @@ func UserPhotos() http.HandlerFunc {
Sort: "created_at desc",
}
pager.ParsePage(r)
log.Error("Pager: %+v", pager)
photos, err := models.PaginateUserPhotos(user.ID, visibility, explicit, pager)
// Get the count of explicit photos if we are not viewing explicit photos.

View File

@ -16,7 +16,7 @@ func init() {
Theme: golog.DarkTheme,
})
log.Config.Level = golog.DebugLevel
log.Config.Level = golog.InfoLevel
}
// SetDebug toggles debug level logging.

View File

@ -152,6 +152,11 @@ func PaginateGalleryPhotos(userID uint64, adminView bool, explicitOK bool, pager
"EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND certified = true)",
)
// Exclude private users' photos.
wheres = append(wheres,
"NOT EXISTS (SELECT 1 FROM users WHERE id = photos.user_id AND visibility = 'private')",
)
// Admin view: get ALL PHOTOS on the site, period.
if adminView {
query = DB

View File

@ -19,9 +19,9 @@ type User struct {
Username string `gorm:"uniqueIndex"`
Email string `gorm:"uniqueIndex"`
HashedPassword string
IsAdmin bool `gorm:"index"`
Status UserStatus `gorm:"index"` // active, disabled
Visibility string `gorm:"index"` // public, private
IsAdmin bool `gorm:"index"`
Status UserStatus `gorm:"index"` // active, disabled
Visibility UserVisibility `gorm:"index"` // public, private
Name *string
Birthdate time.Time
Certified bool
@ -36,6 +36,13 @@ type User struct {
ProfilePhoto Photo `gorm:"foreignKey:profile_photo_id"`
}
type UserVisibility string
const (
UserVisibilityPublic UserVisibility = "public"
UserVisibilityPrivate = "private"
)
// Preload related tables for the user (classmethod).
func (u *User) Preload() *gorm.DB {
return DB.Preload("ProfileField").Preload("ProfilePhoto")
@ -60,9 +67,10 @@ func CreateUser(username, email, password string) (*User, error) {
}
u := &User{
Username: username,
Email: email,
Status: UserStatusActive,
Username: username,
Email: email,
Status: UserStatusActive,
Visibility: UserVisibilityPublic,
}
if err := u.HashPassword(password); err != nil {

View File

@ -56,7 +56,7 @@ func LoadOrNew(r *http.Request) *Session {
key := fmt.Sprintf(config.SessionRedisKeyFormat, sess.UUID)
err = redis.Get(key, sess)
log.Error("LoadOrNew: raw from Redis: %+v", sess)
// log.Error("LoadOrNew: raw from Redis: %+v", sess)
if err != nil {
log.Error("session.LoadOrNew: didn't find %s in Redis: %s", err)
}

View File

@ -29,6 +29,7 @@
<div class="column">
<h1 class="title">
{{.User.NameOrUsername}}
{{if eq .User.Visibility "private"}}<sup class="fa fa-mask ml-2 is-size-6" title="Private Profile"></sup>{{end}}
</h1>
{{if ne .User.Status "active"}}
<h2 class="subtitle">
@ -164,6 +165,14 @@
</div>
</section>
{{if .IsPrivate}}
<div class="block p-4">
<div class="notification block is-warning">
<i class="fa fa-mask"></i> This member's profile page is <strong>private.</strong> You may send them
a friend request, and only if approved, you may then view their profile page and photo gallery.
</div>
</div>
{{else}}
<div class="block p-4">
<div class="tabs is-boxed">
<ul>
@ -352,7 +361,8 @@
</div>
{{end}}
</div>
</div>
</div><!-- /columns-->
</div>
{{end}}<!-- not IsPrivate -->
</div>
{{end}}

View File

@ -183,7 +183,12 @@
</div>
<div class="media-content">
<p class="title is-4">
<a href="/u/{{.Username}}" class="has-text-dark">{{.NameOrUsername}}</a>
<a href="/u/{{.Username}}" class="has-text-dark">
{{.NameOrUsername}}
</a>
{{if eq .Visibility "private"}}
<sup class="fa fa-mask is-size-7" title="Private Profile"></sup>
{{end}}
</p>
<p class="subtitle is-6 mb-2">
<span class="icon"><i class="fa fa-user"></i></span>

View File

@ -219,6 +219,22 @@
</header>
<div class="card-content">
<div class="field">
<label class="label">Private Profile</label>
<label class="checkbox">
<input type="checkbox"
name="private"
value="true"
{{if eq .CurrentUser.Visibility "private"}}checked{{end}}>
Mark my profile page as "private"
</label>
<p class="help">
If you check this box then only friends who you have approved are able to
see your profile page and gallery. Your gallery photos also will NOT appear
on the Site Gallery page.
</p>
</div>
<div class="field">
<label class="label">Explicit Content Filter</label>
<label class="checkbox">

View File

@ -35,7 +35,7 @@
<ul>
<li>
For <strong>nudists:</strong> a default setting on your profile will hide 'explicit content'
from users' profile pages and you will not see the explicit web forums either.
from users' profile pages and you will not see the explicit web forum threads either.
</li>
<li>
For <strong>exhibitionists:</strong> you can toggle that setting to view explicit content
@ -44,6 +44,19 @@
</li>
</ul>
<h1>Open Beta</h1>
<p>
This website is currently open for beta testing. It's not 100% complete yet, but is basically
functional. It currently supports profile pages, photo galleries, friend requests, Direct
Messages, a member directory to discover new users, and basic account management features.
</p>
<p>
Before the "1.0" launch it will include web forums which will provide a community space to
chat with and meet other members.
</p>
<h1>Site Rules</h1>
<ul>
@ -72,25 +85,11 @@
</li>
</ul>
<h1>Site Features</h1>
<p>
This website is still a work in progress, but <em>eventually</em> it will have at least
the following features and functions:
For more details, please check out the <a href="/tos">Terms of Service</a> as well as
the <a href="/privacy">Privacy Policy</a>.
</p>
<ul>
<li>
<strong>Web forums</strong> where you can write posts and meet your fellow members in the comments.
</li>
<li>
<strong>Profile pages</strong> where you can write a bit about yourself and upload some of your
nudist or exhibitionist pictures.
</li>
<li>
<strong>Direct messages</strong> where you can chat with other members on the site.
</li>
</ul>
</div>
<div class="column is-one-quarter">

View File

@ -5,7 +5,12 @@
When User Gallery: .User is defined, .IsOwnPhotos may be.
-->
{{define "title"}}
{{if .IsSiteGallery}}Member Gallery{{else}}Photos of {{.User.Username}}{{end}}
{{if .IsSiteGallery}}
Member Gallery
{{else}}
Photos of {{.User.Username}}
{{if eq .User.Visibility "private"}}<sup class="fa fa-mask ml-2 is-size-6" title="Private Profile"></sup>{{end}}
{{end}}
{{end}}
<!-- Reusable card body -->

View File

@ -19,6 +19,16 @@
{{ $User := .CurrentUser }}
<!-- Drag/Drop Modal -->
<div class="modal" id="drop-modal">
<div class="modal-background"></div>
<div class="modal-content">
<div class="box content has-text-centered">
<h1><i class="fa fa-upload mr-2"></i> Drop image to select it for upload</h1>
</div>
</div>
</div>
{{if .EditPhoto}}
<form action="/photo/edit" method="POST">
<input type="hidden" name="id" value="{{.EditPhoto.ID}}">
@ -347,13 +357,13 @@
$fileName = document.querySelector("#fileName"),
$hiddenPreview = document.querySelector("#imagePreview"),
$previewBox = document.querySelector("#previewBox"),
$cropField = document.querySelector("#cropCoords");
$cropField = document.querySelector("#cropCoords"),
$dropArea = document.querySelector("#drop-modal")
$body = document.querySelector("body");
// Clear the answer in case of page reload.
$cropField.value = "";
$file.addEventListener("change", function() {
let file = this.files[0];
// Common handler for file selection, either via input
// field or drag/drop onto the page.
let onFile = (file) => {
$fileName.innerHTML = file.name;
// Read the image to show the preview on-page.
@ -411,7 +421,45 @@
}
});
reader.readAsDataURL(file);
};
// Set up drag/drop file upload events.
$body.addEventListener("dragenter", function(e) {
e.preventDefault();
e.stopPropagation();
$dropArea.classList.add("is-active");
});
$body.addEventListener("dragover", function(e) {
e.preventDefault();
e.stopPropagation();
$dropArea.classList.add("is-active");
});
$body.addEventListener("dragleave", function(e) {
e.preventDefault();
e.stopPropagation();
$dropArea.classList.remove("is-active");
});
$body.addEventListener("drop", function(e) {
e.preventDefault();
e.stopPropagation();
$dropArea.classList.remove("is-active");
// Grab the file.
let dt = e.dataTransfer;
let file = dt.files[0];
// Set the file on the input field too.
$file.files = dt.files;
onFile(file);
});
// Clear the answer in case of page reload.
$cropField.value = "";
$file.addEventListener("change", function() {
let file = this.files[0];
onFile(file);
});
});
{{end}}