1
0
mirror of https://github.com/muety/wakapi.git synced 2023-08-10 21:12:56 +03:00

feat: user avatars

This commit is contained in:
Ferdinand Mütsch 2021-10-14 12:01:06 +02:00
parent 76a7cf7e80
commit 5df0f48303
11 changed files with 140 additions and 43 deletions

View File

@ -145,6 +145,7 @@ You can specify configuration options either via a config file (default: `config
|---------------------------|---------------------------|--------------|---------------------------------------------------------------------| |---------------------------|---------------------------|--------------|---------------------------------------------------------------------|
| `env` | `ENVIRONMENT` | `dev` | Whether to use development- or production settings | | `env` | `ENVIRONMENT` | `dev` | Whether to use development- or production settings |
| `app.custom_languages` | - | - | Map from file endings to language names | | `app.custom_languages` | - | - | Map from file endings to language names |
| `app.avatar_url_template` | - | (see [`config.default.yml`](config.default.yml)) | URL template for external user avatar images (e.g. from [Dicebear](https://dicebear.com) or [Gravatar](https://gravatar.com)) |
| `server.port` | `WAKAPI_PORT` | `3000` | Port to listen on | | `server.port` | `WAKAPI_PORT` | `3000` | Port to listen on |
| `server.listen_ipv4` | `WAKAPI_LISTEN_IPV4` | `127.0.0.1` | IPv4 network address to listen on (leave blank to disable IPv4) | | `server.listen_ipv4` | `WAKAPI_LISTEN_IPV4` | `127.0.0.1` | IPv4 network address to listen on (leave blank to disable IPv4) |
| `server.listen_ipv6` | `WAKAPI_LISTEN_IPV6` | `::1` | IPv6 network address to listen on (leave blank to disable IPv6) | | `server.listen_ipv6` | `WAKAPI_LISTEN_IPV6` | `::1` | IPv6 network address to listen on (leave blank to disable IPv6) |

View File

@ -21,6 +21,10 @@ app:
jsx: JSX jsx: JSX
svelte: Svelte svelte: Svelte
# url template for user avatar images (to be used with services like gravatar or dicebear)
# available variable placeholders are: username, username_hash, email, email_hash
avatar_url_template: https://avatars.dicebear.com/api/pixel-art-neutral/{username_hash}.svg
db: db:
host: # leave blank when using sqlite3 host: # leave blank when using sqlite3
port: # leave blank when using sqlite3 port: # leave blank when using sqlite3

View File

@ -62,14 +62,15 @@ var cFlag = flag.String("config", defaultConfigPath, "config file location")
var env string var env string
type appConfig struct { type appConfig struct {
AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"` AggregationTime string `yaml:"aggregation_time" default:"02:15" env:"WAKAPI_AGGREGATION_TIME"`
ReportTimeWeekly string `yaml:"report_time_weekly" default:"fri,18:00" env:"WAKAPI_REPORT_TIME_WEEKLY"` ReportTimeWeekly string `yaml:"report_time_weekly" default:"fri,18:00" env:"WAKAPI_REPORT_TIME_WEEKLY"`
ImportBackoffMin int `yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN"` ImportBackoffMin int `yaml:"import_backoff_min" default:"5" env:"WAKAPI_IMPORT_BACKOFF_MIN"`
ImportBatchSize int `yaml:"import_batch_size" default:"50" env:"WAKAPI_IMPORT_BATCH_SIZE"` ImportBatchSize int `yaml:"import_batch_size" default:"50" env:"WAKAPI_IMPORT_BATCH_SIZE"`
InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"` InactiveDays int `yaml:"inactive_days" default:"7" env:"WAKAPI_INACTIVE_DAYS"`
CountCacheTTLMin int `yaml:"count_cache_ttl_min" default:"30" env:"WAKAPI_COUNT_CACHE_TTL_MIN"` CountCacheTTLMin int `yaml:"count_cache_ttl_min" default:"30" env:"WAKAPI_COUNT_CACHE_TTL_MIN"`
CustomLanguages map[string]string `yaml:"custom_languages"` AvatarURLTemplate string `yaml:"avatar_url_template" default:"https://avatars.dicebear.com/api/pixel-art-neutral/{username_hash}.svg"`
Colors map[string]map[string]string `yaml:"-"` CustomLanguages map[string]string `yaml:"custom_languages"`
Colors map[string]map[string]string `yaml:"-"`
} }
type securityConfig struct { type securityConfig struct {

View File

@ -53,6 +53,7 @@ type SummaryViewModel struct {
*Summary *Summary
*SummaryParams *SummaryParams
User *User User *User
AvatarURL string
LanguageColors map[string]string LanguageColors map[string]string
EditorColors map[string]string EditorColors map[string]string
OSColors map[string]string OSColors map[string]string

View File

@ -1,7 +1,10 @@
package models package models
import ( import (
"crypto/md5"
"fmt"
"regexp" "regexp"
"strings"
"time" "time"
) )
@ -92,6 +95,18 @@ func (u *User) TZOffset() time.Duration {
return time.Duration(offset * int(time.Second)) return time.Duration(offset * int(time.Second))
} }
func (u *User) AvatarURL(urlTemplate string) string {
urlTemplate = strings.ReplaceAll(urlTemplate, "{username}", u.ID)
urlTemplate = strings.ReplaceAll(urlTemplate, "{email}", u.Email)
if strings.Contains(urlTemplate, "{username_hash}") {
urlTemplate = strings.ReplaceAll(urlTemplate, "{username_hash}", fmt.Sprintf("%x", md5.Sum([]byte(u.ID))))
}
if strings.Contains(urlTemplate, "{email_hash}") {
urlTemplate = strings.ReplaceAll(urlTemplate, "{email_hash}", fmt.Sprintf("%x", md5.Sum([]byte(u.Email))))
}
return urlTemplate
}
func (c *CredentialsReset) IsValid() bool { func (c *CredentialsReset) IsValid() bool {
return ValidatePassword(c.PasswordNew) && return ValidatePassword(c.PasswordNew) &&
c.PasswordNew == c.PasswordRepeat c.PasswordNew == c.PasswordRepeat

View File

@ -52,6 +52,9 @@ func DefaultTemplateFuncs() template.FuncMap {
"htmlSafe": func(html string) template.HTML { "htmlSafe": func(html string) template.HTML {
return template.HTML(html) return template.HTML(html)
}, },
"avatarUrlTemplate": func() string {
return config.Get().App.AvatarURLTemplate
},
} }
} }

View File

@ -32,6 +32,8 @@ let icons = [
'twemoji:gear', 'twemoji:gear',
'eva:corner-right-down-fill', 'eva:corner-right-down-fill',
'bi:heart-fill', 'bi:heart-fill',
'fxemoji:running',
'ic:round-person'
] ]
const output = path.normalize(path.join(__dirname, '../static/assets/icons.js')) const output = path.normalize(path.join(__dirname, '../static/assets/icons.js'))

View File

@ -420,6 +420,13 @@ function hexToRgb(hex) {
} : null; } : null;
} }
function showUserMenuPopup(event) {
const el = document.getElementById('user-menu-popup')
el.classList.remove('hidden')
el.classList.add('block')
event.stopPropagation()
}
function showApiKeyPopup(event) { function showApiKeyPopup(event) {
const el = document.getElementById('api-key-popup') const el = document.getElementById('api-key-popup')
el.classList.remove('hidden') el.classList.remove('hidden')

File diff suppressed because one or more lines are too long

View File

@ -9,8 +9,10 @@
*/ */
html { html {
line-height: 1.15; /* 1 */ line-height: 1.15;
-webkit-text-size-adjust: 100%; /* 2 */ /* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
} }
/* Sections /* Sections
@ -51,9 +53,12 @@ h1 {
*/ */
hr { hr {
box-sizing: content-box; /* 1 */ box-sizing: content-box;
height: 0; /* 1 */ /* 1 */
overflow: visible; /* 2 */ height: 0;
/* 1 */
overflow: visible;
/* 2 */
} }
/** /**
@ -62,8 +67,10 @@ hr {
*/ */
pre { pre {
font-family: monospace, monospace; /* 1 */ font-family: monospace, monospace;
font-size: 1em; /* 2 */ /* 1 */
font-size: 1em;
/* 2 */
} }
/* Text-level semantics /* Text-level semantics
@ -83,10 +90,13 @@ a {
*/ */
abbr[title] { abbr[title] {
border-bottom: none; /* 1 */ border-bottom: none;
text-decoration: underline; /* 2 */ /* 1 */
text-decoration: underline;
/* 2 */
-webkit-text-decoration: underline dotted; -webkit-text-decoration: underline dotted;
text-decoration: underline dotted; /* 2 */ text-decoration: underline dotted;
/* 2 */
} }
/** /**
@ -106,8 +116,10 @@ strong {
code, code,
kbd, kbd,
samp { samp {
font-family: monospace, monospace; /* 1 */ font-family: monospace, monospace;
font-size: 1em; /* 2 */ /* 1 */
font-size: 1em;
/* 2 */
} }
/** /**
@ -163,10 +175,14 @@ input,
optgroup, optgroup,
select, select,
textarea { textarea {
font-family: inherit; /* 1 */ font-family: inherit;
font-size: 100%; /* 1 */ /* 1 */
line-height: 1.15; /* 1 */ font-size: 100%;
margin: 0; /* 2 */ /* 1 */
line-height: 1.15;
/* 1 */
margin: 0;
/* 2 */
} }
/** /**
@ -175,7 +191,8 @@ textarea {
*/ */
button, button,
input { /* 1 */ input {
/* 1 */
overflow: visible; overflow: visible;
} }
@ -185,7 +202,8 @@ input { /* 1 */
*/ */
button, button,
select { /* 1 */ select {
/* 1 */
text-transform: none; text-transform: none;
} }
@ -239,12 +257,18 @@ fieldset {
*/ */
legend { legend {
box-sizing: border-box; /* 1 */ box-sizing: border-box;
color: inherit; /* 2 */ /* 1 */
display: table; /* 1 */ color: inherit;
max-width: 100%; /* 1 */ /* 2 */
padding: 0; /* 3 */ display: table;
white-space: normal; /* 1 */ /* 1 */
max-width: 100%;
/* 1 */
padding: 0;
/* 3 */
white-space: normal;
/* 1 */
} }
/** /**
@ -394,8 +418,10 @@ ul {
*/ */
html { html {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; /* 1 */ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
line-height: 1.5; /* 2 */ /* 1 */
line-height: 1.5;
/* 2 */
} }
/** /**
@ -427,10 +453,14 @@ html {
*, *,
::before, ::before,
::after { ::after {
box-sizing: border-box; /* 1 */ box-sizing: border-box;
border-width: 0; /* 2 */ /* 1 */
border-style: solid; /* 2 */ border-width: 0;
border-color: #e2e8f0; /* 2 */ /* 2 */
border-style: solid;
/* 2 */
border-color: #e2e8f0;
/* 2 */
} }
/* /*
@ -723,6 +753,10 @@ video {
border-radius: 9999px; border-radius: 9999px;
} }
.border-2 {
border-width: 2px;
}
.border { .border {
border-width: 1px; border-width: 1px;
} }
@ -831,6 +865,10 @@ video {
font-weight: 400; font-weight: 400;
} }
.font-medium {
font-weight: 500;
}
.font-semibold { .font-semibold {
font-weight: 600; font-weight: 600;
} }
@ -999,6 +1037,10 @@ video {
margin-top: 4rem; margin-top: 4rem;
} }
.mt-24 {
margin-top: 6rem;
}
.-ml-1 { .-ml-1 {
margin-left: -0.25rem; margin-left: -0.25rem;
} }

View File

@ -19,6 +19,24 @@
</div> </div>
</div> </div>
<div class="hidden flex bg-gray-800 shadow-md z-10 p-2 absolute top-0 right-0 mt-10 mr-8 border border-green-700 rounded popup mt-24"
id="user-menu-popup" style="min-width: 200px;">
<div class="flex-grow flex flex-col px-2">
<div class="flex flex-col text-xs text-gray-300 mx-1 mb-4 items-center">
<span class="font-semibold">{{ .User.ID }}</span>
{{ if .User.Email }}
<span>({{ .User.Email }})</span>
{{ end }}
</div>
<form action="logout" method="post" class="flex-grow">
<button type="submit" class="py-1 px-3 h-8 rounded bg-green-700 text-white text-sm w-full">
<span>Logout</span>
<span class="iconify inline" data-icon="fxemoji:running"></span>
</button>
</form>
</div>
</div>
<div class="absolute flex top-0 right-0 mr-8 mt-10 py-2"> <div class="absolute flex top-0 right-0 mr-8 mt-10 py-2">
<div class="mx-1"> <div class="mx-1">
<button type="button" class="py-1 px-3 h-8 rounded border border-green-700 text-white text-sm" <button type="button" class="py-1 px-3 h-8 rounded border border-green-700 text-white text-sm"
@ -30,10 +48,12 @@
<span class="iconify inline" data-icon="twemoji:gear"></span> <span class="iconify inline" data-icon="twemoji:gear"></span>
</a> </a>
</div> </div>
<div class="mx-1"> <div class="mx-1 flex items-center">
<form action="logout" method="post"> {{ if avatarUrlTemplate }}
<button type="submit" class="py-1 px-3 h-8 rounded border border-green-700 text-white text-sm">Logout</button> <img src="{{ .User.AvatarURL avatarUrlTemplate }}" width="32px" class="rounded-full border-2 border-green-700 cursor-pointer" onclick="showUserMenuPopup(event)" alt="User Profile Avatar" title="Looks like you, doesn't it?"/>
</form> {{ else }}
<span class="iconify inline cursor-pointer text-gray-500 rounded-full border-2 border-green-700" style="width: 32px; height: 32px" data-icon="ic:round-person" onclick="showUserMenuPopup(event)"></span>
{{ end }}
</div> </div>
</div> </div>