1
0
mirror of https://github.com/hrfee/jfa-go.git synced 2024-11-09 20:00:12 +00:00

Compare commits

...

3 Commits

Author SHA1 Message Date
Richard de Boer
46d1da7cd3 translation from Weblate (Dutch)
Currently translated at 100.0% (175 of 175 strings)

Translation: jfa-go/Admin Page
Translate-URL: https://weblate.jfa-go.com/projects/jfa-go/admin/nl/
2022-01-08 17:43:45 +01:00
77c05a4d4f
prefix css with version to avoid cache conflict 2022-01-08 16:42:36 +00:00
4024334c0c
accounts: add ability to label users
press the pen icon next to their username to add a nick-name to remind
you who they are.
2022-01-08 16:20:31 +00:00
19 changed files with 150 additions and 27 deletions

View File

@ -41,13 +41,14 @@ before:
- mv data/crash.html data/html/
- go get -u github.com/swaggo/swag/cmd/swag
- swag init -g main.go
- mv data/web/css/bundle.css data/web/css/{{.Env.JFA_GO_CSS_VERSION}}bundle.css
builds:
- id: notray
dir: ./
env:
- CGO_ENABLED=0
ldflags:
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary
- -s -w -X main.version={{.Env.JFA_GO_VERSION}} -X main.commit={{.ShortCommit}} -X main.updater=binary -X main.cssVersion={{.Env.JFA_GO_CSS_VERSION}}
goos:
- linux
- darwin

View File

@ -6,12 +6,14 @@ else
endif
GOBINARY ?= go
CSSVERSION ?= v3
VERSION ?= $(shell git describe --exact-match HEAD 2> /dev/null || echo vgit)
VERSION := $(shell echo $(VERSION) | sed 's/v//g')
COMMIT ?= $(shell git rev-parse --short HEAD || echo unknown)
UPDATER ?= off
LDFLAGS := -X main.version=$(VERSION) -X main.commit=$(COMMIT)
LDFLAGS := -X main.version=$(VERSION) -X main.commit=$(COMMIT) -X main.cssVersion=$(CSSVERSION)
ifeq ($(UPDATER), on)
LDFLAGS := $(LDFLAGS) -X main.updater=binary
else ifneq ($(UPDATER), off)
@ -154,6 +156,7 @@ copy:
$(info copying language files)
cp -r lang $(DATA)/
cp LICENSE $(DATA)/
mv $(DATA)/web/css/bundle.css $(DATA)/web/css/$(CSSVERSION)bundle.css
# internal-files:
# python3 scripts/embed.py internal

39
api.go
View File

@ -1469,6 +1469,7 @@ func (app *appContext) GetUsers(gc *gin.Context) {
if email, ok := app.storage.emails[jfUser.ID]; ok {
user.Email = email.Addr
user.NotifyThroughEmail = email.Contact
user.Label = email.Label
}
expiry, ok := app.storage.users[jfUser.ID]
if ok {
@ -1579,6 +1580,44 @@ func (app *appContext) DeleteOmbiProfile(gc *gin.Context) {
respondBool(204, true, gc)
}
// @Summary Modify user's labels, which show next to their name in the accounts tab.
// @Produce json
// @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to labels"
// @Success 204 {object} boolResponse
// @Failure 500 {object} boolResponse
// @Router /users/labels [post]
// @Security Bearer
// @tags Users
func (app *appContext) ModifyLabels(gc *gin.Context) {
var req modifyEmailsDTO
gc.BindJSON(&req)
app.debug.Println("Label modification requested")
users, status, err := app.jf.GetUsers(false)
if !(status == 200 || status == 204) || err != nil {
app.err.Printf("Failed to get users from Jellyfin (%d): %v", status, err)
respond(500, "Couldn't get users", gc)
return
}
for _, jfUser := range users {
id := jfUser.ID
if label, ok := req[id]; ok {
addr := ""
contact := true
if oldEmail, ok := app.storage.emails[id]; ok {
addr = oldEmail.Addr
contact = oldEmail.Contact
}
app.storage.emails[id] = EmailAddress{Addr: addr, Contact: contact, Label: label}
}
}
if err := app.storage.storeEmails(); err != nil {
app.err.Printf("Failed to store email list: %v", err)
respondBool(500, false, gc)
}
app.info.Println("Email list modified")
respondBool(204, true, gc)
}
// @Summary Modify user's email addresses.
// @Produce json
// @Param modifyEmailsDTO body modifyEmailsDTO true "Map of userIDs to email addresses"

2
go.mod
View File

@ -57,7 +57,7 @@ require (
github.com/writeas/go-strip-markdown v2.0.1+incompatible
github.com/xhit/go-simple-mail/v2 v2.10.0
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f // indirect
golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d // indirect
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
golang.org/x/tools v0.1.8 // indirect
google.golang.org/protobuf v1.27.1 // indirect

2
go.sum
View File

@ -329,6 +329,8 @@ golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f h1:hEYJvxw1lSnWIl8X9ofsYMklzaDs90JI2az5YMd4fPM=
golang.org/x/net v0.0.0-20211216030914-fe4d6282115f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d h1:62NvYBuaanGXR2ZOfwDFkhhl6X1DUgf8qg3GuQvxZsE=
golang.org/x/net v0.0.0-20220107192237-5cfca573fb4d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<head>
<link rel="stylesheet" type="text/css" href="css/bundle.css">
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
{{ template "header.html" . }}
<title>404 - jfa-go</title>
</head>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<head>
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/bundle.css">
<link rel="stylesheet" type="text/css" href="{{ .urlBase }}/css/{{ .cssVersion }}bundle.css">
<script>
window.URLBase = "{{ .urlBase }}";
window.notificationsEnabled = {{ .notifications }};
@ -340,15 +340,15 @@
<div class="modal-content wide card">
<span class="heading">{{ .strings.updates }} <span class="modal-close">&times;</span></span>
<p class="content">
<h2>
<h2 class="mt-2">
<a id="update-version"></a> (<span class="font-mono bg-inherit" id="update-commit"></span>)
</h2>
<p class="content" id="update-description"></p>
<p class="support" id="update-date"></p>
<div class="content markdown-box" id="update-changelog"></div>
<p class="content mt-2" id="update-description"></p>
<p class="support mt-2" id="update-date"></p>
<div class="content markdown-box mt-2" id="update-changelog"></div>
</p>
<span class="button ~info @low full-width center" id="update-download">{{ .strings.download }}</span>
<span class="button ~urge @low full-width center" id="update-update">{{ .strings.update }}</span>
<span class="button ~info @low full-width center mt-2" id="update-download">{{ .strings.download }}</span>
<span class="button ~urge @low full-width center mt-2" id="update-update">{{ .strings.update }}</span>
</div>
</div>
{{ if .telegramEnabled }}

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<head>
<link rel="stylesheet" type="text/css" href="css/bundle.css">
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
{{ template "header.html" . }}
<title>{{ .strings.successHeader }} - jfa-go</title>
</head>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<head>
<link rel="stylesheet" type="text/css" href="css/bundle.css">
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
{{ template "header.html" . }}
<title>
{{ if .passwordReset }}

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<head>
<link rel="stylesheet" type="text/css" href="css/bundle.css">
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
{{ template "header.html" . }}
<title>Invalid Code - jfa-go</title>
</head>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en" class="{{ .cssClass }}">
<head>
<link rel="stylesheet" type="text/css" href="css/bundle.css">
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
{{ template "header.html" . }}
<title>{{ .strings.passwordReset }} - jfa-go</title>
</head>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html lang="en" class="light">
<head>
<link rel="stylesheet" type="text/css" href="css/bundle.css">
<link rel="stylesheet" type="text/css" href="css/{{ .cssVersion }}bundle.css">
{{ template "header.html" . }}
<title>{{ .lang.Strings.pageTitle }}</title>
</head>

View File

@ -111,7 +111,8 @@
"sendPWR": "Verstuur wachtwoordreset",
"sendPWRSuccess": "Wachtwoordreset-link verstuurd.",
"sendPWRValidFor": "De link is 30m geldig.",
"ombiProfile": "Ombi gebruikersprofiel"
"ombiProfile": "Ombi gebruikersprofiel",
"logs": "Logs"
},
"notifications": {
"changedEmailAddress": "E-mailadres van {n} gewijzigd.",

View File

@ -145,6 +145,7 @@ type respUser struct {
NotifyThroughDiscord bool `json:"notify_discord"`
Matrix string `json:"matrix"` // Matrix ID (if known)
NotifyThroughMatrix bool `json:"notify_matrix"`
Label string `json:"label"` // Label of user, shown next to their name.
}
type getUsersDTO struct {

View File

@ -160,6 +160,7 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.DELETE(p+"/profiles", app.DeleteProfile)
api.POST(p+"/invites/notify", app.SetNotify)
api.POST(p+"/users/emails", app.ModifyEmails)
api.POST(p+"/users/labels", app.ModifyLabels)
// api.POST(p + "/setDefaults", app.SetDefaults)
api.POST(p+"/users/settings", app.ApplySettings)
api.POST(p+"/users/announce", app.Announce)

View File

@ -2,4 +2,4 @@
# sets version environment variable for goreleaser to use
# scripts/version.sh goreleaser ...
JFA_GO_VERSION=$(git describe --exact-match HEAD 2> /dev/null || echo 'vgit')
JFA_GO_NFPM_EPOCH=$(git rev-list --all --count) JFA_GO_VERSION="$(echo $JFA_GO_VERSION | sed 's/v//g')" $@
JFA_GO_CSS_VERSION="v3" JFA_GO_NFPM_EPOCH=$(git rev-list --all --count) JFA_GO_VERSION="$(echo $JFA_GO_VERSION | sed 's/v//g')" $@

View File

@ -52,6 +52,7 @@ type DiscordUser struct {
type EmailAddress struct {
Addr string
Label string // User Label.
Contact bool
}

View File

@ -20,6 +20,7 @@ interface User {
discord_id: string;
matrix: string;
notify_matrix: boolean;
label: string;
}
interface getPinResponse {
@ -60,6 +61,9 @@ class user implements User {
private _lastActive: HTMLTableDataCellElement;
private _lastActiveUnix: number;
private _notifyDropdown: HTMLDivElement;
private _label: HTMLInputElement;
private _userLabel: string;
private _labelEditButton: HTMLElement;
id = "";
private _selected: boolean;
@ -380,6 +384,19 @@ class user implements User {
}
}
get label(): string { return this._userLabel; }
set label(l: string) {
console.log(l);
this._userLabel = l ? l : "";
this._label.innerHTML = l ? l : "";
this._labelEditButton.classList.add("ri-edit-line");
this._labelEditButton.classList.remove("ri-check-line");
if (!l) {
this._label.classList.remove("chip", "~gray");
} else {
this._label.classList.add("chip", "~gray", "mr-2");
}
}
private _checkEvent = new CustomEvent("accountCheckEvent");
private _uncheckEvent = new CustomEvent("accountUncheckEvent");
@ -387,7 +404,7 @@ class user implements User {
this._row = document.createElement("tr") as HTMLTableRowElement;
let innerHTML = `
<td><input type="checkbox" value=""></td>
<td><div class="table-inline"><span class="accounts-username py-2"></span> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></span></td>
<td><div class="table-inline"><span class="accounts-username py-2 mr-2"></span><span class="accounts-label-container ml-2"></span> <i class="icon ri-edit-line accounts-label-edit"></i> <span class="accounts-admin"></span> <span class="accounts-disabled"></span></span></div></td>
<td><div class="table-inline"><i class="icon ri-edit-line accounts-email-edit"></i><span class="accounts-email-container ml-2"></span></div></td>
`;
if (window.telegramEnabled) {
@ -411,6 +428,7 @@ class user implements User {
`;
this._row.innerHTML = innerHTML;
const emailEditor = `<input type="email" class="input ~neutral @low stealth-input">`;
const labelEditor = `<input type="text" class="field ~neutral @low stealth-input">`;
this._check = this._row.querySelector("input[type=checkbox]") as HTMLInputElement;
this._username = this._row.querySelector(".accounts-username") as HTMLSpanElement;
this._admin = this._row.querySelector(".accounts-admin") as HTMLSpanElement;
@ -422,11 +440,13 @@ class user implements User {
this._matrix = this._row.querySelector(".accounts-matrix") as HTMLTableDataCellElement;
this._expiry = this._row.querySelector(".accounts-expiry") as HTMLTableDataCellElement;
this._lastActive = this._row.querySelector(".accounts-last-active") as HTMLTableDataCellElement;
this._label = this._row.querySelector(".accounts-label-container") as HTMLInputElement;
this._labelEditButton = this._row.querySelector(".accounts-label-edit") as HTMLElement;
this._check.onchange = () => { this.selected = this._check.checked; }
this._notifyDropdown = this._constructDropdown();
const toggleStealthInput = () => {
const toggleEmailInput = () => {
if (this._emailEditButton.classList.contains("ri-edit-line")) {
this._email.innerHTML = emailEditor;
this._email.querySelector("input").value = this._emailAddress;
@ -438,21 +458,52 @@ class user implements User {
this._emailEditButton.classList.toggle("ri-check-line");
this._emailEditButton.classList.toggle("ri-edit-line");
};
const outerClickListener = (event: Event) => {
const emailClickListener = (event: Event) => {
if (!(event.target instanceof HTMLElement && (this._email.contains(event.target) || this._emailEditButton.contains(event.target)))) {
toggleStealthInput();
toggleEmailInput();
this.email = this.email;
document.removeEventListener("click", outerClickListener);
document.removeEventListener("click", emailClickListener);
}
};
this._emailEditButton.onclick = () => {
if (this._emailEditButton.classList.contains("ri-edit-line")) {
document.addEventListener('click', outerClickListener);
document.addEventListener('click', emailClickListener);
} else {
this._updateEmail();
document.removeEventListener('click', outerClickListener);
document.removeEventListener('click', emailClickListener);
}
toggleStealthInput();
toggleEmailInput();
};
const toggleLabelInput = () => {
if (this._labelEditButton.classList.contains("ri-edit-line")) {
this._label.innerHTML = labelEditor;
const input = this._label.querySelector("input");
input.value = this._userLabel;
input.placeholder = window.lang.strings("label");
this._label.classList.remove("ml-2");
this._labelEditButton.classList.add("ri-check-line");
this._labelEditButton.classList.remove("ri-edit-line");
} else {
this._updateLabel();
this._email.classList.add("ml-2");
}
};
const labelClickListener = (event: Event) => {
if (!(event.target instanceof HTMLElement && (this._label.contains(event.target) || this._labelEditButton.contains(event.target)))) {
toggleLabelInput();
document.removeEventListener("click", labelClickListener);
}
};
this._labelEditButton.onclick = () => {
if (this._labelEditButton.classList.contains("ri-edit-line")) {
document.addEventListener('click', labelClickListener);
} else {
document.removeEventListener('click', labelClickListener);
}
toggleLabelInput();
};
this.update(user);
@ -462,6 +513,21 @@ class user implements User {
this.last_active = this.last_active;
});
}
private _updateLabel = () => {
let oldLabel = this.label;
this.label = this._label.querySelector("input").value;
let send = {};
send[this.id] = this.label;
_post("/users/labels", send, (req: XMLHttpRequest) => {
if (req.readyState == 4) {
if (req.status != 204) {
this.label = oldLabel;
window.notifications.customError("labelChanged", window.lang.notif("errorUnknown"));
}
}
});
};
private _updateEmail = () => {
let oldEmail = this.email;
@ -544,6 +610,7 @@ class user implements User {
this.notify_matrix = user.notify_matrix;
this.notify_email = user.notify_email;
this.discord_id = user.discord_id;
this.label = user.label;
}
asElement = (): HTMLTableRowElement => { return this._row; }

View File

@ -12,7 +12,8 @@ import (
"github.com/hrfee/mediabrowser"
)
var css = []string{"bundle.css", "remixicon.css"}
var cssVersion string
var css = []string{cssVersion + "bundle.css", "remixicon.css"}
var cssHeader string
func (app *appContext) loadCSSHeader() string {
@ -119,6 +120,7 @@ func (app *appContext) AdminPage(gc *gin.Context) {
gcHTML(gc, http.StatusOK, "admin.html", gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": "",
"emailEnabled": emailEnabled,
"telegramEnabled": telegramEnabled,
@ -151,6 +153,7 @@ func (app *appContext) ResetPassword(gc *gin.Context) {
data := gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
"strings": app.storage.lang.PasswordReset[lang].Strings,
"success": false,
@ -255,6 +258,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
if !ok {
gcHTML(gc, 404, "invalidCode.html", gin.H{
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
return
@ -272,6 +276,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
fail := func() {
gcHTML(gc, 404, "404.html", gin.H{
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}
@ -334,6 +339,7 @@ func (app *appContext) InviteProxy(gc *gin.Context) {
data := gin.H{
"urlBase": app.getURLBase(gc),
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
"helpMessage": app.config.Section("ui").Key("help_message").String(),
"successMessage": app.config.Section("ui").Key("success_message").String(),
@ -393,6 +399,7 @@ func (app *appContext) NoRouteHandler(gc *gin.Context) {
app.pushResources(gc, false)
gcHTML(gc, 404, "404.html", gin.H{
"cssClass": app.cssClass,
"cssVersion": cssVersion,
"contactMessage": app.config.Section("ui").Key("contact_message").String(),
})
}