mirror of https://github.com/hrfee/jfa-go.git
Compare commits
4 Commits
49c7d83840
...
d56f321aad
Author | SHA1 | Date |
---|---|---|
Harvey Tindall | d56f321aad | |
Harvey Tindall | bedd2bbb23 | |
Harvey Tindall | 27ef7ce560 | |
Harvey Tindall | 775ebd3b1e |
|
@ -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/)
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -432,6 +432,9 @@ p.top {
|
|||
.dropdown {
|
||||
padding-bottom: 0.5rem;
|
||||
margin-bottom: -0.5rem;
|
||||
}
|
||||
|
||||
.dropdown.over-top {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
|
|
@ -59,6 +59,7 @@
|
|||
"reset": "Reset",
|
||||
"edit": "Edit",
|
||||
"donate": "Donate",
|
||||
"unlink": "Unlink Account",
|
||||
"sendPWR": "Send Password Reset",
|
||||
"contactThrough": "Contact through:",
|
||||
"extendExpiry": "Extend expiry",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 <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>
|
||||
|
|
|
@ -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; }
|
||||
|
|
Loading…
Reference in New Issue