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:
parent
76a7cf7e80
commit
5df0f48303
@ -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) |
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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'))
|
||||||
|
@ -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
102
static/assets/vendor/tailwind.dist.css
vendored
102
static/assets/vendor/tailwind.dist.css
vendored
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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>
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user