Disable Gravatar (#37)

* Disable Gravatar
* Lowercase emails
* Add migration
This commit is contained in:
Thomas Miceli 2023-05-26 09:15:37 +02:00 committed by GitHub
parent 7cc77d80dc
commit 4a75a50370
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 138 additions and 17 deletions

View file

@ -28,7 +28,7 @@ A self-hosted pastebin **powered by Git**. [Try it here](https://opengist.thomic
* Editor with indentation mode & size ; drag and drop files * Editor with indentation mode & size ; drag and drop files
* Download raw files or as a ZIP archive * Download raw files or as a ZIP archive
* OAuth2 login with GitHub and Gitea * OAuth2 login with GitHub and Gitea
* Avatars * Avatars via Gravatar or OAuth2 providers
* Responsive UI * Responsive UI
* Enable or disable signups * Enable or disable signups
* Restrict or unrestrict snippets visibility to anonymous users * Restrict or unrestrict snippets visibility to anonymous users

View file

@ -4,5 +4,5 @@ package main
import "embed" import "embed"
//go:embed templates/*/*.html public/manifest.json public/assets/*.js public/assets/*.css public/assets/*.svg //go:embed templates/*/*.html public/manifest.json public/assets/*.js public/assets/*.css public/assets/*.svg public/assets/*.png
var dirFS embed.FS var dirFS embed.FS

View file

@ -13,6 +13,7 @@ const (
SettingDisableSignup = "disable-signup" SettingDisableSignup = "disable-signup"
SettingRequireLogin = "require-login" SettingRequireLogin = "require-login"
SettingDisableLoginForm = "disable-login-form" SettingDisableLoginForm = "disable-login-form"
SettingDisableGravatar = "disable-gravatar"
) )
func GetSetting(key string) (string, error) { func GetSetting(key string) (string, error) {

View file

@ -30,6 +30,7 @@ func Setup(dbpath string) error {
SettingDisableSignup: "0", SettingDisableSignup: "0",
SettingRequireLogin: "0", SettingRequireLogin: "0",
SettingDisableLoginForm: "0", SettingDisableLoginForm: "0",
SettingDisableGravatar: "0",
}) })
} }

View file

@ -28,6 +28,7 @@ func ApplyMigrations(db *gorm.DB) error {
Func func(*gorm.DB) error Func func(*gorm.DB) error
}{ }{
{1, v1_modifyConstraintToSSHKeys}, {1, v1_modifyConstraintToSSHKeys},
{2, v2_lowercaseEmails},
// Add more migrations here as needed // Add more migrations here as needed
} }
@ -94,3 +95,9 @@ func v1_modifyConstraintToSSHKeys(db *gorm.DB) error {
renameSQL := `ALTER TABLE ssh_keys_temp RENAME TO ssh_keys;` renameSQL := `ALTER TABLE ssh_keys_temp RENAME TO ssh_keys;`
return db.Exec(renameSQL).Error return db.Exec(renameSQL).Error
} }
func v2_lowercaseEmails(db *gorm.DB) error {
// Copy the lowercase emails into the new column
copySQL := `UPDATE users SET email = lower(email);`
return db.Exec(copySQL).Error
}

View file

@ -12,6 +12,7 @@ type User struct {
CreatedAt int64 CreatedAt int64
Email string Email string
MD5Hash string // for gravatar, if no Email is specified, the value is random MD5Hash string // for gravatar, if no Email is specified, the value is random
AvatarURL string
GithubID string GithubID string
GiteaID string GiteaID string
@ -81,6 +82,30 @@ func GetUserById(userId uint) (*User, error) {
return user, err return user, err
} }
func GetUsersFromEmails(emailsSet map[string]struct{}) (map[string]*User, error) {
var users []*User
emails := make([]string, 0, len(emailsSet))
for email := range emailsSet {
emails = append(emails, email)
}
err := db.
Where("email IN ?", emails).
Find(&users).Error
if err != nil {
return nil, err
}
userMap := make(map[string]*User)
for _, user := range users {
userMap[user.Email] = user
}
return userMap, nil
}
func SSHKeyExistsForUser(sshKey string, userId uint) (*SSHKey, error) { func SSHKeyExistsForUser(sshKey string, userId uint) (*SSHKey, error) {
key := new(SSHKey) key := new(SSHKey)
err := db. err := db.
@ -135,9 +160,15 @@ func (user *User) HasLiked(gist *Gist) (bool, error) {
func (user *User) DeleteProviderID(provider string) error { func (user *User) DeleteProviderID(provider string) error {
switch provider { switch provider {
case "github": case "github":
return db.Model(&user).Update("github_id", nil).Error return db.Model(&user).
Update("github_id", nil).
Update("avatar_url", nil).
Error
case "gitea": case "gitea":
return db.Model(&user).Update("gitea_id", nil).Error return db.Model(&user).
Update("gitea_id", nil).
Update("avatar_url", nil).
Error
} }
return nil return nil

View file

@ -3,6 +3,7 @@ package web
import ( import (
"context" "context"
"crypto/md5" "crypto/md5"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/labstack/echo/v4" "github.com/labstack/echo/v4"
@ -139,12 +140,14 @@ func oauthCallback(ctx echo.Context) error {
currUser := getUserLogged(ctx) currUser := getUserLogged(ctx)
if currUser != nil { if currUser != nil {
// if user is logged in, link account to user // if user is logged in, link account to user and update its avatar URL
switch user.Provider { switch user.Provider {
case "github": case "github":
currUser.GithubID = user.UserID currUser.GithubID = user.UserID
currUser.AvatarURL = getAvatarUrlFromProvider("github", user.UserID)
case "gitea": case "gitea":
currUser.GiteaID = user.UserID currUser.GiteaID = user.UserID
currUser.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
} }
if err = currUser.Update(); err != nil { if err = currUser.Update(); err != nil {
@ -172,11 +175,14 @@ func oauthCallback(ctx echo.Context) error {
MD5Hash: fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(user.Email))))), MD5Hash: fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(user.Email))))),
} }
// set provider id and avatar URL
switch user.Provider { switch user.Provider {
case "github": case "github":
userDB.GithubID = user.UserID userDB.GithubID = user.UserID
userDB.AvatarURL = getAvatarUrlFromProvider("github", user.UserID)
case "gitea": case "gitea":
userDB.GiteaID = user.UserID userDB.GiteaID = user.UserID
userDB.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName)
} }
if err = userDB.Create(); err != nil { if err = userDB.Create(); err != nil {
@ -328,3 +334,39 @@ func trimGiteaUrl() string {
return giteaUrl return giteaUrl
} }
func getAvatarUrlFromProvider(provider string, identifier string) string {
fmt.Println("getAvatarUrlFromProvider", provider, identifier)
switch provider {
case "github":
return "https://avatars.githubusercontent.com/u/" + identifier + "?v=4"
case "gitea":
resp, err := http.Get("https://gitea.com/api/v1/users/" + identifier)
if err != nil {
log.Error().Err(err).Msg("Cannot get user from Gitea")
return ""
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
log.Error().Err(err).Msg("Cannot read Gitea response body")
return ""
}
var result map[string]interface{}
err = json.Unmarshal(body, &result)
if err != nil {
log.Error().Err(err).Msg("Cannot unmarshal Gitea response body")
return ""
}
field, ok := result["avatar_url"]
if !ok {
log.Error().Msg("Field 'avatar_url' not found in Gitea JSON response")
return ""
}
return field.(string)
}
return ""
}

View file

@ -186,8 +186,22 @@ func revisions(ctx echo.Context) error {
return errorRes(404, "Page not found", nil) return errorRes(404, "Page not found", nil)
} }
emailsSet := map[string]struct{}{}
for _, commit := range commits {
if commit.AuthorEmail == "" {
continue
}
emailsSet[strings.ToLower(commit.AuthorEmail)] = struct{}{}
}
emailsUsers, err := models.GetUsersFromEmails(emailsSet)
if err != nil {
return errorRes(500, "Error fetching users emails", err)
}
setData(ctx, "page", "revisions") setData(ctx, "page", "revisions")
setData(ctx, "revision", "HEAD") setData(ctx, "revision", "HEAD")
setData(ctx, "emails", emailsUsers)
setData(ctx, "htmlTitle", "Revision of "+gist.Title) setData(ctx, "htmlTitle", "Revision of "+gist.Title)
return html(ctx, "revisions.html") return html(ctx, "revisions.html")

View file

@ -2,7 +2,6 @@ package web
import ( import (
"context" "context"
"crypto/md5"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/gorilla/sessions" "github.com/gorilla/sessions"
@ -71,11 +70,16 @@ var fm = template.FuncMap{
"slug": func(s string) string { "slug": func(s string) string {
return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-") return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-")
}, },
"avatarUrl": func(userHash string) string { "avatarUrl": func(user *models.User, noGravatar bool) string {
return "https://www.gravatar.com/avatar/" + userHash + "?d=identicon&s=200" if user.AvatarURL != "" {
}, return user.AvatarURL
"emailToMD5": func(email string) string { }
return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email)))))
if user.MD5Hash != "" && !noGravatar {
return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200"
}
return defaultAvatar()
}, },
"asset": func(jsfile string) string { "asset": func(jsfile string) string {
if dev { if dev {
@ -83,6 +87,7 @@ var fm = template.FuncMap{
} }
return "/" + manifestEntries[jsfile].File return "/" + manifestEntries[jsfile].File
}, },
"defaultAvatar": defaultAvatar,
} }
var EmbedFS fs.FS var EmbedFS fs.FS
@ -364,3 +369,10 @@ func parseManifestEntries() {
log.Fatal().Err(err).Msg("Failed to unmarshal manifest.json") log.Fatal().Err(err).Msg("Failed to unmarshal manifest.json")
} }
} }
func defaultAvatar() string {
if dev {
return "http://localhost:16157/default.png"
}
return "/" + manifestEntries["default.png"].File
}

View file

@ -37,7 +37,7 @@ func emailProcess(ctx echo.Context) error {
hash = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email))))) hash = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email)))))
} }
user.Email = email user.Email = strings.ToLower(email)
user.MD5Hash = hash user.MD5Hash = hash
if err := user.Update(); err != nil { if err := user.Update(); err != nil {

BIN
public/default.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View file

@ -2,6 +2,7 @@ import 'highlight.js/styles/tokyo-night-dark.css';
import './style.css'; import './style.css';
import './markdown.css'; import './markdown.css';
import './favicon.svg'; import './favicon.svg';
import './default.png';
import moment from 'moment'; import moment from 'moment';
import md from 'markdown-it'; import md from 'markdown-it';
import hljs from 'highlight.js'; import hljs from 'highlight.js';

View file

@ -37,6 +37,17 @@
</button> </button>
</div> </div>
</li> </li>
<li class="list-none gap-x-4 py-5">
<div class="flex items-center justify-between">
<span class="flex flex-grow flex-col">
<span class="text-sm font-medium leading-6 text-slate-300">Disable Gravatar</span>
<span class="text-sm text-gray-400">Disable the usage of Gravatar as an avatar provider.</span>
</span>
<button type="button" id="disable-gravatar" data-bool="{{ .DisableGravatar }}" class="toggle-button {{ if .DisableGravatar }}bg-primary-600{{else}}bg-gray-400{{end}} relative inline-flex h-6 w-11 ml-4 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-600 focus:ring-offset-2" role="switch" aria-checked="false" aria-labelledby="availability-label" aria-describedby="availability-description">
<span aria-hidden="true" class="{{ if .DisableGravatar }}translate-x-5{{else}}translate-x-0{{end}} pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out"></span>
</button>
</div>
</li>
</ul> </ul>
{{ .csrfHtml }} {{ .csrfHtml }}
</div> </div>

View file

@ -5,7 +5,7 @@
{{if .fromUser}} {{if .fromUser}}
<div class="flex items-center"> <div class="flex items-center">
<div class="flex-shrink-0"> <div class="flex-shrink-0">
<img class="h-12 w-12 rounded-md mr-2 border border-gray-700" src="{{ avatarUrl .fromUser.MD5Hash }}" alt=""> <img class="h-12 w-12 rounded-md mr-2 border border-gray-700" src="{{ avatarUrl .fromUser .DisableGravatar }}" alt="">
</div> </div>
<div> <div>
<h1 class="text-2xl font-bold leading-tight">{{.fromUser.Username}}</h1> <h1 class="text-2xl font-bold leading-tight">{{.fromUser.Username}}</h1>

View file

@ -8,7 +8,7 @@
{{ range $gist := .forks }} {{ range $gist := .forks }}
<li class="flex py-4"> <li class="flex py-4">
<a href="/{{ $gist.User.Username }}"> <a href="/{{ $gist.User.Username }}">
<img class="h-12 w-12 rounded-md mr-2 border border-gray-700" src="{{ avatarUrl $gist.User.MD5Hash }}" alt=""> <img class="h-12 w-12 rounded-md mr-2 border border-gray-700" src="{{ avatarUrl $gist.User $.DisableGravatar }}" alt="">
</a> </a>
<div> <div>
<a href="/{{ $gist.User.Username }}" class="text-sm font-medium text-slate-300">{{ $gist.User.Username }}</a> <a href="/{{ $gist.User.Username }}" class="text-sm font-medium text-slate-300">{{ $gist.User.Username }}</a>

View file

@ -6,7 +6,7 @@
{{ range $user := .likers }} {{ range $user := .likers }}
<div class="relative flex items-center space-x-3 rounded-lg border border-gray-600 bg-gray-800 px-6 py-5 shadow-sm focus-within:ring-1 focus-within:border-primary-500 focus-within:ring-primary-500 hover:border-gray-400"> <div class="relative flex items-center space-x-3 rounded-lg border border-gray-600 bg-gray-800 px-6 py-5 shadow-sm focus-within:ring-1 focus-within:border-primary-500 focus-within:ring-primary-500 hover:border-gray-400">
<div class="min-w-0 flex"> <div class="min-w-0 flex">
<img class="h-12 w-12 rounded-md mr-2 border border-gray-700" src="{{ avatarUrl $user.MD5Hash }}" alt=""> <img class="h-12 w-12 rounded-md mr-2 border border-gray-700" src="{{ avatarUrl $user $.DisableGravatar }}" alt="">
<a href="/{{ $user.Username }}" class="focus:outline-none"> <a href="/{{ $user.Username }}" class="focus:outline-none">
<span class="absolute inset-0" aria-hidden="true"></span> <span class="absolute inset-0" aria-hidden="true"></span>
<p class="text-sm font-medium text-slate-300 align-middle">{{ $user.Username }}</p> <p class="text-sm font-medium text-slate-300 align-middle">{{ $user.Username }}</p>

View file

@ -10,8 +10,9 @@
<svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"> <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3 mr-1 inline" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M13 5l7 7-7 7M5 5l7 7-7 7" /> <path stroke-linecap="round" stroke-linejoin="round" d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg> </svg>
<img class="h-5 w-5 rounded-full inline" src="{{ avatarUrl (emailToMD5 $commit.AuthorEmail) }}" alt="" /> {{ $user := (index $.emails $commit.AuthorEmail) }}
<span class="font-bold">{{ $commit.AuthorName }}</span> revised this gist <span class="moment-timestamp font-bold">{{ $commit.Timestamp }}</span>. <a href="/{{ $.gist.User.Username }}/{{ $.gist.Uuid }}/rev/{{ $commit.Hash }}">Go to revision</a></h3> <img class="h-5 w-5 rounded-full inline" src="{{if $user }}{{ avatarUrl $user $.DisableGravatar }}{{else}}{{defaultAvatar}}{{end}}" alt="" />
<span class="font-bold">{{if $user}}<a href="/{{$user.Username}}" class="text-slate-300 hover:text-slate-300 hover:underline">{{ $commit.AuthorName }}</a>{{else}}{{ $commit.AuthorName }}{{end}}</span> revised this gist <span class="moment-timestamp font-bold">{{ $commit.Timestamp }}</span>. <a href="/{{ $.gist.User.Username }}/{{ $.gist.Uuid }}/rev/{{ $commit.Hash }}">Go to revision</a></h3>
{{ if ne $commit.Changed "" }} {{ if ne $commit.Changed "" }}
<p class="text-sm float-right py-2"> <p class="text-sm float-right py-2">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline-flex"> <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 inline-flex">