diff --git a/README.md b/README.md index cb8d923..6f0fabb 100644 --- a/README.md +++ b/README.md @@ -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 * Download raw files or as a ZIP archive * OAuth2 login with GitHub and Gitea -* Avatars +* Avatars via Gravatar or OAuth2 providers * Responsive UI * Enable or disable signups * Restrict or unrestrict snippets visibility to anonymous users diff --git a/fs_embed.go b/fs_embed.go index a54d661..30d7fd4 100644 --- a/fs_embed.go +++ b/fs_embed.go @@ -4,5 +4,5 @@ package main 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 diff --git a/internal/models/admin_setting.go b/internal/models/admin_setting.go index 0cfae52..e23e938 100644 --- a/internal/models/admin_setting.go +++ b/internal/models/admin_setting.go @@ -13,6 +13,7 @@ const ( SettingDisableSignup = "disable-signup" SettingRequireLogin = "require-login" SettingDisableLoginForm = "disable-login-form" + SettingDisableGravatar = "disable-gravatar" ) func GetSetting(key string) (string, error) { diff --git a/internal/models/db.go b/internal/models/db.go index 540d539..4a417ca 100644 --- a/internal/models/db.go +++ b/internal/models/db.go @@ -30,6 +30,7 @@ func Setup(dbpath string) error { SettingDisableSignup: "0", SettingRequireLogin: "0", SettingDisableLoginForm: "0", + SettingDisableGravatar: "0", }) } diff --git a/internal/models/migration.go b/internal/models/migration.go index d4d7629..3240bf0 100644 --- a/internal/models/migration.go +++ b/internal/models/migration.go @@ -28,6 +28,7 @@ func ApplyMigrations(db *gorm.DB) error { Func func(*gorm.DB) error }{ {1, v1_modifyConstraintToSSHKeys}, + {2, v2_lowercaseEmails}, // 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;` 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 +} diff --git a/internal/models/user.go b/internal/models/user.go index 73682e2..9208d0d 100644 --- a/internal/models/user.go +++ b/internal/models/user.go @@ -12,6 +12,7 @@ type User struct { CreatedAt int64 Email string MD5Hash string // for gravatar, if no Email is specified, the value is random + AvatarURL string GithubID string GiteaID string @@ -81,6 +82,30 @@ func GetUserById(userId uint) (*User, error) { 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) { key := new(SSHKey) err := db. @@ -135,9 +160,15 @@ func (user *User) HasLiked(gist *Gist) (bool, error) { func (user *User) DeleteProviderID(provider string) error { switch provider { 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": - return db.Model(&user).Update("gitea_id", nil).Error + return db.Model(&user). + Update("gitea_id", nil). + Update("avatar_url", nil). + Error } return nil diff --git a/internal/web/auth.go b/internal/web/auth.go index a904ce4..2db5af0 100644 --- a/internal/web/auth.go +++ b/internal/web/auth.go @@ -3,6 +3,7 @@ package web import ( "context" "crypto/md5" + "encoding/json" "errors" "fmt" "github.com/labstack/echo/v4" @@ -139,12 +140,14 @@ func oauthCallback(ctx echo.Context) error { currUser := getUserLogged(ctx) 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 { case "github": currUser.GithubID = user.UserID + currUser.AvatarURL = getAvatarUrlFromProvider("github", user.UserID) case "gitea": currUser.GiteaID = user.UserID + currUser.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName) } 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))))), } + // set provider id and avatar URL switch user.Provider { case "github": userDB.GithubID = user.UserID + userDB.AvatarURL = getAvatarUrlFromProvider("github", user.UserID) case "gitea": userDB.GiteaID = user.UserID + userDB.AvatarURL = getAvatarUrlFromProvider("gitea", user.NickName) } if err = userDB.Create(); err != nil { @@ -328,3 +334,39 @@ func trimGiteaUrl() string { 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 "" +} diff --git a/internal/web/gist.go b/internal/web/gist.go index acee492..9778f31 100644 --- a/internal/web/gist.go +++ b/internal/web/gist.go @@ -186,8 +186,22 @@ func revisions(ctx echo.Context) error { 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, "revision", "HEAD") + setData(ctx, "emails", emailsUsers) setData(ctx, "htmlTitle", "Revision of "+gist.Title) return html(ctx, "revisions.html") diff --git a/internal/web/run.go b/internal/web/run.go index 3f0f0dd..dc5d0cc 100644 --- a/internal/web/run.go +++ b/internal/web/run.go @@ -2,7 +2,6 @@ package web import ( "context" - "crypto/md5" "encoding/json" "fmt" "github.com/gorilla/sessions" @@ -71,11 +70,16 @@ var fm = template.FuncMap{ "slug": func(s string) string { return strings.Trim(re.ReplaceAllString(strings.ToLower(s), "-"), "-") }, - "avatarUrl": func(userHash string) string { - return "https://www.gravatar.com/avatar/" + userHash + "?d=identicon&s=200" - }, - "emailToMD5": func(email string) string { - return fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email))))) + "avatarUrl": func(user *models.User, noGravatar bool) string { + if user.AvatarURL != "" { + return user.AvatarURL + } + + if user.MD5Hash != "" && !noGravatar { + return "https://www.gravatar.com/avatar/" + user.MD5Hash + "?d=identicon&s=200" + } + + return defaultAvatar() }, "asset": func(jsfile string) string { if dev { @@ -83,6 +87,7 @@ var fm = template.FuncMap{ } return "/" + manifestEntries[jsfile].File }, + "defaultAvatar": defaultAvatar, } var EmbedFS fs.FS @@ -364,3 +369,10 @@ func parseManifestEntries() { 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 +} diff --git a/internal/web/settings.go b/internal/web/settings.go index 4e4a3d5..fd6f0d3 100644 --- a/internal/web/settings.go +++ b/internal/web/settings.go @@ -37,7 +37,7 @@ func emailProcess(ctx echo.Context) error { hash = fmt.Sprintf("%x", md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email))))) } - user.Email = email + user.Email = strings.ToLower(email) user.MD5Hash = hash if err := user.Update(); err != nil { diff --git a/public/default.png b/public/default.png new file mode 100644 index 0000000..af51e5d Binary files /dev/null and b/public/default.png differ diff --git a/public/main.ts b/public/main.ts index a4b997e..86d36fc 100644 --- a/public/main.ts +++ b/public/main.ts @@ -2,6 +2,7 @@ import 'highlight.js/styles/tokyo-night-dark.css'; import './style.css'; import './markdown.css'; import './favicon.svg'; +import './default.png'; import moment from 'moment'; import md from 'markdown-it'; import hljs from 'highlight.js'; diff --git a/templates/pages/admin_settings.html b/templates/pages/admin_settings.html index 4782b67..77e5cc8 100644 --- a/templates/pages/admin_settings.html +++ b/templates/pages/admin_settings.html @@ -37,6 +37,17 @@ +
{{ $user.Username }}
diff --git a/templates/pages/revisions.html b/templates/pages/revisions.html index f332daa..419e2e6 100644 --- a/templates/pages/revisions.html +++ b/templates/pages/revisions.html @@ -10,8 +10,9 @@ - - {{ $commit.AuthorName }} revised this gist . Go to revision + {{ $user := (index $.emails $commit.AuthorEmail) }} + + {{if $user}}{{ $commit.AuthorName }}{{else}}{{ $commit.AuthorName }}{{end}} revised this gist . Go to revision {{ if ne $commit.Changed "" }}