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

Compare commits

...

2 Commits

Author SHA1 Message Date
76bb95098c
accounts: add list of available filters, fix deletion of existing date filters
The "Filters" button gives a list of filterable fields, and buttons to
select the type, including true/false, text match, and on/before/after a
date. When clicked, the appropriate values are put in the search box and
the cursor is placed if any input is needed.

Dates and strings are also now matched correctly, and case-insensitively when
deleting a filter.
2023-06-14 14:01:05 +01:00
0e241f56fb
scripts: add script to generate fake accounts
might be useful for screenshots too, currently just using for testing
the sorting/filtering.
2023-06-14 11:50:22 +01:00
6 changed files with 329 additions and 102 deletions

View File

@ -579,11 +579,19 @@
</div> </div>
</div> </div>
<div id="tab-accounts" class="unfocused"> <div id="tab-accounts" class="unfocused">
<div class="card @low dark:~d_neutral accounts mb-4"> <div class="card @low dark:~d_neutral accounts mb-4 overflow-visible">
<div class="flex-expand row"> <div class="flex-expand row">
<div class="row"> <div class="row w-6/12">
<span class="text-3xl font-bold mr-2 col">{{ .strings.accounts }}</span> <span class="text-3xl font-bold mr-2 col">{{ .strings.accounts }}</span>
<input type="search" class="col sm field ~neutral @low input search ml-2 mr-2" id="accounts-search" placeholder="{{ .strings.search }}"> <input type="search" class="col sm field ~neutral @low input search ml-2 mr-2" id="accounts-search" placeholder="{{ .strings.search }}">
<div id="accounts-filter-dropdown" class="col sm dropdown pb-0i" tabindex="0">
<span class="h-100 sm button ~neutral @low center mb-2" id="accounts-filter-button">{{ .strings.filters }}</span>
<div class="dropdown-display">
<div class="card ~neutral @low mt-2" id="accounts-filter-list">
<p class="supra pb-2">{{ .strings.filters }}</p>
</div>
</div>
</div>
</div> </div>
<div class="row"> <div class="row">
<span class="col sm button ~neutral @low center mb-2" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span> <span class="col sm button ~neutral @low center mb-2" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>

View File

@ -118,6 +118,7 @@
"accessJFA": "Access jfa-go", "accessJFA": "Access jfa-go",
"accessJFASettings": "Cannot be changed as either \"Admin Only\" or \"Allow All\" has been set in Settings > General.", "accessJFASettings": "Cannot be changed as either \"Admin Only\" or \"Allow All\" has been set in Settings > General.",
"sortingBy": "Sorting By", "sortingBy": "Sorting By",
"filters": "Filters",
"clickToRemoveFilter": "Click to remove this filter." "clickToRemoveFilter": "Click to remove this filter."
}, },
"notifications": { "notifications": {

View File

@ -0,0 +1,5 @@
module github.com/hrfee/jfa-go/scripts/account-gen
go 1.20
require github.com/hrfee/mediabrowser v0.3.8 // indirect

View File

@ -0,0 +1,2 @@
github.com/hrfee/mediabrowser v0.3.8 h1:y0iBCb6jE3QKcsiCJSYva2fFPHRn4UA+sGRzoPuJ/Dk=
github.com/hrfee/mediabrowser v0.3.8/go.mod h1:PnHZbdxmbv1wCVdAQyM7nwPwpVj9fdKx2EcET7sAk+U=

122
scripts/account-gen/main.go Normal file
View File

@ -0,0 +1,122 @@
package main
import (
"bufio"
"fmt"
"log"
"math/rand"
"os"
"strconv"
"strings"
"time"
"github.com/hrfee/mediabrowser"
)
var (
names = []string{"Aaron", "Agnes", "Bridget", "Brandon", "Dolly", "Drake", "Elizabeth", "Erika", "Geoff", "Graham", "Haley", "Halsey", "Josie", "John", "Kayleigh", "Luka", "Melissa", "Nasreen", "Paul", "Ross", "Sam", "Talib", "Veronika", "Zaynab"}
)
const (
PASSWORD = "test"
COUNT = 10
)
func main() {
fmt.Println("Usage: account-gen <server> <username> <password, or file://path to file containing password>")
var server, username, password string
reader := bufio.NewReader(os.Stdin)
if len(os.Args) > 1 {
server = os.Args[1]
} else {
fmt.Print("Server Address: ")
server, _ = reader.ReadString('\n')
server = strings.TrimSuffix(server, "\n")
}
if len(os.Args) > 2 {
username = os.Args[2]
} else {
fmt.Print("Username: ")
username, _ = reader.ReadString('\n')
username = strings.TrimSuffix(username, "\n")
}
if len(os.Args) > 3 {
password = os.Args[3]
if strings.HasPrefix(password, "file://") {
p, err := os.ReadFile(strings.TrimPrefix(password, "file://"))
if err != nil {
log.Fatalf("Failed to read password file \"%s\": %+v\n", password, err)
}
password = strings.TrimSuffix(string(p), "\n")
}
} else {
fmt.Print("Password: ")
password, _ = reader.ReadString('\n')
password = strings.TrimSuffix(password, "\n")
}
jf, err := mediabrowser.NewServer(
mediabrowser.JellyfinServer,
server,
"jfa-go-account-gen-script",
"0.0.1",
"testing",
"my_left_foot",
mediabrowser.NewNamedTimeoutHandler("Jellyfin Account Gen", "\""+server+"\"", true),
30,
)
if err != nil {
log.Fatalf("Failed to connect to Jellyin @ \"%s\": %+v\n", server, err)
}
_, status, err := jf.Authenticate(username, password)
if status != 200 || err != nil {
log.Fatalf("Failed to authenticate: %+v\n", err)
}
jfTemp, err := mediabrowser.NewServer(
mediabrowser.JellyfinServer,
server,
"jfa-go-account-gen-script",
"0.0.1",
"fake-activity",
"my_left_foot",
mediabrowser.NewNamedTimeoutHandler("Jellyfin Account Gen", "\""+server+"\"", true),
30,
)
if err != nil {
log.Fatalf("Failed to connect to Jellyin @ \"%s\": %+v\n", server, err)
}
rand.Seed(time.Now().Unix())
for i := 0; i < COUNT; i++ {
name := names[rand.Intn(len(names))] + strconv.Itoa(rand.Intn(100))
user, status, err := jf.NewUser(name, PASSWORD)
if (status != 200 && status != 201 && status != 204) || err != nil {
log.Fatalf("Failed to create user \"%s\" (%d): %+v\n", name, status, err)
}
if rand.Intn(100) > 65 {
user.Policy.IsAdministrator = true
}
if rand.Intn(100) > 80 {
user.Policy.IsDisabled = true
}
status, err = jf.SetPolicy(user.ID, user.Policy)
if (status != 200 && status != 201 && status != 204) || err != nil {
log.Fatalf("Failed to set policy for user \"%s\" (%d): %+v\n", name, status, err)
}
if rand.Intn(100) > 20 {
jfTemp.Authenticate(name, PASSWORD)
}
}
}

View File

@ -790,8 +790,7 @@ export class accountsList {
} }
} }
search = (query: String): string[] => { private _queries: { [field: string]: { name: string, getter: string, bool: boolean, string: boolean, date: boolean, dependsOnTableHeader?: string, show?: boolean }} = {
const queries: { [field: string]: { name: string, getter: string, bool: boolean, string: boolean, date: boolean }} = {
"id": { "id": {
name: "Jellyfin ID", name: "Jellyfin ID",
getter: "id", getter: "id",
@ -818,7 +817,8 @@ export class accountsList {
getter: "name", getter: "name",
bool: false, bool: false,
string: true, string: true,
date: false date: false,
show: false
}, },
"admin": { "admin": {
name: "Admin", name: "Admin",
@ -839,42 +839,48 @@ export class accountsList {
getter: "accounts_admin", getter: "accounts_admin",
bool: true, bool: true,
string: false, string: false,
date: false date: false,
dependsOnTableHeader: "accounts-header-access-jfa"
}, },
"email": { "email": {
name: "Email", name: "Email",
getter: "email", getter: "email",
bool: true, bool: true,
string: true, string: true,
date: false date: false,
dependsOnTableHeader: "accounts-header-email"
}, },
"telegram": { "telegram": {
name: "Telegram", name: "Telegram",
getter: "telegram", getter: "telegram",
bool: true, bool: true,
string: true, string: true,
date: false date: false,
dependsOnTableHeader: "accounts-header-telegram"
}, },
"matrix": { "matrix": {
name: "Matrix", name: "Matrix",
getter: "matrix", getter: "matrix",
bool: true, bool: true,
string: true, string: true,
date: false date: false,
dependsOnTableHeader: "accounts-header-matrix"
}, },
"discord": { "discord": {
name: "Discord", name: "Discord",
getter: "discord", getter: "discord",
bool: true, bool: true,
string: true, string: true,
date: false date: false,
dependsOnTableHeader: "accounts-header-discord"
}, },
"expiry": { "expiry": {
name: "Expiry", name: "Expiry",
getter: "expiry", getter: "expiry",
bool: true, bool: true,
string: false, string: false,
date: true date: true,
dependsOnTableHeader: "accounts-header-expiry"
}, },
"last-active": { "last-active": {
name: "Last Active", name: "Last Active",
@ -885,6 +891,7 @@ export class accountsList {
} }
} }
search = (query: String): string[] => {
const filterArea = document.getElementById("accounts-filter-area"); const filterArea = document.getElementById("accounts-filter-area");
filterArea.textContent = ""; filterArea.textContent = "";
@ -945,9 +952,9 @@ export class accountsList {
} }
const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":")+1)]; const split = [word.substring(0, word.indexOf(":")), word.substring(word.indexOf(":")+1)];
if (!(split[0] in queries)) continue; if (!(split[0] in this._queries)) continue;
const queryFormat = queries[split[0]]; const queryFormat = this._queries[split[0]];
if (queryFormat.bool) { if (queryFormat.bool) {
let isBool = false; let isBool = false;
@ -1004,7 +1011,8 @@ export class accountsList {
filterCard.addEventListener("click", () => { filterCard.addEventListener("click", () => {
for (let quote of [`"`, `'`, ``]) { for (let quote of [`"`, `'`, ``]) {
this._search.value = this._search.value.replace(split[0] + ":" + quote + split[1] + quote, ""); let regex = new RegExp(split[0] + ":" + quote + split[1] + quote, "ig");
this._search.value = this._search.value.replace(regex, "");
} }
this._search.oninput((null as Event)); this._search.oninput((null as Event));
}) })
@ -1024,6 +1032,7 @@ export class accountsList {
if (queryFormat.date) { if (queryFormat.date) {
// -1 = Before, 0 = On, 1 = After, 2 = No symbol, assume 0 // -1 = Before, 0 = On, 1 = After, 2 = No symbol, assume 0
let compareType = (split[1][0] == ">") ? 1 : ((split[1][0] == "<") ? -1 : ((split[1][0] == "=") ? 0 : 2)); let compareType = (split[1][0] == ">") ? 1 : ((split[1][0] == "<") ? -1 : ((split[1][0] == "=") ? 0 : 2));
let unmodifiedValue = split[1];
if (compareType != 2) { if (compareType != 2) {
split[1] = split[1].substring(1); split[1] = split[1].substring(1);
} }
@ -1046,7 +1055,8 @@ export class accountsList {
filterCard.addEventListener("click", () => { filterCard.addEventListener("click", () => {
for (let quote of [`"`, `'`, ``]) { for (let quote of [`"`, `'`, ``]) {
this._search.value = this._search.value.replace(split[0] + ":" + quote + split[1] + quote, ""); let regex = new RegExp(split[0] + ":" + quote + unmodifiedValue + quote, "ig");
this._search.value = this._search.value.replace(regex, "");
} }
this._search.oninput((null as Event)); this._search.oninput((null as Event));
@ -1823,7 +1833,7 @@ export class accountsList {
const headerNames: string[] = ["username", "access-jfa", "email", "telegram", "matrix", "discord", "expiry", "last-active"]; const headerNames: string[] = ["username", "access-jfa", "email", "telegram", "matrix", "discord", "expiry", "last-active"];
const headerGetters: string[] = ["name", "accounts_admin", "email", "telegram", "matrix", "discord", "expiry", "last_active"]; const headerGetters: string[] = ["name", "accounts_admin", "email", "telegram", "matrix", "discord", "expiry", "last_active"];
for (let i = 0; i < headerNames.length; i++) { for (let i = 0; i < headerNames.length; i++) {
const header: HTMLTableHeaderCellElement = document.getElementsByClassName("accounts-header-" + headerNames[i])[0] as HTMLTableHeaderCellElement; const header: HTMLTableHeaderCellElement = document.querySelector(".accounts-header-" + headerNames[i]) as HTMLTableHeaderCellElement;
if (header !== null) { if (header !== null) {
this._columns[header.className] = new Column(header, Object.getOwnPropertyDescriptor(user.prototype, headerGetters[i]).get); this._columns[header.className] = new Column(header, Object.getOwnPropertyDescriptor(user.prototype, headerGetters[i]).get);
} }
@ -1854,6 +1864,85 @@ export class accountsList {
}); });
defaultSort(); defaultSort();
const filterList = document.getElementById("accounts-filter-list");
const fillInFilter = (name: string, value: string, offset?: number) => {
this._search.value = name + ":" + value + " " + this._search.value;
this._search.focus();
let newPos = name.length + 1 + value.length;
if (typeof offset !== 'undefined')
newPos += offset;
this._search.setSelectionRange(newPos, newPos);
this._search.oninput(null as any);
};
// Generate filter buttons
for (let queryName of Object.keys(this._queries)) {
const query = this._queries[queryName];
if ("show" in query && !query.show) continue;
if ("dependsOnTableHeader" in query && query.dependsOnTableHeader) {
const el = document.querySelector("."+query.dependsOnTableHeader);
if (el === null) continue;
}
const container = document.createElement("span") as HTMLSpanElement;
container.classList.add("button", "button-xl", "~neutral", "@low", "mb-1", "mr-2");
container.innerHTML = `<span class="mr-2">${query.name}</span>`;
if (query.bool) {
const pos = document.createElement("button") as HTMLButtonElement;
pos.type = "button";
pos.ariaLabel = `Filter by "${query.name}": True`;
pos.classList.add("button", "~positive", "ml-2");
pos.innerHTML = `<i class="ri-checkbox-circle-fill"></i>`;
pos.addEventListener("click", () => fillInFilter(queryName, "true"));
const neg = document.createElement("button") as HTMLButtonElement;
neg.type = "button";
neg.ariaLabel = `Filter by "${query.name}": False`;
neg.classList.add("button", "~critical", "ml-2");
neg.innerHTML = `<i class="ri-close-circle-fill"></i>`;
neg.addEventListener("click", () => fillInFilter(queryName, "false"));
container.appendChild(pos);
container.appendChild(neg);
}
if (query.string) {
const button = document.createElement("button") as HTMLButtonElement;
button.type = "button";
button.classList.add("button", "~urge", "ml-2");
button.innerHTML = `<i class="ri-equal-line mr-2"></i>Match Text`;
// Position cursor between quotes
button.addEventListener("click", () => fillInFilter(queryName, `""`, -1));
container.appendChild(button);
}
if (query.date) {
const onDate = document.createElement("button") as HTMLButtonElement;
onDate.type = "button";
onDate.classList.add("button", "~urge", "ml-2");
onDate.innerHTML = `<i class="ri-calendar-check-line mr-2"></i>On Date`;
onDate.addEventListener("click", () => fillInFilter(queryName, `"="`, -1));
const beforeDate = document.createElement("button") as HTMLButtonElement;
beforeDate.type = "button";
beforeDate.classList.add("button", "~urge", "ml-2");
beforeDate.innerHTML = `<i class="ri-calendar-check-line mr-2"></i>Before Date`;
beforeDate.addEventListener("click", () => fillInFilter(queryName, `"<"`, -1));
const afterDate = document.createElement("button") as HTMLButtonElement;
afterDate.type = "button";
afterDate.classList.add("button", "~urge", "ml-2");
afterDate.innerHTML = `<i class="ri-calendar-check-line mr-2"></i>After Date`;
afterDate.addEventListener("click", () => fillInFilter(queryName, `">"`, -1));
container.appendChild(onDate);
container.appendChild(beforeDate);
container.appendChild(afterDate);
}
filterList.appendChild(container);
}
} }
reload = () => { reload = () => {