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.
This commit is contained in:
parent
09d61aa5c7
commit
7f96edf95d
|
@ -145,8 +145,11 @@ func initdb(c *cli.Context) {
|
||||||
// Load the settings.json
|
// Load the settings.json
|
||||||
config.LoadSettings()
|
config.LoadSettings()
|
||||||
|
|
||||||
var gormcfg = &gorm.Config{
|
var gormcfg = &gorm.Config{}
|
||||||
Logger: logger.Default.LogMode(logger.Info),
|
if c.Bool("debug") {
|
||||||
|
gormcfg = &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Info),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize the database.
|
// Initialize the database.
|
||||||
|
|
|
@ -3,7 +3,6 @@ package account
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
"git.kirsle.net/apps/gosocial/pkg/log"
|
|
||||||
"git.kirsle.net/apps/gosocial/pkg/templates"
|
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -11,7 +10,6 @@ import (
|
||||||
func Dashboard() http.HandlerFunc {
|
func Dashboard() http.HandlerFunc {
|
||||||
tmpl := templates.Must("account/dashboard.html")
|
tmpl := templates.Must("account/dashboard.html")
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Error("Dashboard called")
|
|
||||||
if err := tmpl.Execute(w, r, nil); err != nil {
|
if err := tmpl.Execute(w, r, nil); err != nil {
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
return
|
return
|
||||||
|
|
|
@ -37,6 +37,8 @@ func Profile() http.HandlerFunc {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var isSelf = currentUser.ID == user.ID
|
||||||
|
|
||||||
// Banned or disabled? Only admin can view then.
|
// Banned or disabled? Only admin can view then.
|
||||||
if user.Status != models.UserStatusActive && !currentUser.IsAdmin {
|
if user.Status != models.UserStatusActive && !currentUser.IsAdmin {
|
||||||
templates.NotFoundPage(w, r)
|
templates.NotFoundPage(w, r)
|
||||||
|
@ -49,9 +51,16 @@ func Profile() http.HandlerFunc {
|
||||||
return
|
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{}{
|
vars := map[string]interface{}{
|
||||||
"User": user,
|
"User": user,
|
||||||
"IsFriend": models.FriendStatus(currentUser.ID, user.ID),
|
"IsFriend": isFriend,
|
||||||
|
"IsPrivate": isPrivate,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tmpl.Execute(w, r, vars); err != nil {
|
if err := tmpl.Execute(w, r, vars); err != nil {
|
||||||
|
|
|
@ -93,9 +93,15 @@ func Settings() http.HandlerFunc {
|
||||||
case "preferences":
|
case "preferences":
|
||||||
var (
|
var (
|
||||||
explicit = r.PostFormValue("explicit") == "true"
|
explicit = r.PostFormValue("explicit") == "true"
|
||||||
|
private = r.PostFormValue("private") == "true"
|
||||||
)
|
)
|
||||||
|
|
||||||
user.Explicit = explicit
|
user.Explicit = explicit
|
||||||
|
if private {
|
||||||
|
user.Visibility = models.UserVisibilityPrivate
|
||||||
|
} else {
|
||||||
|
user.Visibility = models.UserVisibilityPublic
|
||||||
|
}
|
||||||
|
|
||||||
if err := user.Save(); err != nil {
|
if err := user.Save(); err != nil {
|
||||||
session.FlashError(w, r, "Failed to save user to database: %s", err)
|
session.FlashError(w, r, "Failed to save user to database: %s", err)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
||||||
"git.kirsle.net/apps/gosocial/pkg/config"
|
"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/models"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/session"
|
"git.kirsle.net/apps/gosocial/pkg/session"
|
||||||
"git.kirsle.net/apps/gosocial/pkg/templates"
|
"git.kirsle.net/apps/gosocial/pkg/templates"
|
||||||
|
@ -52,6 +51,17 @@ func UserPhotos() http.HandlerFunc {
|
||||||
return
|
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?
|
// What set of visibilities to query?
|
||||||
visibility := []models.PhotoVisibility{models.PhotoPublic}
|
visibility := []models.PhotoVisibility{models.PhotoPublic}
|
||||||
if isOwnPhotos || currentUser.IsAdmin {
|
if isOwnPhotos || currentUser.IsAdmin {
|
||||||
|
@ -73,7 +83,6 @@ func UserPhotos() http.HandlerFunc {
|
||||||
Sort: "created_at desc",
|
Sort: "created_at desc",
|
||||||
}
|
}
|
||||||
pager.ParsePage(r)
|
pager.ParsePage(r)
|
||||||
log.Error("Pager: %+v", pager)
|
|
||||||
photos, err := models.PaginateUserPhotos(user.ID, visibility, explicit, pager)
|
photos, err := models.PaginateUserPhotos(user.ID, visibility, explicit, pager)
|
||||||
|
|
||||||
// Get the count of explicit photos if we are not viewing explicit photos.
|
// Get the count of explicit photos if we are not viewing explicit photos.
|
||||||
|
|
|
@ -16,7 +16,7 @@ func init() {
|
||||||
Theme: golog.DarkTheme,
|
Theme: golog.DarkTheme,
|
||||||
})
|
})
|
||||||
|
|
||||||
log.Config.Level = golog.DebugLevel
|
log.Config.Level = golog.InfoLevel
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetDebug toggles debug level logging.
|
// SetDebug toggles debug level logging.
|
||||||
|
|
|
@ -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)",
|
"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.
|
// Admin view: get ALL PHOTOS on the site, period.
|
||||||
if adminView {
|
if adminView {
|
||||||
query = DB
|
query = DB
|
||||||
|
|
|
@ -19,9 +19,9 @@ type User struct {
|
||||||
Username string `gorm:"uniqueIndex"`
|
Username string `gorm:"uniqueIndex"`
|
||||||
Email string `gorm:"uniqueIndex"`
|
Email string `gorm:"uniqueIndex"`
|
||||||
HashedPassword string
|
HashedPassword string
|
||||||
IsAdmin bool `gorm:"index"`
|
IsAdmin bool `gorm:"index"`
|
||||||
Status UserStatus `gorm:"index"` // active, disabled
|
Status UserStatus `gorm:"index"` // active, disabled
|
||||||
Visibility string `gorm:"index"` // public, private
|
Visibility UserVisibility `gorm:"index"` // public, private
|
||||||
Name *string
|
Name *string
|
||||||
Birthdate time.Time
|
Birthdate time.Time
|
||||||
Certified bool
|
Certified bool
|
||||||
|
@ -36,6 +36,13 @@ type User struct {
|
||||||
ProfilePhoto Photo `gorm:"foreignKey:profile_photo_id"`
|
ProfilePhoto Photo `gorm:"foreignKey:profile_photo_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UserVisibility string
|
||||||
|
|
||||||
|
const (
|
||||||
|
UserVisibilityPublic UserVisibility = "public"
|
||||||
|
UserVisibilityPrivate = "private"
|
||||||
|
)
|
||||||
|
|
||||||
// Preload related tables for the user (classmethod).
|
// Preload related tables for the user (classmethod).
|
||||||
func (u *User) Preload() *gorm.DB {
|
func (u *User) Preload() *gorm.DB {
|
||||||
return DB.Preload("ProfileField").Preload("ProfilePhoto")
|
return DB.Preload("ProfileField").Preload("ProfilePhoto")
|
||||||
|
@ -60,9 +67,10 @@ func CreateUser(username, email, password string) (*User, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
u := &User{
|
u := &User{
|
||||||
Username: username,
|
Username: username,
|
||||||
Email: email,
|
Email: email,
|
||||||
Status: UserStatusActive,
|
Status: UserStatusActive,
|
||||||
|
Visibility: UserVisibilityPublic,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := u.HashPassword(password); err != nil {
|
if err := u.HashPassword(password); err != nil {
|
||||||
|
|
|
@ -56,7 +56,7 @@ func LoadOrNew(r *http.Request) *Session {
|
||||||
key := fmt.Sprintf(config.SessionRedisKeyFormat, sess.UUID)
|
key := fmt.Sprintf(config.SessionRedisKeyFormat, sess.UUID)
|
||||||
|
|
||||||
err = redis.Get(key, sess)
|
err = redis.Get(key, sess)
|
||||||
log.Error("LoadOrNew: raw from Redis: %+v", sess)
|
// log.Error("LoadOrNew: raw from Redis: %+v", sess)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("session.LoadOrNew: didn't find %s in Redis: %s", err)
|
log.Error("session.LoadOrNew: didn't find %s in Redis: %s", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@
|
||||||
<div class="column">
|
<div class="column">
|
||||||
<h1 class="title">
|
<h1 class="title">
|
||||||
{{.User.NameOrUsername}}
|
{{.User.NameOrUsername}}
|
||||||
|
{{if eq .User.Visibility "private"}}<sup class="fa fa-mask ml-2 is-size-6" title="Private Profile"></sup>{{end}}
|
||||||
</h1>
|
</h1>
|
||||||
{{if ne .User.Status "active"}}
|
{{if ne .User.Status "active"}}
|
||||||
<h2 class="subtitle">
|
<h2 class="subtitle">
|
||||||
|
@ -164,6 +165,14 @@
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</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="block p-4">
|
||||||
<div class="tabs is-boxed">
|
<div class="tabs is-boxed">
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -352,7 +361,8 @@
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div><!-- /columns-->
|
||||||
</div>
|
</div>
|
||||||
|
{{end}}<!-- not IsPrivate -->
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
|
@ -183,7 +183,12 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="media-content">
|
<div class="media-content">
|
||||||
<p class="title is-4">
|
<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>
|
||||||
<p class="subtitle is-6 mb-2">
|
<p class="subtitle is-6 mb-2">
|
||||||
<span class="icon"><i class="fa fa-user"></i></span>
|
<span class="icon"><i class="fa fa-user"></i></span>
|
||||||
|
|
|
@ -219,6 +219,22 @@
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="card-content">
|
<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">
|
<div class="field">
|
||||||
<label class="label">Explicit Content Filter</label>
|
<label class="label">Explicit Content Filter</label>
|
||||||
<label class="checkbox">
|
<label class="checkbox">
|
||||||
|
|
|
@ -35,7 +35,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li>
|
<li>
|
||||||
For <strong>nudists:</strong> a default setting on your profile will hide 'explicit content'
|
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>
|
||||||
<li>
|
<li>
|
||||||
For <strong>exhibitionists:</strong> you can toggle that setting to view explicit content
|
For <strong>exhibitionists:</strong> you can toggle that setting to view explicit content
|
||||||
|
@ -44,6 +44,19 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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>
|
<h1>Site Rules</h1>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
|
@ -72,25 +85,11 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h1>Site Features</h1>
|
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
This website is still a work in progress, but <em>eventually</em> it will have at least
|
For more details, please check out the <a href="/tos">Terms of Service</a> as well as
|
||||||
the following features and functions:
|
the <a href="/privacy">Privacy Policy</a>.
|
||||||
</p>
|
</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>
|
||||||
|
|
||||||
<div class="column is-one-quarter">
|
<div class="column is-one-quarter">
|
||||||
|
|
|
@ -5,7 +5,12 @@
|
||||||
When User Gallery: .User is defined, .IsOwnPhotos may be.
|
When User Gallery: .User is defined, .IsOwnPhotos may be.
|
||||||
-->
|
-->
|
||||||
{{define "title"}}
|
{{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}}
|
{{end}}
|
||||||
|
|
||||||
<!-- Reusable card body -->
|
<!-- Reusable card body -->
|
||||||
|
|
|
@ -19,6 +19,16 @@
|
||||||
|
|
||||||
{{ $User := .CurrentUser }}
|
{{ $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}}
|
{{if .EditPhoto}}
|
||||||
<form action="/photo/edit" method="POST">
|
<form action="/photo/edit" method="POST">
|
||||||
<input type="hidden" name="id" value="{{.EditPhoto.ID}}">
|
<input type="hidden" name="id" value="{{.EditPhoto.ID}}">
|
||||||
|
@ -347,13 +357,13 @@
|
||||||
$fileName = document.querySelector("#fileName"),
|
$fileName = document.querySelector("#fileName"),
|
||||||
$hiddenPreview = document.querySelector("#imagePreview"),
|
$hiddenPreview = document.querySelector("#imagePreview"),
|
||||||
$previewBox = document.querySelector("#previewBox"),
|
$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.
|
// Common handler for file selection, either via input
|
||||||
$cropField.value = "";
|
// field or drag/drop onto the page.
|
||||||
|
let onFile = (file) => {
|
||||||
$file.addEventListener("change", function() {
|
|
||||||
let file = this.files[0];
|
|
||||||
$fileName.innerHTML = file.name;
|
$fileName.innerHTML = file.name;
|
||||||
|
|
||||||
// Read the image to show the preview on-page.
|
// Read the image to show the preview on-page.
|
||||||
|
@ -411,7 +421,45 @@
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
reader.readAsDataURL(file);
|
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}}
|
{{end}}
|
||||||
|
|
Reference in New Issue
Block a user