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

Compare commits

...

5 Commits

Author SHA1 Message Date
3fa4b01115
setup: add user page
also sprinkled mentions of it throughout other relevant pages.
2023-06-26 21:29:49 +01:00
65f402fd35
admin: hide my account button when disabled 2023-06-26 20:48:57 +01:00
46f1bc20c8
css: fix font error
comment wasn't ended, so some font weights/styles weren't loading and
esbuild was complaining.
2023-06-26 20:29:59 +01:00
a13a72c626
admin: fix logout when url base is used
two tries are made, with and without the url base.
2023-06-26 20:28:20 +01:00
5a80145607
css: add notification animation
simple slide animation, plus a little scale effect when a duplicate
notification gets sent to make the notification more obvious.
2023-06-26 20:13:02 +01:00
10 changed files with 113 additions and 23 deletions

View File

@ -22,7 +22,7 @@
font-family: 'Hanken Grotesk'; font-family: 'Hanken Grotesk';
font-style: italic; font-style: italic;
font-weight: 500; font-weight: 500;
src: url('../fonts/hanken-grotesk-v8-cyrillic-ext_latin_vietnamese-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ * src: url('../fonts/hanken-grotesk-v8-cyrillic-ext_latin_vietnamese-500italic.woff2') format('woff2'); /* Chrome 36+, Opera 23+, Firefox 39+, Safari 12+, iOS 10+ */
} }
/* hanken-grotesk-700 - cyrillic-ext_latin_vietnamese */ /* hanken-grotesk-700 - cyrillic-ext_latin_vietnamese */

View File

@ -417,9 +417,11 @@
</span> </span>
<span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span> <span class="button ~warning" alt="{{ .strings.theme }}" id="button-theme"><i class="ri-sun-line"></i></span>
</div> </div>
<div class="top-4 right-4 absolute"> {{ if .userPageEnabled }}
<a class="button ~info" href="/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a> <div class="top-4 right-4 absolute">
</div> <a class="button ~info" href="/my/account"><i class="ri-account-circle-fill mr-2"></i>{{ .strings.myAccount }}</a>
</div>
{{ end }}
<div class="page-container"> <div class="page-container">
<div class="mb-4"> <div class="mb-4">
<header class="flex flex-wrap items-center justify-between"> <header class="flex flex-wrap items-center justify-between">

View File

@ -146,6 +146,7 @@
<label class="row switch pb-4"> <label class="row switch pb-4">
<input type="radio" class="mr-2" name="ui-jellyfin_login" value="false"><span>{{ .lang.Login.authorizeManual }}</span> <input type="radio" class="mr-2" name="ui-jellyfin_login" value="false"><span>{{ .lang.Login.authorizeManual }}</span>
</label> </label>
<p class="support pb-4 pl-4 mt-1">{{ .lang.Login.authorizeManualUserPageNotice }}</p>
</div> </div>
<div id="login-manual"> <div id="login-manual">
<label class="label"> <label class="label">
@ -238,6 +239,21 @@
</div> </div>
</section> </section>
</div> </div>
<div class="card ~neutral @low mb-2 unfocused">
<span class="heading">{{ .lang.UserPage.title }}</span>
<p class="content my-2">{{ .lang.UserPage.description }}</p>
<p class="content my-2">{{ .lang.UserPage.customizeMessages }}</p>
<label class="row switch pb-4">
<input type="checkbox" class="mr-2" id="userpage-enabled"><span>{{ .lang.Strings.enabled }}</span>
</label>
<p class="support mb-1 mt-1">{{ .lang.UserPage.requiredSettings }}</p>
<section class="section ~neutral banner footer flex-expand middle">
<span class="button ~neutral @low back">{{ .lang.Strings.back }}</span>
<div>
<span class="button ~urge @low next">{{ .lang.Strings.next }}</span>
</div>
</section>
</div>
<div class="card ~neutral @low mb-2 unfocused"> <div class="card ~neutral @low mb-2 unfocused">
<span class="heading">{{ .lang.Messages.title }}</span> <span class="heading">{{ .lang.Messages.title }}</span>
<p class="content my-2" id="messages-description"></p> <p class="content my-2" id="messages-description"></p>
@ -391,7 +407,7 @@
</label> </label>
<label class="switch"> <label class="switch">
<input type="checkbox" class="mr-2" id="password_resets-link_reset"><span>{{ .lang.PasswordResets.resetLinks }}</span> <input type="checkbox" class="mr-2" id="password_resets-link_reset"><span>{{ .lang.PasswordResets.resetLinks }}</span>
<p class="support mb-2 mt-1">{{ .lang.PasswordResets.resetLinksNotice }}</p> <p class="support mb-2 mt-1">{{ .lang.PasswordResets.resetLinksNotice }} {{ .lang.PasswordResets.resetLinksRequiredForUserPage }}</p>
</label> </label>
<label class="switch"> <label class="switch">
<input type="checkbox" class="mr-2" id="password_resets-set_password"><span>{{ .lang.PasswordResets.setPassword }}</span> <input type="checkbox" class="mr-2" id="password_resets-set_password"><span>{{ .lang.PasswordResets.setPassword }}</span>

View File

@ -123,6 +123,7 @@ type setupLang struct {
Email langSection `json:"email"` Email langSection `json:"email"`
Messages langSection `json:"messages"` Messages langSection `json:"messages"`
Notifications langSection `json:"notifications"` Notifications langSection `json:"notifications"`
UserPage langSection `json:"userPage"`
WelcomeEmails langSection `json:"welcomeEmails"` WelcomeEmails langSection `json:"welcomeEmails"`
PasswordResets langSection `json:"passwordResets"` PasswordResets langSection `json:"passwordResets"`
InviteEmails langSection `json:"inviteEmails"` InviteEmails langSection `json:"inviteEmails"`

View File

@ -70,6 +70,7 @@
"adminOnly": "Admin users only (recommended)", "adminOnly": "Admin users only (recommended)",
"allowAll": "Allow all Jellyfin users to login", "allowAll": "Allow all Jellyfin users to login",
"allowAllDescription": "Not recommended, you should allow individual users to login once setup.", "allowAllDescription": "Not recommended, you should allow individual users to login once setup.",
"authorizeManualUserPageNotice": "Using this will disable the \"User Page\" feature.",
"emailNotice": "Your email address can be used to receive notifications." "emailNotice": "Your email address can be used to receive notifications."
}, },
"jellyfinEmby": { "jellyfinEmby": {
@ -109,6 +110,12 @@
"title": "Admin Notifications", "title": "Admin Notifications",
"description": "If enabled, you can choose (per invite) to receive an message when an invite expires, or a user is created. If you didn't choose the Jellyfin login method, make sure you provided your email address, or add another contact method later." "description": "If enabled, you can choose (per invite) to receive an message when an invite expires, or a user is created. If you didn't choose the Jellyfin login method, make sure you provided your email address, or add another contact method later."
}, },
"userPage": {
"title": "User Page",
"description": "The user page (shown as \"My Account\") allows users to access information about their account, such as their contact methods and account expiry. They can also change their password, start a password reset, and link/change contact methods, without having to ask you. Additionally, customized Markdown messages can be shown to the users before and after logging in.",
"customizeMessages": "Click the edit button next to \"User Page\" in settings to set them later.",
"requiredSettings": "Log-in to jfa-go via Jellyfin must be set. Ensure \"reset password via link\" is selected later for self-service password resets."
},
"welcomeEmails": { "welcomeEmails": {
"title": "Welcome messages", "title": "Welcome messages",
"description": "If enabled, an message will be sent to new users with the Jellyfin/Emby URL and their username." "description": "If enabled, an message will be sent to new users with the Jellyfin/Emby URL and their username."
@ -119,10 +126,11 @@
}, },
"passwordResets": { "passwordResets": {
"title": "Password Resets", "title": "Password Resets",
"description": "When a user tries to reset their password, Jellyfin creates a file named 'passwordreset-*.json' which contains a PIN. jfa-go reads the file and sends the PIN to the user.", "description": "When a user tries to reset their password, Jellyfin creates a file named 'passwordreset-*.json' which contains a PIN. jfa-go reads the file and sends the PIN to the user. If you enabled the \"User Page\" feature, a reset can also be performed there, given a username, email, or contact method.",
"pathToJellyfin": "Path to Jellyfin configuration directory", "pathToJellyfin": "Path to Jellyfin configuration directory",
"pathToJellyfinNotice": "If you don't know where this is, try resetting your password in Jellyfin. A popup with '<path to jellyfin>/passwordreset-*.json' will appear.", "pathToJellyfinNotice": "If you don't know where this is, try resetting your password in Jellyfin. A popup with '<path to jellyfin>/passwordreset-*.json' will appear. This is not necessary if you only want to use self-service password resets through the \"User Page\".",
"resetLinks": "Send a link instead of a PIN", "resetLinks": "Send a link instead of a PIN",
"resetLinksRequiredForUserPage": "Required for self-service password reset on the User Page.",
"resetLinksNotice": "If Ombi integration is enabled, use this to sync Jellyfin password resets with Ombi.", "resetLinksNotice": "If Ombi integration is enabled, use this to sync Jellyfin password resets with Ombi.",
"resetLinksLanguage": "Default reset link language", "resetLinksLanguage": "Default reset link language",
"setPassword": "Set password through link", "setPassword": "Set password through link",
@ -149,4 +157,4 @@
"emailMessage": "Email Message", "emailMessage": "Email Message",
"emailMessageNotice": "Displays at the bottom of emails." "emailMessageNotice": "Displays at the bottom of emails."
} }
} }

View File

@ -133,6 +133,7 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
patchLang(&lang.Email, &fallback.Email, &english.Email) patchLang(&lang.Email, &fallback.Email, &english.Email)
patchLang(&lang.Messages, &fallback.Messages, &english.Messages) patchLang(&lang.Messages, &fallback.Messages, &english.Messages)
patchLang(&lang.Notifications, &fallback.Notifications, &english.Notifications) patchLang(&lang.Notifications, &fallback.Notifications, &english.Notifications)
patchLang(&lang.UserPage, &fallback.UserPage, &english.UserPage)
patchLang(&lang.PasswordResets, &fallback.PasswordResets, &english.PasswordResets) patchLang(&lang.PasswordResets, &fallback.PasswordResets, &english.PasswordResets)
patchLang(&lang.InviteEmails, &fallback.InviteEmails, &english.InviteEmails) patchLang(&lang.InviteEmails, &fallback.InviteEmails, &english.InviteEmails)
patchLang(&lang.PasswordValidation, &fallback.PasswordValidation, &english.PasswordValidation) patchLang(&lang.PasswordValidation, &fallback.PasswordValidation, &english.PasswordValidation)
@ -150,6 +151,7 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
patchLang(&lang.Email, &english.Email) patchLang(&lang.Email, &english.Email)
patchLang(&lang.Messages, &english.Messages) patchLang(&lang.Messages, &english.Messages)
patchLang(&lang.Notifications, &english.Notifications) patchLang(&lang.Notifications, &english.Notifications)
patchLang(&lang.UserPage, &english.UserPage)
patchLang(&lang.PasswordResets, &english.PasswordResets) patchLang(&lang.PasswordResets, &english.PasswordResets)
patchLang(&lang.InviteEmails, &english.InviteEmails) patchLang(&lang.InviteEmails, &english.InviteEmails)
patchLang(&lang.PasswordValidation, &english.PasswordValidation) patchLang(&lang.PasswordValidation, &english.PasswordValidation)

View File

@ -23,10 +23,44 @@ module.exports = {
opacity: '0' opacity: '0'
} }
}, },
'slide-in': {
'0%': {
opacity: '0',
transform: 'translateY(-100%)'
},
'100%': {
opacity: '1',
transform: 'translateY(0%)'
},
},
'slide-out': {
'0%': {
opacity: '1',
transform: 'translateY(0%)'
},
'100%': {
opacity: '0',
transform: 'translateY(-100%)'
},
},
'pulse': {
'0%': {
transform: 'scale(1)'
},
'50%': {
transform: 'scale(1.05)'
},
'100%': {
transform: 'scale(1)'
}
}
}, },
animation: { animation: {
'fade-in': 'fade-in 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)', 'fade-in': 'fade-in 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
'fade-out': 'fade-out 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)' 'fade-out': 'fade-out 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94)',
'slide-in': 'slide-in 0.2s cubic-bezier(.08,.52,.01,.98)',
'slide-out': 'slide-out 0.2s cubic-bezier(.08,.52,.01,.98)',
'pulse': 'pulse 0.2s cubic-bezier(0.25, 0.45, 0.45, 0.94)'
}, },
colors: { colors: {
neutral: colors.slate, neutral: colors.slate,

View File

@ -113,7 +113,8 @@ export class notificationBox implements NotificationBox {
const closeButton = document.createElement('span') as HTMLSpanElement; const closeButton = document.createElement('span') as HTMLSpanElement;
closeButton.classList.add("button", "~critical", "@low", "ml-4"); closeButton.classList.add("button", "~critical", "@low", "ml-4");
closeButton.innerHTML = `<i class="icon ri-close-line"></i>`; closeButton.innerHTML = `<i class="icon ri-close-line"></i>`;
closeButton.onclick = () => { this._box.removeChild(noti); }; closeButton.onclick = () => this._close(noti);
noti.classList.add("animate-slide-in");
noti.appendChild(closeButton); noti.appendChild(closeButton);
return noti; return noti;
} }
@ -125,11 +126,21 @@ export class notificationBox implements NotificationBox {
const closeButton = document.createElement('span') as HTMLSpanElement; const closeButton = document.createElement('span') as HTMLSpanElement;
closeButton.classList.add("button", "~positive", "@low", "ml-4"); closeButton.classList.add("button", "~positive", "@low", "ml-4");
closeButton.innerHTML = `<i class="icon ri-close-line"></i>`; closeButton.innerHTML = `<i class="icon ri-close-line"></i>`;
closeButton.onclick = () => { this._box.removeChild(noti); }; closeButton.onclick = () => this._close(noti);
noti.classList.add("animate-slide-in");
noti.appendChild(closeButton); noti.appendChild(closeButton);
return noti; return noti;
} }
private _close = (noti: HTMLElement) => {
noti.classList.remove("animate-slide-in");
noti.classList.add("animate-slide-out");
noti.addEventListener(window.animationEvent, () => {
this._box.removeChild(noti);
}, false);
}
connectionError = () => { this.customError("connectionError", window.lang.notif("errorConnection")); } connectionError = () => { this.customError("connectionError", window.lang.notif("errorConnection")); }
customError = (type: string, message: string) => { customError = (type: string, message: string) => {
@ -138,11 +149,13 @@ export class notificationBox implements NotificationBox {
noti.classList.add("error-" + type); noti.classList.add("error-" + type);
const previousNoti: HTMLElement | undefined = this._box.querySelector("aside.error-" + type); const previousNoti: HTMLElement | undefined = this._box.querySelector("aside.error-" + type);
if (this._errorTypes[type] && previousNoti !== undefined && previousNoti != null) { if (this._errorTypes[type] && previousNoti !== undefined && previousNoti != null) {
previousNoti.remove(); this._box.removeChild(previousNoti);
noti.classList.add("animate-pulse");
noti.classList.remove("animate-slide-in");
} }
this._box.appendChild(noti); this._box.appendChild(noti);
this._errorTypes[type] = true; this._errorTypes[type] = true;
setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._errorTypes[type] = false; } }, this.timeout*1000); setTimeout(() => { if (this._box.contains(noti)) { this._close(noti); this._errorTypes[type] = false; } }, this.timeout*1000);
} }
customPositive = (type: string, bold: string, message: string) => { customPositive = (type: string, bold: string, message: string) => {
@ -151,11 +164,13 @@ export class notificationBox implements NotificationBox {
noti.classList.add("positive-" + type); noti.classList.add("positive-" + type);
const previousNoti: HTMLElement | undefined = this._box.querySelector("aside.positive-" + type); const previousNoti: HTMLElement | undefined = this._box.querySelector("aside.positive-" + type);
if (this._positiveTypes[type] && previousNoti !== undefined && previousNoti != null) { if (this._positiveTypes[type] && previousNoti !== undefined && previousNoti != null) {
previousNoti.remove(); this._box.removeChild(previousNoti);
noti.classList.add("animate-pulse");
noti.classList.remove("animate-slide-in");
} }
this._box.appendChild(noti); this._box.appendChild(noti);
this._positiveTypes[type] = true; this._positiveTypes[type] = true;
setTimeout(() => { if (this._box.contains(noti)) { this._box.removeChild(noti); this._positiveTypes[type] = false; } }, this.timeout*1000); setTimeout(() => { if (this._box.contains(noti)) { this._close(noti); this._positiveTypes[type] = false; } }, this.timeout*1000);
} }
customSuccess = (type: string, message: string) => this.customPositive(type, window.lang.strings("success") + ":", message) customSuccess = (type: string, message: string) => this.customPositive(type, window.lang.strings("success") + ":", message)

View File

@ -5,11 +5,12 @@ export class Login {
private _modal: Modal; private _modal: Modal;
private _form: HTMLFormElement; private _form: HTMLFormElement;
private _url: string; private _url: string;
private _endpoint: string;
private _onLogin: (username: string, password: string) => void; private _onLogin: (username: string, password: string) => void;
private _logoutButton: HTMLElement = null; private _logoutButton: HTMLElement = null;
constructor(modal: Modal, endpoint: string) { constructor(modal: Modal, endpoint: string) {
this._endpoint = endpoint;
this._url = window.URLBase + endpoint; this._url = window.URLBase + endpoint;
if (this._url[this._url.length-1] != '/') this._url += "/"; if (this._url[this._url.length-1] != '/') this._url += "/";
@ -32,13 +33,21 @@ export class Login {
bindLogout = (button: HTMLElement) => { bindLogout = (button: HTMLElement) => {
this._logoutButton = button; this._logoutButton = button;
this._logoutButton.classList.add("unfocused"); this._logoutButton.classList.add("unfocused");
this._logoutButton.onclick = () => _post(this._url + "logout", null, (req: XMLHttpRequest): boolean => { const logoutFunc = (url: string, tryAgain: boolean) => {
if (req.readyState == 4 && req.status == 200) { _post(url + "logout", null, (req: XMLHttpRequest): boolean => {
window.token = ""; if (req.readyState == 4 && req.status == 200) {
location.reload(); window.token = "";
return false; location.reload();
} return false;
}); }
}, false, (req: XMLHttpRequest) => {
if (req.readyState == 4 && req.status == 404 && tryAgain) {
console.log("trying without URL Base...");
logoutFunc(this._endpoint, false);
}
});
};
this._logoutButton.onclick = () => logoutFunc(this._url, true);
}; };
get onLogin() { return this._onLogin; } get onLogin() { return this._onLogin; }

View File

@ -283,6 +283,9 @@ const settings = {
"notifications": { "notifications": {
"enabled": new Checkbox(get("notifications-enabled")) "enabled": new Checkbox(get("notifications-enabled"))
}, },
"user_page": {
"enabled": new Checkbox(get("userpage-enabled"))
},
"welcome_email": { "welcome_email": {
"enabled": new Checkbox(get("welcome_email-enabled"), "", false, "welcome_email", "enabled"), "enabled": new Checkbox(get("welcome_email-enabled"), "", false, "welcome_email", "enabled"),
"subject": new Input(get("welcome_email-subject"), "", "", "enabled", true, "welcome_email") "subject": new Input(get("welcome_email-subject"), "", "", "enabled", true, "welcome_email")