Assets for user avatars
This commit is contained in:
parent
eb32b04437
commit
f4f439489d
|
@ -1,4 +1,4 @@
|
|||
function ImageSelector(form, maxFileSize, container) {
|
||||
function ImageSelector(form, maxFileSize, container, defaultImageUrl) {
|
||||
this.form = form;
|
||||
this.maxFileSize = maxFileSize;
|
||||
this.fileInput = container.querySelector(".image_input");
|
||||
|
@ -8,6 +8,7 @@ function ImageSelector(form, maxFileSize, container) {
|
|||
this.removeLink = container.querySelector(".remove");
|
||||
this.originalImageUrl = this.imageEl.getAttribute("data-original");
|
||||
this.currentImageUrl = this.originalImageUrl;
|
||||
this.defaultImageUrl = defaultImageUrl || "";
|
||||
|
||||
this.fileInput.value = "";
|
||||
this.removeImageInput.value = "";
|
||||
|
@ -45,7 +46,7 @@ ImageSelector.prototype.removeImage = function() {
|
|||
this.updateSizeLimit(0);
|
||||
this.fileInput.value = "";
|
||||
this.removeImageInput.value = "true";
|
||||
this.setImageUrl("");
|
||||
this.setImageUrl(this.defaultImageUrl);
|
||||
this.updateButtons();
|
||||
};
|
||||
|
||||
|
@ -82,13 +83,15 @@ ImageSelector.prototype.setImageUrl = function(url) {
|
|||
};
|
||||
|
||||
ImageSelector.prototype.updateButtons = function() {
|
||||
if (this.originalImageUrl.length > 0 && this.currentImageUrl != this.originalImageUrl) {
|
||||
if ((this.originalImageUrl.length > 0 && this.originalImageUrl != this.defaultImageUrl)
|
||||
&& this.currentImageUrl != this.originalImageUrl) {
|
||||
|
||||
this.resetLink.style.display = "inline-block";
|
||||
} else {
|
||||
this.resetLink.style.display = "none";
|
||||
}
|
||||
|
||||
if (!this.fileInput.required && this.currentImageUrl != "") {
|
||||
if (!this.fileInput.required && this.currentImageUrl != this.defaultImageUrl) {
|
||||
this.removeLink.style.display = "inline-block";
|
||||
} else {
|
||||
this.removeLink.style.display = "none";
|
||||
|
|
|
@ -399,6 +399,7 @@ func init() {
|
|||
}
|
||||
width := 0
|
||||
height := 0
|
||||
mime := ""
|
||||
fileExtensionOverrides := []string{".svg"}
|
||||
fileExt := strings.ToLower(path.Ext(filepath))
|
||||
tryDecode := true
|
||||
|
@ -414,9 +415,13 @@ func init() {
|
|||
}
|
||||
width = config.Width
|
||||
height = config.Height
|
||||
mime = http.DetectContentType(contents)
|
||||
} else {
|
||||
if fileExt == ".svg" {
|
||||
mime = "image/svg+xml"
|
||||
}
|
||||
}
|
||||
|
||||
mime := http.DetectContentType(contents)
|
||||
filename := path.Base(filepath)
|
||||
|
||||
asset, err := assets.Create(ctx, conn, assets.CreateInput{
|
||||
|
@ -490,5 +495,109 @@ func init() {
|
|||
}
|
||||
adminCommand.AddCommand(uploadProjectLogos)
|
||||
|
||||
uploadUserAvatars := &cobra.Command{
|
||||
Use: "uploaduseravatars",
|
||||
Short: "Uploads avatar imagefiles to S3 and replaces them with assets",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
ctx := context.Background()
|
||||
conn := db.NewConnPool(1, 1)
|
||||
defer conn.Close()
|
||||
|
||||
type userQuery struct {
|
||||
User models.User `db:"auth_user"`
|
||||
}
|
||||
allUsers, err := db.Query(ctx, conn, userQuery{}, `SELECT $columns FROM auth_user LEFT JOIN handmade_asset AS auth_user_avatar ON auth_user_avatar.id = auth_user.avatar_asset_id`)
|
||||
if err != nil {
|
||||
panic(oops.New(err, "Failed to fetch projects from db"))
|
||||
}
|
||||
|
||||
var fixupUsers []*models.User
|
||||
numImages := 0
|
||||
for _, user := range allUsers {
|
||||
u := &user.(*userQuery).User
|
||||
if u.Avatar != nil && *u.Avatar != "" {
|
||||
fixupUsers = append(fixupUsers, u)
|
||||
numImages += 1
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("%d images to upload\n", numImages)
|
||||
|
||||
uploadImage := func(ctx context.Context, conn db.ConnOrTx, filepath string, owner *models.User) *models.Asset {
|
||||
filepath = "./public/media/" + filepath
|
||||
contents, err := os.ReadFile(filepath)
|
||||
if err != nil {
|
||||
panic(oops.New(err, fmt.Sprintf("Failed to read file: %s", filepath)))
|
||||
}
|
||||
width := 0
|
||||
height := 0
|
||||
mime := ""
|
||||
fileExtensionOverrides := []string{".svg"}
|
||||
fileExt := strings.ToLower(path.Ext(filepath))
|
||||
tryDecode := true
|
||||
for _, ext := range fileExtensionOverrides {
|
||||
if fileExt == ext {
|
||||
tryDecode = false
|
||||
}
|
||||
}
|
||||
if tryDecode {
|
||||
config, _, err := image.DecodeConfig(bytes.NewReader(contents))
|
||||
if err != nil {
|
||||
panic(oops.New(err, fmt.Sprintf("Failed to decode file: %s", filepath)))
|
||||
}
|
||||
width = config.Width
|
||||
height = config.Height
|
||||
mime = http.DetectContentType(contents)
|
||||
} else {
|
||||
if fileExt == ".svg" {
|
||||
mime = "image/svg+xml"
|
||||
}
|
||||
}
|
||||
|
||||
filename := path.Base(filepath)
|
||||
|
||||
asset, err := assets.Create(ctx, conn, assets.CreateInput{
|
||||
Content: contents,
|
||||
Filename: filename,
|
||||
ContentType: mime,
|
||||
UploaderID: &owner.ID,
|
||||
Width: width,
|
||||
Height: height,
|
||||
})
|
||||
if err != nil {
|
||||
panic(oops.New(err, "Failed to create asset"))
|
||||
}
|
||||
|
||||
return asset
|
||||
}
|
||||
|
||||
for _, u := range fixupUsers {
|
||||
if u.Avatar != nil && *u.Avatar != "" {
|
||||
avatarAsset := uploadImage(ctx, conn, *u.Avatar, u)
|
||||
_, err := conn.Exec(ctx,
|
||||
`
|
||||
UPDATE auth_user
|
||||
SET
|
||||
avatar_asset_id = $2,
|
||||
avatar = NULL
|
||||
WHERE
|
||||
id = $1
|
||||
`,
|
||||
u.ID,
|
||||
avatarAsset.ID,
|
||||
)
|
||||
if err != nil {
|
||||
panic(oops.New(err, "Failed to update user"))
|
||||
}
|
||||
numImages -= 1
|
||||
fmt.Printf(".")
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("\nDone! %d images not patched for some reason.\n\n", numImages)
|
||||
},
|
||||
}
|
||||
adminCommand.AddCommand(uploadUserAvatars)
|
||||
|
||||
addProjectCommands(adminCommand)
|
||||
}
|
||||
|
|
31
src/db/db.go
31
src/db/db.go
|
@ -242,12 +242,22 @@ func Query(ctx context.Context, conn ConnOrTx, destExample interface{}, query st
|
|||
|
||||
func QueryIterator(ctx context.Context, conn ConnOrTx, destExample interface{}, query string, args ...interface{}) (*StructQueryIterator, error) {
|
||||
destType := reflect.TypeOf(destExample)
|
||||
columnNames, fieldPaths, err := getColumnNamesAndPaths(destType, nil, "")
|
||||
columnNames, fieldPaths, err := getColumnNamesAndPaths(destType, nil, nil)
|
||||
if err != nil {
|
||||
return nil, oops.New(err, "failed to generate column names")
|
||||
}
|
||||
|
||||
columnNamesString := strings.Join(columnNames, ", ")
|
||||
columns := make([]string, 0, len(columnNames))
|
||||
for _, strSlice := range columnNames {
|
||||
tableName := strings.Join(strSlice[0:len(strSlice)-1], "_")
|
||||
fullName := strSlice[len(strSlice)-1]
|
||||
if tableName != "" {
|
||||
fullName = tableName + "." + fullName
|
||||
}
|
||||
columns = append(columns, fullName)
|
||||
}
|
||||
|
||||
columnNamesString := strings.Join(columns, ", ")
|
||||
query = strings.Replace(query, "$columns", columnNamesString, -1)
|
||||
|
||||
rows, err := conn.Query(ctx, query, args...)
|
||||
|
@ -282,8 +292,8 @@ func QueryIterator(ctx context.Context, conn ConnOrTx, destExample interface{},
|
|||
return it, nil
|
||||
}
|
||||
|
||||
func getColumnNamesAndPaths(destType reflect.Type, pathSoFar []int, prefix string) (names []string, paths [][]int, err error) {
|
||||
var columnNames []string
|
||||
func getColumnNamesAndPaths(destType reflect.Type, pathSoFar []int, prefix []string) (names [][]string, paths [][]int, err error) {
|
||||
var columnNames [][]string
|
||||
var fieldPaths [][]int
|
||||
|
||||
if destType.Kind() == reflect.Ptr {
|
||||
|
@ -301,7 +311,10 @@ func getColumnNamesAndPaths(destType reflect.Type, pathSoFar []int, prefix strin
|
|||
var anonPrefixes []AnonPrefix
|
||||
|
||||
for _, field := range reflect.VisibleFields(destType) {
|
||||
path := append(pathSoFar, field.Index...)
|
||||
path := make([]int, len(pathSoFar))
|
||||
copy(path, pathSoFar)
|
||||
path = append(path, field.Index...)
|
||||
fieldColumnNames := prefix[:]
|
||||
|
||||
if columnName := field.Tag.Get("db"); columnName != "" {
|
||||
if field.Anonymous {
|
||||
|
@ -318,7 +331,7 @@ func getColumnNamesAndPaths(destType reflect.Type, pathSoFar []int, prefix strin
|
|||
}
|
||||
}
|
||||
if equal {
|
||||
columnName = anonPrefix.Prefix + "." + columnName
|
||||
fieldColumnNames = append(fieldColumnNames, anonPrefix.Prefix)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
@ -329,11 +342,13 @@ func getColumnNamesAndPaths(destType reflect.Type, pathSoFar []int, prefix strin
|
|||
fieldType = fieldType.Elem()
|
||||
}
|
||||
|
||||
fieldColumnNames = append(fieldColumnNames, columnName)
|
||||
|
||||
if typeIsQueryable(fieldType) {
|
||||
columnNames = append(columnNames, prefix+columnName)
|
||||
columnNames = append(columnNames, fieldColumnNames)
|
||||
fieldPaths = append(fieldPaths, path)
|
||||
} else if fieldType.Kind() == reflect.Struct {
|
||||
subCols, subPaths, err := getColumnNamesAndPaths(fieldType, path, columnName+".")
|
||||
subCols, subPaths, err := getColumnNamesAndPaths(fieldType, path, fieldColumnNames)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
|
|
@ -97,6 +97,7 @@ func (bot *botInstance) handleProfileCommand(ctx context.Context, i *Interaction
|
|||
FROM
|
||||
handmade_discorduser AS duser
|
||||
JOIN auth_user ON duser.hmn_user_id = auth_user.id
|
||||
LEFT JOIN handmade_asset AS auth_user_avatar ON auth_user_avatar.id = auth_user.avatar_asset_id
|
||||
WHERE
|
||||
duser.userid = $1
|
||||
`,
|
||||
|
|
|
@ -640,6 +640,7 @@ func (bot *botInstance) messageDelete(ctx context.Context, msgDelete MessageDele
|
|||
handmade_discordmessage AS msg
|
||||
LEFT JOIN handmade_discorduser AS duser ON msg.user_id = duser.userid
|
||||
LEFT JOIN auth_user AS hmnuser ON duser.hmn_user_id = hmnuser.id
|
||||
LEFT JOIN handmade_asset AS hmnuser_avatar ON hmnuser_avatar.id = hmnuser.avatar_asset_id
|
||||
LEFT JOIN handmade_snippet AS snippet ON snippet.discord_message_id = msg.id
|
||||
WHERE msg.id = $1 AND msg.channel_id = $2
|
||||
`,
|
||||
|
|
|
@ -526,6 +526,7 @@ func FetchDiscordUser(ctx context.Context, dbConn db.ConnOrTx, discordUserID str
|
|||
FROM
|
||||
handmade_discorduser AS duser
|
||||
JOIN auth_user AS u ON duser.hmn_user_id = u.id
|
||||
LEFT JOIN handmade_asset AS u_avatar ON u_avatar.id = u.avatar_asset_id
|
||||
WHERE
|
||||
duser.userid = $1
|
||||
`,
|
||||
|
|
|
@ -9,7 +9,6 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/perf"
|
||||
"git.handmade.network/hmn/hmn/src/templates"
|
||||
)
|
||||
|
||||
type ProjectTypeQuery int
|
||||
|
@ -52,10 +51,6 @@ func (p *ProjectAndStuff) TagText() string {
|
|||
}
|
||||
}
|
||||
|
||||
func (p *ProjectAndStuff) LogoURL(theme string) string {
|
||||
return templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset, theme)
|
||||
}
|
||||
|
||||
func FetchProjects(
|
||||
ctx context.Context,
|
||||
dbConn db.ConnOrTx,
|
||||
|
@ -366,12 +361,16 @@ func FetchMultipleProjectsOwners(
|
|||
userIds = append(userIds, userProject.UserID)
|
||||
}
|
||||
}
|
||||
iusers, err := db.Query(ctx, tx, models.User{},
|
||||
type userQuery struct {
|
||||
User models.User `db:"auth_user"`
|
||||
}
|
||||
iusers, err := db.Query(ctx, tx, userQuery{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM auth_user
|
||||
LEFT JOIN handmade_asset AS auth_user_avatar ON auth_user_avatar.id = auth_user.avatar_asset_id
|
||||
WHERE
|
||||
id = ANY($1)
|
||||
auth_user.id = ANY($1)
|
||||
`,
|
||||
userIds,
|
||||
)
|
||||
|
@ -398,9 +397,9 @@ func FetchMultipleProjectsOwners(
|
|||
// Get the full user record we fetched
|
||||
var user *models.User
|
||||
for _, iuser := range iusers {
|
||||
u := iuser.(*models.User)
|
||||
u := iuser.(*userQuery).User
|
||||
if u.ID == userProject.UserID {
|
||||
user = u
|
||||
user = &u
|
||||
}
|
||||
}
|
||||
if user == nil {
|
||||
|
|
|
@ -80,6 +80,7 @@ func FetchSnippets(
|
|||
FROM
|
||||
handmade_snippet AS snippet
|
||||
LEFT JOIN auth_user AS owner ON snippet.owner_id = owner.id
|
||||
LEFT JOIN handmade_asset AS owner_avatar ON owner_avatar.id = owner.avatar_asset_id
|
||||
LEFT JOIN handmade_asset AS asset ON snippet.asset_id = asset.id
|
||||
LEFT JOIN handmade_discordmessage AS discord_message ON snippet.discord_message_id = discord_message.id
|
||||
WHERE
|
||||
|
|
|
@ -78,7 +78,9 @@ func FetchThreads(
|
|||
JOIN handmade_postversion AS first_version ON first_version.id = first_post.current_id
|
||||
JOIN handmade_postversion AS last_version ON last_version.id = last_post.current_id
|
||||
LEFT JOIN auth_user AS first_author ON first_author.id = first_post.author_id
|
||||
LEFT JOIN handmade_asset AS first_author_avatar ON first_author_avatar.id = first_author.avatar_asset_id
|
||||
LEFT JOIN auth_user AS last_author ON last_author.id = last_post.author_id
|
||||
LEFT JOIN handmade_asset AS last_author_avatar ON last_author_avatar.id = last_author.avatar_asset_id
|
||||
LEFT JOIN handmade_threadlastreadinfo AS tlri ON (
|
||||
tlri.thread_id = thread.id
|
||||
AND tlri.user_id = $?
|
||||
|
@ -221,6 +223,7 @@ func CountThreads(
|
|||
JOIN handmade_project AS project ON thread.project_id = project.id
|
||||
JOIN handmade_post AS first_post ON first_post.id = thread.first_id
|
||||
LEFT JOIN auth_user AS first_author ON first_author.id = first_post.author_id
|
||||
LEFT JOIN handmade_asset AS first_author_avatar ON first_author_avatar.id = first_author.avatar_asset_id
|
||||
WHERE
|
||||
NOT thread.deleted
|
||||
AND ( -- project has valid lifecycle
|
||||
|
@ -330,7 +333,9 @@ func FetchPosts(
|
|||
JOIN handmade_project AS project ON post.project_id = project.id
|
||||
JOIN handmade_postversion AS ver ON ver.id = post.current_id
|
||||
LEFT JOIN auth_user AS author ON author.id = post.author_id
|
||||
LEFT JOIN handmade_asset AS author_avatar ON author_avatar.id = author.avatar_asset_id
|
||||
LEFT JOIN auth_user AS editor ON ver.editor_id = editor.id
|
||||
LEFT JOIN handmade_asset AS editor_avatar ON editor_avatar.id = editor.avatar_asset_id
|
||||
LEFT JOIN handmade_threadlastreadinfo AS tlri ON (
|
||||
tlri.thread_id = thread.id
|
||||
AND tlri.user_id = $?
|
||||
|
@ -344,6 +349,7 @@ func FetchPosts(
|
|||
-- check fails.
|
||||
LEFT JOIN handmade_post AS reply_post ON reply_post.id = post.reply_id
|
||||
LEFT JOIN auth_user AS reply_author ON reply_post.author_id = reply_author.id
|
||||
LEFT JOIN handmade_asset AS reply_author_avatar ON reply_author_avatar.id = reply_author.avatar_asset_id
|
||||
WHERE
|
||||
NOT thread.deleted
|
||||
AND NOT post.deleted
|
||||
|
@ -543,6 +549,7 @@ func CountPosts(
|
|||
JOIN handmade_thread AS thread ON post.thread_id = thread.id
|
||||
JOIN handmade_project AS project ON post.project_id = project.id
|
||||
LEFT JOIN auth_user AS author ON author.id = post.author_id
|
||||
LEFT JOIN handmade_asset AS author_avatar ON author_avatar.id = author.avatar_asset_id
|
||||
WHERE
|
||||
NOT thread.deleted
|
||||
AND NOT post.deleted
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/migration/types"
|
||||
"github.com/jackc/pgx/v4"
|
||||
)
|
||||
|
||||
func init() {
|
||||
registerMigration(UserAvatarAssetId{})
|
||||
}
|
||||
|
||||
type UserAvatarAssetId struct{}
|
||||
|
||||
func (m UserAvatarAssetId) Version() types.MigrationVersion {
|
||||
return types.MigrationVersion(time.Date(2021, 12, 26, 10, 16, 33, 0, time.UTC))
|
||||
}
|
||||
|
||||
func (m UserAvatarAssetId) Name() string {
|
||||
return "UserAvatarAssetId"
|
||||
}
|
||||
|
||||
func (m UserAvatarAssetId) Description() string {
|
||||
return "Add avatar_asset_id to users"
|
||||
}
|
||||
|
||||
func (m UserAvatarAssetId) Up(ctx context.Context, tx pgx.Tx) error {
|
||||
_, err := tx.Exec(ctx,
|
||||
`
|
||||
ALTER TABLE auth_user
|
||||
ADD COLUMN avatar_asset_id UUID REFERENCES handmade_asset (id) ON DELETE SET NULL;
|
||||
`,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m UserAvatarAssetId) Down(ctx context.Context, tx pgx.Tx) error {
|
||||
_, err := tx.Exec(ctx,
|
||||
`
|
||||
ALTER TABLE auth_user
|
||||
DROP COLUMN avatar_asset_id;
|
||||
`,
|
||||
)
|
||||
return err
|
||||
}
|
|
@ -3,6 +3,8 @@ package models
|
|||
import (
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
var UserType = reflect.TypeOf(User{})
|
||||
|
@ -29,11 +31,13 @@ type User struct {
|
|||
IsStaff bool `db:"is_staff"`
|
||||
Status UserStatus `db:"status"`
|
||||
|
||||
Name string `db:"name"`
|
||||
Bio string `db:"bio"`
|
||||
Blurb string `db:"blurb"`
|
||||
Signature string `db:"signature"`
|
||||
Avatar *string `db:"avatar"`
|
||||
Name string `db:"name"`
|
||||
Bio string `db:"bio"`
|
||||
Blurb string `db:"blurb"`
|
||||
Signature string `db:"signature"`
|
||||
Avatar *string `db:"avatar"`
|
||||
AvatarAssetID *uuid.UUID `db:"avatar_asset_id"`
|
||||
AvatarAsset *Asset `db:"avatar"`
|
||||
|
||||
DarkTheme bool `db:"darktheme"`
|
||||
Timezone string `db:"timezone"`
|
||||
|
|
|
@ -8,6 +8,7 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/hmndata"
|
||||
"git.handmade.network/hmn/hmn/src/hmnurl"
|
||||
"git.handmade.network/hmn/hmn/src/models"
|
||||
)
|
||||
|
@ -100,8 +101,10 @@ func ProjectToTemplate(
|
|||
}
|
||||
}
|
||||
|
||||
func (p *Project) AddLogo(logoUrl string) {
|
||||
p.Logo = logoUrl
|
||||
func ProjectAndStuffToTemplate(p *hmndata.ProjectAndStuff, url string, theme string) Project {
|
||||
res := ProjectToTemplate(&p.Project, url)
|
||||
res.Logo = ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset, theme)
|
||||
return res
|
||||
}
|
||||
|
||||
var ProjectLifecycleValues = map[models.ProjectLifecycle]string{
|
||||
|
@ -161,15 +164,23 @@ func ThreadToTemplate(t *models.Thread) Thread {
|
|||
}
|
||||
}
|
||||
|
||||
func UserAvatarDefaultUrl(currentTheme string) string {
|
||||
return hmnurl.BuildTheme("empty-avatar.svg", currentTheme, true)
|
||||
}
|
||||
|
||||
func UserAvatarUrl(u *models.User, currentTheme string) string {
|
||||
if currentTheme == "" {
|
||||
currentTheme = "light"
|
||||
}
|
||||
avatar := ""
|
||||
if u != nil && u.Avatar != nil && len(*u.Avatar) > 0 {
|
||||
avatar = hmnurl.BuildUserFile(*u.Avatar)
|
||||
if u != nil && (u.AvatarAsset != nil || (u.Avatar != nil && len(*u.Avatar) > 0)) {
|
||||
if u.AvatarAsset != nil {
|
||||
avatar = hmnurl.BuildS3Asset(u.AvatarAsset.S3Key)
|
||||
} else {
|
||||
avatar = hmnurl.BuildUserFile(*u.Avatar)
|
||||
}
|
||||
} else {
|
||||
avatar = hmnurl.BuildTheme("empty-avatar.svg", currentTheme, true)
|
||||
avatar = UserAvatarDefaultUrl(currentTheme)
|
||||
}
|
||||
return avatar
|
||||
}
|
||||
|
@ -178,7 +189,7 @@ func UserToTemplate(u *models.User, currentTheme string) User {
|
|||
if u == nil {
|
||||
return User{
|
||||
Name: "Deleted user",
|
||||
AvatarUrl: UserAvatarUrl(u, currentTheme),
|
||||
AvatarUrl: UserAvatarUrl(nil, currentTheme),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -200,8 +211,8 @@ func UserToTemplate(u *models.User, currentTheme string) User {
|
|||
Blurb: u.Blurb,
|
||||
Signature: u.Signature,
|
||||
DateJoined: u.DateJoined,
|
||||
AvatarUrl: UserAvatarUrl(u, currentTheme),
|
||||
ProfileUrl: hmnurl.BuildUserProfile(u.Username),
|
||||
AvatarUrl: UserAvatarUrl(u, currentTheme),
|
||||
|
||||
DarkTheme: u.DarkTheme,
|
||||
Timezone: u.Timezone,
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
{{ define "extrahead" }}
|
||||
<script src="{{ static "js/tabs.js" }}"></script>
|
||||
<script src="{{ static "js/image_selector.js" }}"></script>
|
||||
{{ end }}
|
||||
|
||||
{{ define "content" }}
|
||||
|
@ -41,12 +42,19 @@
|
|||
</div>
|
||||
<div class="edit-form-row">
|
||||
<div>Avatar:</div>
|
||||
<div>
|
||||
<input type="file" name="avatar" id="avatar">
|
||||
<div class="avatar-preview"><img src="{{ .User.AvatarUrl }}" width="200px">
|
||||
</div>
|
||||
<div class="user_avatar">
|
||||
{{ template "image_selector.html" imageselectordata "avatar" .User.AvatarUrl false }}
|
||||
<div class="c--dim f7">(Allowed image types: PNG, JPEG and GIF. Avatars may weigh up to 1MB and will be resized if larger than 400x400 pixels)</div>
|
||||
</div>
|
||||
<script>
|
||||
let avatarMaxFileSize = {{ .AvatarMaxFileSize }};
|
||||
let avatarSelector = new ImageSelector(
|
||||
document.querySelector("#user_form"),
|
||||
avatarMaxFileSize,
|
||||
document.querySelector(".user_avatar"),
|
||||
{{ .DefaultAvatarUrl }}
|
||||
);
|
||||
</script>
|
||||
</div>
|
||||
<div class="edit-form-row">
|
||||
<div class="pt-input-ns">Short bio:</div>
|
||||
|
@ -198,4 +206,4 @@
|
|||
}
|
||||
</script>
|
||||
</form>
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
|
|
|
@ -162,9 +162,15 @@ func AdminApprovalQueueSubmit(c *RequestContext) ResponseData {
|
|||
return RejectRequest(c, "User id can't be parsed")
|
||||
}
|
||||
|
||||
u, err := db.QueryOne(c.Context(), c.Conn, models.User{},
|
||||
type userQuery struct {
|
||||
User models.User `db:"auth_user"`
|
||||
}
|
||||
u, err := db.QueryOne(c.Context(), c.Conn, userQuery{},
|
||||
`
|
||||
SELECT $columns FROM auth_user WHERE id = $1
|
||||
SELECT $columns
|
||||
FROM auth_user
|
||||
LEFT JOIN handmade_asset AS auth_user_avatar ON auth_user_avatar.id = auth_user.avatar_asset_id
|
||||
WHERE auth_user.id = $1
|
||||
`,
|
||||
userId,
|
||||
)
|
||||
|
@ -175,7 +181,7 @@ func AdminApprovalQueueSubmit(c *RequestContext) ResponseData {
|
|||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user"))
|
||||
}
|
||||
}
|
||||
user := u.(*models.User)
|
||||
user := u.(*userQuery).User
|
||||
|
||||
whatHappened := ""
|
||||
if action == ApprovalQueueActionApprove {
|
||||
|
@ -241,6 +247,7 @@ func fetchUnapprovedPosts(c *RequestContext) ([]*UnapprovedPost, error) {
|
|||
JOIN handmade_thread AS thread ON post.thread_id = thread.id
|
||||
JOIN handmade_postversion AS ver ON ver.id = post.current_id
|
||||
JOIN auth_user AS author ON author.id = post.author_id
|
||||
LEFT JOIN handmade_asset AS author_avatar ON author_avatar.id = author.avatar_asset_id
|
||||
WHERE
|
||||
NOT thread.deleted
|
||||
AND NOT post.deleted
|
||||
|
|
|
@ -19,11 +19,15 @@ func APICheckUsername(c *RequestContext) ResponseData {
|
|||
requestedUsername := usernameArgs[0]
|
||||
found = true
|
||||
c.Perf.StartBlock("SQL", "Fetch user")
|
||||
userResult, err := db.QueryOne(c.Context(), c.Conn, models.User{},
|
||||
type userQuery struct {
|
||||
User models.User `db:"auth_user"`
|
||||
}
|
||||
userResult, err := db.QueryOne(c.Context(), c.Conn, userQuery{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM
|
||||
auth_user
|
||||
LEFT JOIN handmade_asset AS auth_user_avatar ON auth_user_avatar.id = auth_user.avatar_asset_id
|
||||
WHERE
|
||||
LOWER(auth_user.username) = LOWER($1)
|
||||
AND status = ANY ($2)
|
||||
|
@ -39,7 +43,7 @@ func APICheckUsername(c *RequestContext) ResponseData {
|
|||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user: %s", requestedUsername))
|
||||
}
|
||||
} else {
|
||||
canonicalUsername = userResult.(*models.User).Username
|
||||
canonicalUsername = userResult.(*userQuery).User.Username
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -75,7 +75,18 @@ func Login(c *RequestContext) ResponseData {
|
|||
return res
|
||||
}
|
||||
|
||||
userRow, err := db.QueryOne(c.Context(), c.Conn, models.User{}, "SELECT $columns FROM auth_user WHERE LOWER(username) = LOWER($1)", username)
|
||||
type userQuery struct {
|
||||
User models.User `db:"auth_user"`
|
||||
}
|
||||
userRow, err := db.QueryOne(c.Context(), c.Conn, userQuery{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM auth_user
|
||||
LEFT JOIN handmade_asset AS auth_user_avatar ON auth_user_avatar.id = auth_user.avatar_asset_id
|
||||
WHERE LOWER(username) = LOWER($1)
|
||||
`,
|
||||
username,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.NotFound) {
|
||||
return showLoginWithFailure(c, redirect)
|
||||
|
@ -83,7 +94,7 @@ func Login(c *RequestContext) ResponseData {
|
|||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to look up user by username"))
|
||||
}
|
||||
}
|
||||
user := userRow.(*models.User)
|
||||
user := &userRow.(*userQuery).User
|
||||
|
||||
success, err := tryLogin(c, user, password)
|
||||
|
||||
|
@ -446,10 +457,14 @@ func RequestPasswordResetSubmit(c *RequestContext) ResponseData {
|
|||
var user *models.User
|
||||
|
||||
c.Perf.StartBlock("SQL", "Fetching user")
|
||||
userRow, err := db.QueryOne(c.Context(), c.Conn, models.User{},
|
||||
type userQuery struct {
|
||||
User models.User `db:"auth_user"`
|
||||
}
|
||||
userRow, err := db.QueryOne(c.Context(), c.Conn, userQuery{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM auth_user
|
||||
LEFT JOIN handmade_asset AS auth_user_avatar ON auth_user_avatar.id = auth_user.avatar_asset_id
|
||||
WHERE
|
||||
LOWER(username) = LOWER($1)
|
||||
AND LOWER(email) = LOWER($2)
|
||||
|
@ -464,7 +479,7 @@ func RequestPasswordResetSubmit(c *RequestContext) ResponseData {
|
|||
}
|
||||
}
|
||||
if userRow != nil {
|
||||
user = userRow.(*models.User)
|
||||
user = &userRow.(*userQuery).User
|
||||
}
|
||||
|
||||
if user != nil {
|
||||
|
@ -776,6 +791,7 @@ func validateUsernameAndToken(c *RequestContext, username string, token string,
|
|||
`
|
||||
SELECT $columns
|
||||
FROM auth_user
|
||||
LEFT JOIN handmade_asset AS auth_user_avatar ON auth_user_avatar.id = auth_user.avatar_asset_id
|
||||
LEFT JOIN handmade_onetimetoken AS onetimetoken ON onetimetoken.owner_id = auth_user.id
|
||||
WHERE
|
||||
LOWER(auth_user.username) = LOWER($1)
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
"math"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
@ -76,8 +77,7 @@ func ProjectIndex(c *RequestContext) ResponseData {
|
|||
var restProjects []templates.Project
|
||||
now := time.Now()
|
||||
for _, p := range officialProjects {
|
||||
templateProject := templates.ProjectToTemplate(&p.Project, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
|
||||
templateProject.AddLogo(p.LogoURL(c.Theme))
|
||||
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage(), c.Theme)
|
||||
|
||||
if p.Project.Slug == "hero" {
|
||||
// NOTE(asaf): Handmade Hero gets special treatment. Must always be first in the list.
|
||||
|
@ -139,8 +139,7 @@ func ProjectIndex(c *RequestContext) ResponseData {
|
|||
if i >= maxPersonalProjects {
|
||||
break
|
||||
}
|
||||
templateProject := templates.ProjectToTemplate(&p.Project, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
|
||||
templateProject.AddLogo(p.LogoURL(c.Theme))
|
||||
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage(), c.Theme)
|
||||
personalProjects = append(personalProjects, templateProject)
|
||||
}
|
||||
}
|
||||
|
@ -244,6 +243,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
|||
handmade_post AS post
|
||||
INNER JOIN handmade_thread AS thread ON thread.id = post.thread_id
|
||||
INNER JOIN auth_user AS author ON author.id = post.author_id
|
||||
LEFT JOIN handmade_asset AS author_avatar ON author_avatar.id = author.avatar_asset_id
|
||||
WHERE
|
||||
post.project_id = $1
|
||||
ORDER BY post.postdate DESC
|
||||
|
@ -272,8 +272,7 @@ func ProjectHomepage(c *RequestContext) ResponseData {
|
|||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch project details"))
|
||||
}
|
||||
templateData.Project = templates.ProjectToTemplate(c.CurrentProject, c.UrlContext.BuildHomepage())
|
||||
templateData.Project.AddLogo(p.LogoURL(c.Theme))
|
||||
templateData.Project = templates.ProjectAndStuffToTemplate(&p, c.UrlContext.BuildHomepage(), c.Theme)
|
||||
for _, owner := range owners {
|
||||
templateData.Owners = append(templateData.Owners, templates.UserToTemplate(owner, c.Theme))
|
||||
}
|
||||
|
@ -514,11 +513,14 @@ func ProjectEdit(c *RequestContext) ResponseData {
|
|||
}
|
||||
c.Perf.EndBlock()
|
||||
|
||||
lightLogoUrl := templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset, "light")
|
||||
darkLogoUrl := templates.ProjectLogoUrl(&p.Project, p.LogoLightAsset, p.LogoDarkAsset, "dark")
|
||||
|
||||
projectSettings := templates.ProjectToProjectSettings(
|
||||
&p.Project,
|
||||
p.Owners,
|
||||
p.TagText(),
|
||||
p.LogoURL("light"), p.LogoURL("dark"),
|
||||
lightLogoUrl, darkLogoUrl,
|
||||
c.Theme,
|
||||
)
|
||||
|
||||
|
@ -816,10 +818,14 @@ func updateProject(ctx context.Context, tx pgx.Tx, user *models.User, payload *P
|
|||
}
|
||||
}
|
||||
|
||||
ownerRows, err := db.Query(ctx, tx, models.User{},
|
||||
type userQuery struct {
|
||||
User models.User `db:"auth_user"`
|
||||
}
|
||||
ownerRows, err := db.Query(ctx, tx, userQuery{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM auth_user
|
||||
LEFT JOIN handmade_asset AS auth_user_avatar ON auth_user_avatar.id = auth_user.avatar_asset_id
|
||||
WHERE LOWER(username) = ANY ($1)
|
||||
`,
|
||||
payload.OwnerUsernames,
|
||||
|
@ -847,7 +853,7 @@ func updateProject(ctx context.Context, tx pgx.Tx, user *models.User, payload *P
|
|||
VALUES
|
||||
($1, $2)
|
||||
`,
|
||||
ownerRow.(*models.User).ID,
|
||||
ownerRow.(*userQuery).User.ID,
|
||||
payload.ProjectID,
|
||||
)
|
||||
if err != nil {
|
||||
|
@ -914,13 +920,28 @@ func GetFormImage(c *RequestContext, fieldName string) (FormImage, error) {
|
|||
img.Read(res.Content)
|
||||
img.Seek(0, io.SeekStart)
|
||||
|
||||
config, _, err := image.DecodeConfig(img)
|
||||
if err != nil {
|
||||
return FormImage{}, err
|
||||
fileExtensionOverrides := []string{".svg"}
|
||||
fileExt := strings.ToLower(path.Ext(res.Filename))
|
||||
tryDecode := true
|
||||
for _, ext := range fileExtensionOverrides {
|
||||
if fileExt == ext {
|
||||
tryDecode = false
|
||||
}
|
||||
}
|
||||
|
||||
if tryDecode {
|
||||
config, _, err := image.DecodeConfig(img)
|
||||
if err != nil {
|
||||
return FormImage{}, err
|
||||
}
|
||||
res.Width = config.Width
|
||||
res.Height = config.Height
|
||||
res.Mime = http.DetectContentType(res.Content)
|
||||
} else {
|
||||
if fileExt == ".svg" {
|
||||
res.Mime = "image/svg+xml"
|
||||
}
|
||||
}
|
||||
res.Width = config.Width
|
||||
res.Height = config.Height
|
||||
res.Mime = http.DetectContentType(res.Content)
|
||||
}
|
||||
|
||||
return res, nil
|
||||
|
|
|
@ -501,20 +501,7 @@ func LoadCommonWebsiteData(c *RequestContext) (bool, ResponseData) {
|
|||
panic("failed to load project data")
|
||||
}
|
||||
|
||||
canEditProject := false
|
||||
if c.CurrentUser != nil {
|
||||
if c.CurrentUser.IsStaff {
|
||||
canEditProject = true
|
||||
} else {
|
||||
for _, o := range owners {
|
||||
if o.ID == c.CurrentUser.ID {
|
||||
canEditProject = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
c.CurrentUserCanEditCurrentProject = canEditProject
|
||||
c.CurrentUserCanEditCurrentProject = CanEditProject(c.CurrentUser, owners)
|
||||
|
||||
c.UrlContext = hmndata.UrlContextForProject(c.CurrentProject)
|
||||
}
|
||||
|
@ -557,7 +544,18 @@ func getCurrentUserAndSession(c *RequestContext, sessionId string) (*models.User
|
|||
}
|
||||
}
|
||||
|
||||
userRow, err := db.QueryOne(c.Context(), c.Conn, models.User{}, "SELECT $columns FROM auth_user WHERE username = $1", session.Username)
|
||||
type userQuery struct {
|
||||
User models.User `db:"auth_user"`
|
||||
}
|
||||
userRow, err := db.QueryOne(c.Context(), c.Conn, userQuery{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM auth_user
|
||||
LEFT JOIN handmade_asset AS auth_user_avatar ON auth_user_avatar.id = auth_user.avatar_asset_id
|
||||
WHERE username = $1
|
||||
`,
|
||||
session.Username,
|
||||
)
|
||||
if err != nil {
|
||||
if errors.Is(err, db.NotFound) {
|
||||
logging.Debug().Str("username", session.Username).Msg("returning no current user for this request because the user for the session couldn't be found")
|
||||
|
@ -566,7 +564,7 @@ func getCurrentUserAndSession(c *RequestContext, sessionId string) (*models.User
|
|||
return nil, nil, oops.New(err, "failed to get user for session")
|
||||
}
|
||||
}
|
||||
user := userRow.(*models.User)
|
||||
user := &userRow.(*userQuery).User
|
||||
|
||||
return user, session, nil
|
||||
}
|
||||
|
|
|
@ -2,13 +2,12 @@ package website
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.handmade.network/hmn/hmn/src/assets"
|
||||
"git.handmade.network/hmn/hmn/src/auth"
|
||||
"git.handmade.network/hmn/hmn/src/config"
|
||||
"git.handmade.network/hmn/hmn/src/db"
|
||||
|
@ -19,6 +18,7 @@ import (
|
|||
"git.handmade.network/hmn/hmn/src/models"
|
||||
"git.handmade.network/hmn/hmn/src/oops"
|
||||
"git.handmade.network/hmn/hmn/src/templates"
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v4"
|
||||
)
|
||||
|
||||
|
@ -52,11 +52,15 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
profileUser = c.CurrentUser
|
||||
} else {
|
||||
c.Perf.StartBlock("SQL", "Fetch user")
|
||||
userResult, err := db.QueryOne(c.Context(), c.Conn, models.User{},
|
||||
type userQuery struct {
|
||||
User models.User `db:"auth_user"`
|
||||
}
|
||||
userResult, err := db.QueryOne(c.Context(), c.Conn, userQuery{},
|
||||
`
|
||||
SELECT $columns
|
||||
FROM
|
||||
auth_user
|
||||
LEFT JOIN handmade_asset AS auth_user_avatar ON auth_user_avatar.id = auth_user.avatar_asset_id
|
||||
WHERE
|
||||
LOWER(auth_user.username) = $1
|
||||
`,
|
||||
|
@ -70,7 +74,7 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to fetch user: %s", username))
|
||||
}
|
||||
}
|
||||
profileUser = userResult.(*models.User)
|
||||
profileUser = &userResult.(*userQuery).User
|
||||
}
|
||||
|
||||
{
|
||||
|
@ -114,8 +118,7 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
templateProjects := make([]templates.Project, 0, len(projectsAndStuff))
|
||||
numPersonalProjects := 0
|
||||
for _, p := range projectsAndStuff {
|
||||
templateProject := templates.ProjectToTemplate(&p.Project, hmndata.UrlContextForProject(&p.Project).BuildHomepage())
|
||||
templateProject.AddLogo(p.LogoURL(c.Theme))
|
||||
templateProject := templates.ProjectAndStuffToTemplate(&p, hmndata.UrlContextForProject(&p.Project).BuildHomepage(), c.Theme)
|
||||
templateProjects = append(templateProjects, templateProject)
|
||||
|
||||
if p.Project.Personal {
|
||||
|
@ -201,12 +204,17 @@ func UserProfile(c *RequestContext) ResponseData {
|
|||
return res
|
||||
}
|
||||
|
||||
var UserAvatarMaxFileSize = 1 * 1024 * 1024
|
||||
|
||||
func UserSettings(c *RequestContext) ResponseData {
|
||||
var res ResponseData
|
||||
|
||||
type UserSettingsTemplateData struct {
|
||||
templates.BaseData
|
||||
|
||||
AvatarMaxFileSize int
|
||||
DefaultAvatarUrl string
|
||||
|
||||
User templates.User
|
||||
Email string // these fields are handled specially on templates.User
|
||||
ShowEmail bool
|
||||
|
@ -280,11 +288,13 @@ func UserSettings(c *RequestContext) ResponseData {
|
|||
baseData := getBaseDataAutocrumb(c, templateUser.Name)
|
||||
|
||||
res.MustWriteTemplate("user_settings.html", UserSettingsTemplateData{
|
||||
BaseData: baseData,
|
||||
User: templateUser,
|
||||
Email: c.CurrentUser.Email,
|
||||
ShowEmail: c.CurrentUser.ShowEmail,
|
||||
LinksText: linksText,
|
||||
BaseData: baseData,
|
||||
AvatarMaxFileSize: UserAvatarMaxFileSize,
|
||||
DefaultAvatarUrl: templates.UserAvatarDefaultUrl(c.Theme),
|
||||
User: templateUser,
|
||||
Email: c.CurrentUser.Email,
|
||||
ShowEmail: c.CurrentUser.ShowEmail,
|
||||
LinksText: linksText,
|
||||
|
||||
SubmitUrl: hmnurl.BuildUserSettings(""),
|
||||
ContactUrl: hmnurl.BuildContactPage(),
|
||||
|
@ -299,6 +309,14 @@ func UserSettings(c *RequestContext) ResponseData {
|
|||
}
|
||||
|
||||
func UserSettingsSave(c *RequestContext) ResponseData {
|
||||
maxBodySize := int64(UserAvatarMaxFileSize + 2*1024*1024)
|
||||
c.Req.Body = http.MaxBytesReader(c.Res, c.Req.Body, maxBodySize)
|
||||
err := c.Req.ParseMultipartForm(maxBodySize)
|
||||
if err != nil {
|
||||
// NOTE(asaf): The error for exceeding the max filesize doesn't have a special type, so we can't easily detect it here.
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to parse form"))
|
||||
}
|
||||
|
||||
tx, err := c.Conn.Begin(c.Context())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
@ -396,25 +414,39 @@ func UserSettingsSave(c *RequestContext) ResponseData {
|
|||
}
|
||||
|
||||
// Update avatar
|
||||
imageSaveResult := SaveImageFile(c, tx, "avatar", 1*1024*1024, fmt.Sprintf("members/avatars/%s-%d", c.CurrentUser.Username, time.Now().UTC().Unix()))
|
||||
if imageSaveResult.ValidationError != "" {
|
||||
return RejectRequest(c, imageSaveResult.ValidationError)
|
||||
} else if imageSaveResult.FatalError != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(imageSaveResult.FatalError, "failed to save new avatar"))
|
||||
} else if imageSaveResult.ImageFile != nil {
|
||||
_, err = tx.Exec(c.Context(),
|
||||
newAvatar, err := GetFormImage(c, "avatar")
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to read image from form"))
|
||||
}
|
||||
var avatarUUID *uuid.UUID
|
||||
if newAvatar.Exists {
|
||||
avatarAsset, err := assets.Create(c.Context(), tx, assets.CreateInput{
|
||||
Content: newAvatar.Content,
|
||||
Filename: newAvatar.Filename,
|
||||
ContentType: newAvatar.Mime,
|
||||
UploaderID: &c.CurrentUser.ID,
|
||||
Width: newAvatar.Width,
|
||||
Height: newAvatar.Height,
|
||||
})
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to upload avatar"))
|
||||
}
|
||||
avatarUUID = &avatarAsset.ID
|
||||
}
|
||||
if newAvatar.Exists || newAvatar.Remove {
|
||||
_, err := tx.Exec(c.Context(),
|
||||
`
|
||||
UPDATE auth_user
|
||||
SET
|
||||
avatar = $2
|
||||
avatar_asset_id = $2
|
||||
WHERE
|
||||
id = $1
|
||||
`,
|
||||
c.CurrentUser.ID,
|
||||
imageSaveResult.ImageFile.File,
|
||||
avatarUUID,
|
||||
)
|
||||
if err != nil {
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update user"))
|
||||
return c.ErrorResponse(http.StatusInternalServerError, oops.New(err, "failed to update user's avatar"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue