Compare commits

...

4 Commits

Author SHA1 Message Date
Harvey Tindall d56f321aad
site changes 2023-02-05 01:08:29 +00:00
Harvey Tindall bedd2bbb23
README changes 2023-02-05 00:59:04 +00:00
Harvey Tindall 27ef7ce560
css: revert global change to absolute position for dropdowns
changing dropdowns to always have absolute positioning (in c187b94)
caused issues with all other dropdowns, where neighbors were positioned
below and hidden. adding the "over-top" class to a dropdown now gives
it absolute positioning.
2023-02-05 00:49:13 +00:00
Harvey Tindall 775ebd3b1e
Accounts: Unlink Telegram/Discord/Matrix through cog
Added an unlinking section to the little cog dropdown next to users so
that one can remove and re-link a different account for a Jellyfin user.
Also adjusted padding in the dropdown.
2023-02-05 00:23:16 +00:00
8 changed files with 126 additions and 15 deletions

View File

@ -19,14 +19,15 @@ a rewrite of [jellyfin-accounts](https://github.com/hrfee/jellyfin-accounts) (or
* Granular control over invites: Validity period as well as number of uses can be specified.
* Account profiles: Assign settings profiles to invites so new users have your predefined permissions, homescreen layout, etc. applied to their account on creation.
* Password validation: Ensure users choose a strong password.
* CAPTCHAs can be enabled to avoid bots
* ⌛ User expiry: Specify a validity period, and new users accounts will be disabled/deleted after it. The period can be manually extended too.
* 🔗 Ombi Integration: Automatically creates Ombi accounts for new users using their email address and login details, and your own defined set of permissions.
* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason.
* Telegram/Discord/Matrix Integration: Verify users via a chat bot, and send Password Resets, Announcements, etc. through it.
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
* Email addresses can optionally be used instead of usernames
* 🔑 Password resets: When users forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to the user via email/telegram.
* Notifications: Get notified when someone creates an account, or an invite expires.
* 🔑 Password resets: When users forget their passwords and request a change in Jellyfin, jfa-go reads the PIN from the created file and sends it straight to them via email/telegram.
* Admin Notifications: Get notified when someone creates an account, or an invite expires.
* 📣 Announcements: Bulk message your users with announcements about your server.
* Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider.
* Enables the usage of jfa-go by multiple people
@ -150,7 +151,7 @@ If you're switching from jellyfin-accounts, copy your existing `~/.jf-accounts`
(or specify config/data path with `-config/-data` respectively.)
#### Contributing
See [CONTRIBUTING.md](https://github.com/hrfee/jfa-go/blob/main/CONTRIBUTING.md).
See [the wiki page](https://wiki.jfa-go.com/docs/dev/) or [CONTRIBUTING.md](https://github.com/hrfee/jfa-go/blob/main/CONTRIBUTING.md).
##### Translation
[![Translation status](https://weblate.jfa-go.com/widgets/jfa-go/-/multi-auto.svg)](https://weblate.jfa-go.com/engage/jfa-go/)

View File

@ -724,3 +724,60 @@ func (app *appContext) DiscordConnect(gc *gin.Context) {
linkExistingOmbiDiscordTelegram(app)
respondBool(200, true, gc)
}
// @Summary unlink a Discord account from a Jellyfin user. Always succeeds.
// @Produce json
// @Success 200 {object} boolResponse
// @Param forUserDTO body forUserDTO true "User's Jellyfin ID."
// @Router /users/discord [delete]
// @Tags Users
func (app *appContext) UnlinkDiscord(gc *gin.Context) {
var req forUserDTO
gc.BindJSON(&req)
/* user, status, err := app.jf.UserByID(req.ID, false)
if req.ID == "" || status != 200 || err != nil {
respond(400, "User not found", gc)
return
} */
delete(app.storage.discord, req.ID)
app.storage.storeDiscordUsers()
respondBool(200, true, gc)
}
// @Summary unlink a Telegram account from a Jellyfin user. Always succeeds.
// @Produce json
// @Success 200 {object} boolResponse
// @Param forUserDTO body forUserDTO true "User's Jellyfin ID."
// @Router /users/telegram [delete]
// @Tags Users
func (app *appContext) UnlinkTelegram(gc *gin.Context) {
var req forUserDTO
gc.BindJSON(&req)
/* user, status, err := app.jf.UserByID(req.ID, false)
if req.ID == "" || status != 200 || err != nil {
respond(400, "User not found", gc)
return
} */
delete(app.storage.telegram, req.ID)
app.storage.storeTelegramUsers()
respondBool(200, true, gc)
}
// @Summary unlink a Matrix account from a Jellyfin user. Always succeeds.
// @Produce json
// @Success 200 {object} boolResponse
// @Param forUserDTO body forUserDTO true "User's Jellyfin ID."
// @Router /users/matrix [delete]
// @Tags Users
func (app *appContext) UnlinkMatrix(gc *gin.Context) {
var req forUserDTO
gc.BindJSON(&req)
/* user, status, err := app.jf.UserByID(req.ID, false)
if req.ID == "" || status != 200 || err != nil {
respond(400, "User not found", gc)
return
} */
delete(app.storage.matrix, req.ID)
app.storage.storeMatrixUsers()
respondBool(200, true, gc)
}

View File

@ -432,6 +432,9 @@ p.top {
.dropdown {
padding-bottom: 0.5rem;
margin-bottom: -0.5rem;
}
.dropdown.over-top {
position: absolute;
}

View File

@ -59,6 +59,7 @@
"reset": "Reset",
"edit": "Edit",
"donate": "Donate",
"unlink": "Unlink Account",
"sendPWR": "Send Password Reset",
"contactThrough": "Contact through:",
"extendExpiry": "Extend expiry",

View File

@ -355,3 +355,7 @@ type setAccountsAdminDTO map[string]bool
type genCaptchaDTO struct {
ID string `json:"id"`
}
type forUserDTO struct {
ID string `json:"id"` // Jellyfin ID
}

View File

@ -192,6 +192,9 @@ func (app *appContext) loadRoutes(router *gin.Engine) {
api.GET(p+"/telegram/pin", app.TelegramGetPin)
api.GET(p+"/telegram/verified/:pin", app.TelegramVerified)
api.POST(p+"/users/telegram", app.TelegramAddUser)
api.DELETE(p+"/users/telegram", app.UnlinkTelegram)
api.DELETE(p+"/users/discord", app.UnlinkDiscord)
api.DELETE(p+"/users/matrix", app.UnlinkMatrix)
}
if emailEnabled {
api.POST(p+"/users/contact", app.SetContactMethods)

View File

@ -45,7 +45,7 @@ sudo apt-get install jfa-go-tray
-p 8056:8056 <span style="color: #BB6622; font-weight: bold">\</span>
<span style="color: #408080; font-style: italic"># -p 8057:8057 if using tls</span>
-v /path/to/.config/jfa-go:/data <span style="color: #BB6622; font-weight: bold">\ </span><span style="color: #408080; font-style: italic"># Path to wherever you want to store the config file and other data</span>
-v /path/to/jellyfin:/jf <span style="color: #BB6622; font-weight: bold">\ </span><span style="color: #408080; font-style: italic"># Path to Jellyfin config directory, ignore if using Emby</span>
-v /path/to/jellyfin:/jf <span style="color: #BB6622; font-weight: bold">\ </span><span style="color: #408080; font-style: italic"># Optional path to Jellyfin config directory for auto password resets, ignore if using Emby</span>
-v /etc/localtime:/etc/localtime:ro <span style="color: #BB6622; font-weight: bold">\ </span><span style="color: #408080; font-style: italic"># Makes sure time is correct</span>
hrfee/jfa-go<span id="docker-unstable" class="unfocused">:unstable</span></pre>
</div>
@ -64,7 +64,14 @@ sudo apt-get install jfa-go-tray
<li>Send invite links to your users, let them sign up themselves</li>
<li>Create setting profiles to restrict permissions of new users</li>
<li>Handles password resets without your intervention</li>
<li>Enforce password requirements on sign-up</li>
<li>
Enforce sign-up requirements:
<ul>
<li>Password strength</li>
<li>Contact method verification</li>
<li>CAPTCHA</li>
</ul>
</li>
<li>Send messages & notifications to your users (email, discord, telegram, matrix available)</li>
<li>Set accounts to expire after a specified time</li>
<li>Manage your users in bulk</li>
@ -95,11 +102,11 @@ sudo apt-get install jfa-go-tray
<p class="row col flex center support">instructions can be found&nbsp<a target="_blank" href="https://github.com/hrfee/jfa-go#install">here</a></p>
<p class="row col flex center text-center support">note: tray icon builds should only be used on systems with a Desktop Interface, and require extra dependencies on linux, see the github README for more info.</p>
<div class="row col flex center">
<span class="button ~neutral @high mr-1 mt-1" id="download-stable">Stable</span>
<span class="button ~neutral @high mr-1 mt-1" id="download-stable">Stable (-ish)</span>
<span class="button ~neutral mt-1 mr-1" id="download-unstable">Unstable</span>
</div>
<div class="mt-1" id="sect-stable">
<p class="row center">Usually released once/twice every month, and aren't necessarily super stable.</p>
<p class="row center">Released sporadically, not necessarily super stable.</p>
<div class="row col flex center">
<a class="button ~info mr-2 mb-2 lang-link" target="_blank" href="https://github.com/hrfee/jfa-go/releases">windows/mac/linux</a>
<a class="button ~info mr-2 mb-2 lang-link" id="download-docker">docker</a>
@ -109,7 +116,7 @@ sudo apt-get install jfa-go-tray
</div>
</div>
<div class="mt-1 unfocused" id="sect-unstable">
<p class="row center">These are built on every commit, so may include incomplete/broken features. Take care.</p>
<p class="row center">Built on every commit, so may include incomplete/broken features. Take care.</p>
<div class="row col flex center">
<a class="button ~info mr-2 mb-2 lang-link" id="download-docker-unstable">docker</a>
<a class="button ~info mr-2 mb-2 lang-link" id="download-deb-unstable">debian/ubuntu</a>
@ -117,7 +124,7 @@ sudo apt-get install jfa-go-tray
</div>
</div>
<section class="section ~neutral banner footer flex-expand middle">
<a href="https://github.com/hrfee/jfa-go/blob/main/LICENSE" class="support">© 2021 Harvey Tindall</a>
<a href="https://github.com/hrfee/jfa-go/blob/main/LICENSE" class="support">© 2023 Harvey Tindall</a>
</section>
</div>
</div>

View File

@ -81,6 +81,15 @@ class user implements User {
if (email) return "email";
}
private _checkUnlinkArea = () => {
const unlinkHeader = this._notifyDropdown.querySelector(".accounts-unlink-header") as HTMLSpanElement;
if (this.lastNotifyMethod() == "email" || !this.lastNotifyMethod()) {
unlinkHeader.classList.add("unfocused");
} else {
unlinkHeader.classList.remove("unfocused");
}
}
get selected(): boolean { return this._selected; }
set selected(state: boolean) {
this._selected = state;
@ -161,34 +170,44 @@ class user implements User {
if (!telegram && !discord && !matrix && !email) return;
let innerHTML = `
<i class="icon ri-settings-2-line ml-2 dropdown-button"></i>
<div class="dropdown manual">
<div class="dropdown over-top manual">
<div class="dropdown-display lg">
<div class="card ~neutral @low">
<span class="supra sm">${window.lang.strings("contactThrough")}</span>
<div class="supra sm mb-2">${window.lang.strings("contactThrough")}</div>
<div class="accounts-area-email">
<label class="row switch pb-4 mt-2">
<label class="row switch pb-2">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-email mr-2">
</span>Email</span>
</label>
</div>
<div class="accounts-area-telegram">
<label class="row switch pb-4">
<label class="row switch pb-2">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-telegram mr-2">
<span>Telegram</span>
</label>
</div>
<div class="accounts-area-discord">
<label class="row switch pb-4">
<label class="row switch pb-2">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-discord mr-2">
<span>Discord</span>
</label>
</div>
<div class="accounts-area-matrix">
<label class="row switch pb-4">
<label class="row switch pb-2">
<input type="checkbox" name="accounts-contact-${this.id}" class="accounts-contact-matrix mr-2">
<span>Matrix</span>
</label>
</div>
<div class="supra sm mb-2 accounts-unlink-header">${window.lang.strings("unlink")}:</div>
<div class="accounts-unlink-telegram">
<button class="button ~critical mb-2 w-100">Telegram</button>
</div>
<div class="accounts-unlink-discord">
<button class="button ~critical mb-2 w-100">Discord</button>
</div>
<div class="accounts-unlink-matrix">
<button class="button ~critical mb-2 w-100">Matrix</button>
</div>
</div>
</div>
</div>
@ -200,6 +219,10 @@ class user implements User {
for (let i = 0; i < checks.length; i++) {
checks[i].onclick = () => this._setNotifyMethod();
}
for (let service of ["telegram", "discord", "matrix"]) {
el.querySelector(".accounts-unlink-"+service).addEventListener("click", () => _delete(`/users/${service}`, {"id": this.id}, () => document.dispatchEvent(new CustomEvent("accounts-reload"))));
}
button.onclick = () => {
dropdown.classList.add("selected");
@ -218,12 +241,14 @@ class user implements User {
set matrix(u: string) {
if (!window.matrixEnabled) {
this._notifyDropdown.querySelector(".accounts-area-matrix").classList.add("unfocused");
this._notifyDropdown.querySelector(".accounts-unlink-matrix").classList.add("unfocused");
return;
}
const lastNotifyMethod = this.lastNotifyMethod() == "matrix";
this._matrixID = u;
if (!u) {
this._notifyDropdown.querySelector(".accounts-area-matrix").classList.add("unfocused");
this._notifyDropdown.querySelector(".accounts-unlink-matrix").classList.add("unfocused");
this._matrix.innerHTML = `
<div class="table-inline justify-center">
<span class="chip btn @low"><i class="ri-link" alt="${window.lang.strings("add")}"></i></span>
@ -233,6 +258,7 @@ class user implements User {
(this._matrix.querySelector("span") as HTMLSpanElement).onclick = this._addMatrix;
} else {
this._notifyDropdown.querySelector(".accounts-area-matrix").classList.remove("unfocused");
this._notifyDropdown.querySelector(".accounts-unlink-matrix").classList.remove("unfocused");
this._matrix.innerHTML = `
<div class="table-inline">
${u}
@ -242,6 +268,7 @@ class user implements User {
(this._matrix.querySelector(".table-inline") as HTMLDivElement).appendChild(this._notifyDropdown);
}
}
this._checkUnlinkArea();
}
private _addMatrix = () => {
@ -290,16 +317,19 @@ class user implements User {
set telegram(u: string) {
if (!window.telegramEnabled) {
this._notifyDropdown.querySelector(".accounts-area-telegram").classList.add("unfocused");
this._notifyDropdown.querySelector(".accounts-unlink-telegram").classList.add("unfocused");
return;
}
const lastNotifyMethod = this.lastNotifyMethod() == "telegram";
this._telegramUsername = u;
if (!u) {
this._notifyDropdown.querySelector(".accounts-area-telegram").classList.add("unfocused");
this._notifyDropdown.querySelector(".accounts-unlink-telegram").classList.add("unfocused");
this._telegram.innerHTML = `<div class="table-inline justify-center"><span class="chip btn @low"><i class="ri-link" alt="${window.lang.strings("add")}"></i></span></div>`;
(this._telegram.querySelector("span") as HTMLSpanElement).onclick = this._addTelegram;
} else {
this._notifyDropdown.querySelector(".accounts-area-telegram").classList.remove("unfocused");
this._notifyDropdown.querySelector(".accounts-unlink-telegram").classList.remove("unfocused");
this._telegram.innerHTML = `
<div class="table-inline">
<a href="https://t.me/${u}" target="_blank">@${u}</a>
@ -309,6 +339,7 @@ class user implements User {
(this._telegram.querySelector(".table-inline") as HTMLDivElement).appendChild(this._notifyDropdown);
}
}
this._checkUnlinkArea();
}
get notify_telegram(): boolean { return this._notifyTelegram; }
@ -356,6 +387,7 @@ class user implements User {
set discord(u: string) {
if (!window.discordEnabled) {
this._notifyDropdown.querySelector(".accounts-area-discord").classList.add("unfocused");
this._notifyDropdown.querySelector(".accounts-unlink-discord").classList.add("unfocused");
return;
}
const lastNotifyMethod = this.lastNotifyMethod() == "discord";
@ -364,8 +396,10 @@ class user implements User {
this._discord.innerHTML = `<div class="table-inline justify-center"><span class="chip btn @low"><i class="ri-link" alt="${window.lang.strings("add")}"></i></span></div>`;
(this._discord.querySelector("span") as HTMLSpanElement).onclick = () => addDiscord(this.id);
this._notifyDropdown.querySelector(".accounts-area-discord").classList.add("unfocused");
this._notifyDropdown.querySelector(".accounts-unlink-discord").classList.add("unfocused");
} else {
this._notifyDropdown.querySelector(".accounts-area-discord").classList.remove("unfocused");
this._notifyDropdown.querySelector(".accounts-unlink-discord").classList.remove("unfocused");
this._discord.innerHTML = `
<div class="table-inline">
<a href="https://discord.com/users/${this._discordID}" class="discord-link" target="_blank">${u}</a>
@ -375,6 +409,7 @@ class user implements User {
(this._discord.querySelector(".table-inline") as HTMLDivElement).appendChild(this._notifyDropdown);
}
}
this._checkUnlinkArea();
}
get discord_id(): string { return this._discordID; }