Compare commits
10 Commits
cd67d3e7ab
...
618cc32a17
Author | SHA1 | Date | |
---|---|---|---|
618cc32a17 | |||
a8bf670697 | |||
0bdf8ad6ce | |||
8f65e2e968 | |||
0d3f96c3a7 | |||
cfa7947020 | |||
b91de3f319 | |||
1704ae8cb1 | |||
|
50c6e6031d | ||
de92516d52 |
15
.drone.yml
@ -20,12 +20,12 @@ steps:
|
|||||||
- apt install build-essential python3-pip curl software-properties-common sed upx -y
|
- apt install build-essential python3-pip curl software-properties-common sed upx -y
|
||||||
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
|
- (curl -sL https://deb.nodesource.com/setup_14.x | bash -)
|
||||||
- apt install nodejs
|
- apt install nodejs
|
||||||
- curl -sL https://git.io/goreleaser > goreleaser
|
- curl -sL https://git.io/goreleaser > ../goreleaser
|
||||||
- chmod +x goreleaser
|
- chmod +x ../goreleaser
|
||||||
- ./scripts/version.sh ./goreleaser
|
- ./scripts/version.sh ../goreleaser
|
||||||
- wget https://builds.hrfee.pw/upload.py
|
- wget https://builds.hrfee.pw/upload.py -P ../
|
||||||
- pip3 install requests
|
- pip3 install requests
|
||||||
- bash -c 'python3 upload.py https://builds.hrfee.pw hrfee jfa-go --tag internal=true'
|
- bash -c 'python3 ../upload.py https://builds.hrfee.pw hrfee jfa-go --tag internal=true'
|
||||||
trigger:
|
trigger:
|
||||||
event:
|
event:
|
||||||
- tag
|
- tag
|
||||||
@ -40,9 +40,6 @@ steps:
|
|||||||
volumes:
|
volumes:
|
||||||
- name: ssh_key
|
- name: ssh_key
|
||||||
path: /root/drone_rsa
|
path: /root/drone_rsa
|
||||||
environment:
|
|
||||||
BUILDRONE_KEY:
|
|
||||||
from_secret: BUILDRONE_KEY
|
|
||||||
settings:
|
settings:
|
||||||
host:
|
host:
|
||||||
from_secret: ssh2_host
|
from_secret: ssh2_host
|
||||||
@ -54,8 +51,6 @@ steps:
|
|||||||
- /root/.ssh/docker-build:/root/drone_rsa
|
- /root/.ssh/docker-build:/root/drone_rsa
|
||||||
key_path: /root/drone_rsa
|
key_path: /root/drone_rsa
|
||||||
command_timeout: 50m
|
command_timeout: 50m
|
||||||
envs:
|
|
||||||
- BUILDRONE_KEY
|
|
||||||
script:
|
script:
|
||||||
- /mnt/buildx/jfa-go/build.sh stable
|
- /mnt/buildx/jfa-go/build.sh stable
|
||||||
- wget https://builds.hrfee.pw/upload.py -O /mnt/buildx/jfa-go/jfa-go/upload.py
|
- wget https://builds.hrfee.pw/upload.py -O /mnt/buildx/jfa-go/jfa-go/upload.py
|
||||||
|
@ -16,6 +16,7 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
|
|||||||
* Granular control over invites: Validity period as well as number of uses can be specified.
|
* 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.
|
* 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.
|
* Password validation: Ensure users choose a strong password.
|
||||||
|
* ⌛ User expiry: Specify a validity period, and new user's 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.
|
* 🔗 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.
|
* Account management: Apply settings to your users individually or en masse, and delete users, optionally sending them an email notification with a reason.
|
||||||
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
|
* 📨 Email storage: Add your existing users email addresses through the UI, and jfa-go will ask new users for them on account creation.
|
||||||
@ -26,6 +27,7 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
|
|||||||
* Authentication via Jellyfin: Instead of using separate credentials for jfa-go and Jellyfin, jfa-go can use it as the authentication provider.
|
* 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
|
* Enables the usage of jfa-go by multiple people
|
||||||
* 🌓 Customizable look
|
* 🌓 Customizable look
|
||||||
|
* Edit emails with variables and markdown
|
||||||
* Specify contact and help messages to appear in emails and pages
|
* Specify contact and help messages to appear in emails and pages
|
||||||
* Light and dark themes available
|
* Light and dark themes available
|
||||||
|
|
||||||
@ -35,8 +37,9 @@ I chose to rewrite the python [jellyfin-accounts](https://github.com/hrfee/jelly
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="images/invites.png" width="48%" style="margin-left: 1.5%;" alt="Invites tab"></img>
|
<img src="images/invites.png" width="31%" style="margin-left: 1.5%;" alt="Invites tab"></img>
|
||||||
<img src="images/accounts.png" width="48%" style="margin-right: 1.5%;" alt="Accounts tab"></img>
|
<img src="images/accounts.png" width="31%" style="margin-right: 1.5%;" alt="Accounts tab"></img>
|
||||||
|
<img src="images/create.png" width="31%" style="margin-right: 1.5%;" alt="Accounts creation"></img>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
#### Install
|
#### Install
|
||||||
|
9
api.go
@ -1246,6 +1246,15 @@ func (app *appContext) GetConfig(gc *gin.Context) {
|
|||||||
el := resp.Sections["email"].Settings["language"]
|
el := resp.Sections["email"].Settings["language"]
|
||||||
el.Options = emailOptions
|
el.Options = emailOptions
|
||||||
el.Value = app.config.Section("email").Key("language").MustString("en-us")
|
el.Value = app.config.Section("email").Key("language").MustString("en-us")
|
||||||
|
if updater == "" {
|
||||||
|
delete(resp.Sections, "updates")
|
||||||
|
for i, v := range resp.Order {
|
||||||
|
if v == "updates" {
|
||||||
|
resp.Order = append(resp.Order[:i], resp.Order[i+1:]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
for sectName, section := range resp.Sections {
|
for sectName, section := range resp.Sections {
|
||||||
for settingName, setting := range section.Settings {
|
for settingName, setting := range section.Settings {
|
||||||
val := app.config.Section(sectName).Key(settingName)
|
val := app.config.Section(sectName).Key(settingName)
|
||||||
|
11
css/base.css
@ -153,6 +153,10 @@ div.card:contains(section.banner.footer) {
|
|||||||
margin: 0.5rem;
|
margin: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.col.sm {
|
||||||
|
margin: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.flex-col {
|
.flex-col {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -434,18 +438,19 @@ pre {
|
|||||||
overflow-y: scroll;
|
overflow-y: scroll;
|
||||||
}
|
}
|
||||||
|
|
||||||
a:link {
|
a:link:not(.lang-link):not(.\~urge) {
|
||||||
color: var(--color-urge-200);
|
color: var(--color-urge-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
a:visited {
|
a:visited:not(.lang-link):not(.\~urge) {
|
||||||
color: var(--color-urge-100);
|
color: var(--color-urge-100);
|
||||||
}
|
}
|
||||||
|
|
||||||
a:hover, a:active {
|
a:hover:not(.lang-link):not(.\~urge), a:active:not(.lang-link):not(.\~urge) {
|
||||||
color: var(--color-urge-200);
|
color: var(--color-urge-200);
|
||||||
}
|
}
|
||||||
|
|
||||||
.search {
|
.search {
|
||||||
max-width: 15rem;
|
max-width: 15rem;
|
||||||
|
min-width: 10rem;
|
||||||
}
|
}
|
||||||
|
@ -305,12 +305,12 @@
|
|||||||
<span class="heading">{{ .strings.create }}</span>
|
<span class="heading">{{ .strings.create }}</span>
|
||||||
<div class="row" id="create-inv">
|
<div class="row" id="create-inv">
|
||||||
<div class="card ~neutral !normal col">
|
<div class="card ~neutral !normal col">
|
||||||
<div class="flex-row mb-1">
|
<div class="row mb-1">
|
||||||
<label class="flex-row-group mr-1">
|
<label class="col mr-1">
|
||||||
<input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked>
|
<input type="radio" name="duration" class="unfocused" id="radio-inv-duration" checked>
|
||||||
<span class="button ~neutral !high supra full-width center">{{ .strings.inviteDuration }}</span>
|
<span class="button ~neutral !high supra full-width center">{{ .strings.inviteDuration }}</span>
|
||||||
</label>
|
</label>
|
||||||
<label class="flex-row-group ml-1">
|
<label class="col ml-1">
|
||||||
<input type="radio" name="duration" class="unfocused" id="radio-user-expiry">
|
<input type="radio" name="duration" class="unfocused" id="radio-user-expiry">
|
||||||
<span class="button ~neutral !normal supra full-width center">{{ .strings.userExpiry }}</span>
|
<span class="button ~neutral !normal supra full-width center">{{ .strings.userExpiry }}</span>
|
||||||
</label>
|
</label>
|
||||||
@ -396,17 +396,17 @@
|
|||||||
</div>
|
</div>
|
||||||
<div id="tab-accounts" class="unfocused">
|
<div id="tab-accounts" class="unfocused">
|
||||||
<div class="card ~neutral !low accounts mb-1">
|
<div class="card ~neutral !low accounts mb-1">
|
||||||
<div class="flex-expand">
|
<div class="flex-expand row">
|
||||||
<div class="flex-row">
|
<div class="row">
|
||||||
<span class="heading">{{ .strings.accounts }}</span>
|
<span class="heading mr-1 col sm">{{ .strings.accounts }}</span>
|
||||||
<input type="search" class="field ~neutral !normal input search ml-1" id="accounts-search" placeholder="{{ .strings.search }}">
|
<input type="search" class="col sm field ~neutral !normal input search ml-1 mr-1" id="accounts-search" placeholder="{{ .strings.search }}">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div class="row">
|
||||||
<span class="button ~neutral !normal mb-half" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
|
<span class="col sm button ~neutral !normal center mb-half" id="accounts-add-user">{{ .quantityStrings.addUser.Singular }}</span>
|
||||||
<span class="button ~info !normal mb-half" id="accounts-announce">{{ .strings.announce }}</span>
|
<span class="col sm button ~info !normal center mb-half" id="accounts-announce">{{ .strings.announce }}</span>
|
||||||
<span class="button ~urge !normal mb-half" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
|
<span class="col sm button ~urge !normal center mb-half" id="accounts-modify-user">{{ .strings.modifySettings }}</span>
|
||||||
<span class="button ~warning !normal mb-half" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
|
<span class="col sm button ~warning !normal center mb-half" id="accounts-extend-expiry">{{ .strings.extendExpiry }}</span>
|
||||||
<span class="button ~critical !normal mb-half" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
|
<span class="col sm button ~critical !normal center mb-half" id="accounts-delete-user">{{ .quantityStrings.deleteUser.Singular }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card ~neutral !normal accounts-header table-responsive mt-half">
|
<div class="card ~neutral !normal accounts-header table-responsive mt-half">
|
||||||
|
@ -82,6 +82,20 @@
|
|||||||
<span class="mt-half">{{ .lang.General.pathToKeyFile }}</span>
|
<span class="mt-half">{{ .lang.General.pathToKeyFile }}</span>
|
||||||
<input type="text" class="input ~neutral !normal mt-half mb-1" id="advanced-tls_key">
|
<input type="text" class="input ~neutral !normal mt-half mb-1" id="advanced-tls_key">
|
||||||
</label>
|
</label>
|
||||||
|
<span class="heading">{{ .lang.Updates.title }}</span>
|
||||||
|
<p class="content" id="updates-description"></p>
|
||||||
|
<label class="row switch pb-1">
|
||||||
|
<input type="checkbox" id="updates-enabled" checked><span>{{ .lang.Strings.enabled }}</span>
|
||||||
|
</label>
|
||||||
|
<label class="label">
|
||||||
|
<span>{{ .lang.Updates.updateChannel }}</span>
|
||||||
|
<div class="select ~neutral !normal mt-half mb-1">
|
||||||
|
<select id="updates-channel">
|
||||||
|
<option value="stable">{{ .lang.Updates.stable }}</option>
|
||||||
|
<option value="unstable">{{ .lang.Updates.unstable }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<label class="label">
|
<label class="label">
|
||||||
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 50 KiB |
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 80 KiB |
BIN
images/demo.gif
Before Width: | Height: | Size: 2.4 MiB After Width: | Height: | Size: 1.9 MiB |
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 83 KiB |
1
lang.go
@ -94,6 +94,7 @@ type setupLang struct {
|
|||||||
StartPage langSection `json:"startPage"`
|
StartPage langSection `json:"startPage"`
|
||||||
EndPage langSection `json:"endPage"`
|
EndPage langSection `json:"endPage"`
|
||||||
General langSection `json:"general"`
|
General langSection `json:"general"`
|
||||||
|
Updates langSection `json:"updates"`
|
||||||
Language langSection `json:"language"`
|
Language langSection `json:"language"`
|
||||||
Login langSection `json:"login"`
|
Login langSection `json:"login"`
|
||||||
JellyfinEmby langSection `json:"jellyfinEmby"`
|
JellyfinEmby langSection `json:"jellyfinEmby"`
|
||||||
|
@ -87,7 +87,8 @@
|
|||||||
"updates": "Updates",
|
"updates": "Updates",
|
||||||
"update": "Bijwerken",
|
"update": "Bijwerken",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
"search": "Zoeken"
|
"search": "Zoeken",
|
||||||
|
"advancedSettings": "Geavanceerde instellingen"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"changedEmailAddress": "E-mailadres van {n} gewijzigd.",
|
"changedEmailAddress": "E-mailadres van {n} gewijzigd.",
|
||||||
|
@ -48,6 +48,13 @@
|
|||||||
"pathToCertificate": "Path to certificate",
|
"pathToCertificate": "Path to certificate",
|
||||||
"pathToKeyFile": "Path to key file"
|
"pathToKeyFile": "Path to key file"
|
||||||
},
|
},
|
||||||
|
"updates": {
|
||||||
|
"title": "Updates",
|
||||||
|
"description": "Enable to be notified when new updates are available. jfa-go will check {n} every 30 minutes. No IPs or personally identifiable information are collected.",
|
||||||
|
"updateChannel": "Update Channel",
|
||||||
|
"stable": "Stable",
|
||||||
|
"unstable": "Unstable"
|
||||||
|
},
|
||||||
"login": {
|
"login": {
|
||||||
"title": "Login",
|
"title": "Login",
|
||||||
"description": "To access the admin page, you need to login with a method below:",
|
"description": "To access the admin page, you need to login with a method below:",
|
||||||
|
56
main.go
@ -152,7 +152,8 @@ func start(asDaemon, firstCall bool) {
|
|||||||
set default config and data paths
|
set default config and data paths
|
||||||
data: Contains invites.json, emails.json, user_profile.json, etc.
|
data: Contains invites.json, emails.json, user_profile.json, etc.
|
||||||
config: config.ini. Usually in data, but can be changed via -config.
|
config: config.ini. Usually in data, but can be changed via -config.
|
||||||
localFS is jfa-go's internal data. On external builds, the directory is named "data" and placed next to the executable.
|
localFS: jfa-go's internal data. On internal builds, this is contained within the binary.
|
||||||
|
On external builds, the directory is named "data" and placed next to the executable.
|
||||||
*/
|
*/
|
||||||
userConfigDir, _ := os.UserConfigDir()
|
userConfigDir, _ := os.UserConfigDir()
|
||||||
app.dataPath = filepath.Join(userConfigDir, "jfa-go")
|
app.dataPath = filepath.Join(userConfigDir, "jfa-go")
|
||||||
@ -164,7 +165,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.info = NewLogger(os.Stdout, "[INFO] ", log.Ltime, color.FgHiWhite)
|
app.info = NewLogger(os.Stdout, "[INFO] ", log.Ltime, color.FgHiWhite)
|
||||||
app.err = NewLogger(os.Stdout, "[ERROR] ", log.Ltime|log.Lshortfile, color.FgRed)
|
app.err = NewLogger(os.Stdout, "[ERROR] ", log.Ltime, color.FgRed)
|
||||||
|
|
||||||
if firstCall {
|
if firstCall {
|
||||||
DATA = flag.String("data", app.dataPath, "alternate path to data directory.")
|
DATA = flag.String("data", app.dataPath, "alternate path to data directory.")
|
||||||
@ -200,8 +201,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.dataPath = *DATA
|
app.dataPath = *DATA
|
||||||
}
|
}
|
||||||
|
|
||||||
// env variables are necessary because syscall.Exec for self-restarts doesn't doesn't work with arguments for some reason.
|
// Previously used for self-restarts but leaving them here as they might be useful.
|
||||||
|
|
||||||
if v := os.Getenv("JFA_CONFIGPATH"); v != "" {
|
if v := os.Getenv("JFA_CONFIGPATH"); v != "" {
|
||||||
app.configPath = v
|
app.configPath = v
|
||||||
}
|
}
|
||||||
@ -254,6 +254,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.debug = emptyLogger(false)
|
app.debug = emptyLogger(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Starts listener to receive commands over a unix socket. Use with 'jfa-go start/stop'
|
||||||
if asDaemon {
|
if asDaemon {
|
||||||
go func() {
|
go func() {
|
||||||
socket := SOCK
|
socket := SOCK
|
||||||
@ -384,6 +385,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read config-base for settings on web.
|
||||||
app.configBasePath = "config-base.json"
|
app.configBasePath = "config-base.json"
|
||||||
configBase, _ := fs.ReadFile(localFS, app.configBasePath)
|
configBase, _ := fs.ReadFile(localFS, app.configBasePath)
|
||||||
json.Unmarshal(configBase, &app.configBase)
|
json.Unmarshal(configBase, &app.configBase)
|
||||||
@ -392,6 +394,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
"Jellyfin (Dark)": "dark-theme",
|
"Jellyfin (Dark)": "dark-theme",
|
||||||
"Default (Light)": "light-theme",
|
"Default (Light)": "light-theme",
|
||||||
}
|
}
|
||||||
|
// For move from Bootstrap to a17t
|
||||||
if app.config.Section("ui").Key("theme").String() == "Bootstrap (Light)" {
|
if app.config.Section("ui").Key("theme").String() == "Bootstrap (Light)" {
|
||||||
app.config.Section("ui").Key("theme").SetValue("Default (Light)")
|
app.config.Section("ui").Key("theme").SetValue("Default (Light)")
|
||||||
}
|
}
|
||||||
@ -403,18 +406,8 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.err.Fatal(err)
|
app.err.Fatal(err)
|
||||||
}
|
}
|
||||||
os.Setenv("JFA_SECRET", secret)
|
os.Setenv("JFA_SECRET", secret)
|
||||||
app.jellyfinLogin = true
|
|
||||||
if val, _ := app.config.Section("ui").Key("jellyfin_login").Bool(); !val {
|
|
||||||
app.jellyfinLogin = false
|
|
||||||
user := User{}
|
|
||||||
user.UserID = shortuuid.New()
|
|
||||||
user.Username = app.config.Section("ui").Key("username").String()
|
|
||||||
user.Password = app.config.Section("ui").Key("password").String()
|
|
||||||
app.users = append(app.users, user)
|
|
||||||
} else {
|
|
||||||
app.debug.Println("Using Jellyfin for authentication")
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Initialize jellyfin/emby connection
|
||||||
server := app.config.Section("jellyfin").Key("server").String()
|
server := app.config.Section("jellyfin").Key("server").String()
|
||||||
cacheTimeout := int(app.config.Section("jellyfin").Key("cache_timeout").MustUint(30))
|
cacheTimeout := int(app.config.Section("jellyfin").Key("cache_timeout").MustUint(30))
|
||||||
stringServerType := app.config.Section("jellyfin").Key("type").String()
|
stringServerType := app.config.Section("jellyfin").Key("type").String()
|
||||||
@ -444,7 +437,8 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status)
|
app.err.Fatalf("Failed to authenticate with Jellyfin @ %s: Code %d", server, status)
|
||||||
}
|
}
|
||||||
app.info.Printf("Authenticated with %s", server)
|
app.info.Printf("Authenticated with %s", server)
|
||||||
// from 10.7.0, jellyfin may hyphenate user IDs. This checks if the version is equal or higher.
|
/* A couple of unstable Jellyfin 10.7.0 releases decided to hyphenate user IDs.
|
||||||
|
This checks if the version is equal or higher. */
|
||||||
checkVersion := func(version string) int {
|
checkVersion := func(version string) int {
|
||||||
numberStrings := strings.Split(version, ".")
|
numberStrings := strings.Split(version, ".")
|
||||||
n := 0
|
n := 0
|
||||||
@ -498,27 +492,40 @@ func start(asDaemon, firstCall bool) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Auth (manual user/pass or jellyfin)
|
||||||
|
app.jellyfinLogin = true
|
||||||
|
if jfLogin, _ := app.config.Section("ui").Key("jellyfin_login").Bool(); !jfLogin {
|
||||||
|
app.jellyfinLogin = false
|
||||||
|
user := User{}
|
||||||
|
user.UserID = shortuuid.New()
|
||||||
|
user.Username = app.config.Section("ui").Key("username").String()
|
||||||
|
user.Password = app.config.Section("ui").Key("password").String()
|
||||||
|
app.users = append(app.users, user)
|
||||||
|
} else {
|
||||||
|
app.debug.Println("Using Jellyfin for authentication")
|
||||||
|
app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout)
|
||||||
|
}
|
||||||
|
|
||||||
// Since email depends on language, the email reload in loadConfig won't work first time.
|
// Since email depends on language, the email reload in loadConfig won't work first time.
|
||||||
app.email = NewEmailer(app)
|
app.email = NewEmailer(app)
|
||||||
|
|
||||||
app.authJf, _ = mediabrowser.NewServer(serverType, server, "jfa-go", app.version, "auth", "auth", timeoutHandler, cacheTimeout)
|
|
||||||
|
|
||||||
app.loadStrftime()
|
app.loadStrftime()
|
||||||
|
|
||||||
validatorConf := ValidatorConf{
|
var validatorConf ValidatorConf
|
||||||
|
|
||||||
|
if !app.config.Section("password_validation").Key("enabled").MustBool(false) {
|
||||||
|
validatorConf = ValidatorConf{}
|
||||||
|
} else {
|
||||||
|
validatorConf = ValidatorConf{
|
||||||
"length": app.config.Section("password_validation").Key("min_length").MustInt(0),
|
"length": app.config.Section("password_validation").Key("min_length").MustInt(0),
|
||||||
"uppercase": app.config.Section("password_validation").Key("upper").MustInt(0),
|
"uppercase": app.config.Section("password_validation").Key("upper").MustInt(0),
|
||||||
"lowercase": app.config.Section("password_validation").Key("lower").MustInt(0),
|
"lowercase": app.config.Section("password_validation").Key("lower").MustInt(0),
|
||||||
"number": app.config.Section("password_validation").Key("number").MustInt(0),
|
"number": app.config.Section("password_validation").Key("number").MustInt(0),
|
||||||
"special": app.config.Section("password_validation").Key("special").MustInt(0),
|
"special": app.config.Section("password_validation").Key("special").MustInt(0),
|
||||||
}
|
}
|
||||||
if !app.config.Section("password_validation").Key("enabled").MustBool(false) {
|
|
||||||
for key := range validatorConf {
|
|
||||||
validatorConf[key] = 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
app.validator.init(validatorConf)
|
app.validator.init(validatorConf)
|
||||||
|
|
||||||
|
// Test mode for testing connection to Jellyfin, accessed with 'jfa-go test'
|
||||||
if TEST {
|
if TEST {
|
||||||
test(app)
|
test(app)
|
||||||
os.Exit(0)
|
os.Exit(0)
|
||||||
@ -546,6 +553,7 @@ func start(asDaemon, firstCall bool) {
|
|||||||
app.info.Fatalf("Failed to load language files: %+v\n", err)
|
app.info.Fatalf("Failed to load language files: %+v\n", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cssHeader = app.loadCSSHeader()
|
cssHeader = app.loadCSSHeader()
|
||||||
// workaround for potentially broken windows mime types
|
// workaround for potentially broken windows mime types
|
||||||
mime.AddExtensionType(".js", "application/javascript")
|
mime.AddExtensionType(".js", "application/javascript")
|
||||||
|
1
setup.go
@ -89,6 +89,7 @@ func (st *Storage) loadLangSetup(filesystems ...fs.FS) error {
|
|||||||
if fname != "en-us.json" {
|
if fname != "en-us.json" {
|
||||||
patchLang(&english.Strings, &lang.Strings)
|
patchLang(&english.Strings, &lang.Strings)
|
||||||
patchLang(&english.StartPage, &lang.StartPage)
|
patchLang(&english.StartPage, &lang.StartPage)
|
||||||
|
patchLang(&english.Updates, &lang.Updates)
|
||||||
patchLang(&english.EndPage, &lang.EndPage)
|
patchLang(&english.EndPage, &lang.EndPage)
|
||||||
patchLang(&english.Language, &lang.Language)
|
patchLang(&english.Language, &lang.Language)
|
||||||
patchLang(&english.Login, &lang.Login)
|
patchLang(&english.Login, &lang.Login)
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hrfee/jfa-go/mediabrowser"
|
"github.com/hrfee/jfa-go/mediabrowser"
|
||||||
@ -25,6 +26,7 @@ type Storage struct {
|
|||||||
policy mediabrowser.Policy
|
policy mediabrowser.Policy
|
||||||
configuration mediabrowser.Configuration
|
configuration mediabrowser.Configuration
|
||||||
lang Lang
|
lang Lang
|
||||||
|
invitesLock sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
type customEmails struct {
|
type customEmails struct {
|
||||||
@ -402,10 +404,14 @@ func (st *Storage) loadLangEmail(filesystems ...fs.FS) error {
|
|||||||
type Invites map[string]Invite
|
type Invites map[string]Invite
|
||||||
|
|
||||||
func (st *Storage) loadInvites() error {
|
func (st *Storage) loadInvites() error {
|
||||||
|
st.invitesLock.Lock()
|
||||||
|
defer st.invitesLock.Unlock()
|
||||||
return loadJSON(st.invite_path, &st.invites)
|
return loadJSON(st.invite_path, &st.invites)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (st *Storage) storeInvites() error {
|
func (st *Storage) storeInvites() error {
|
||||||
|
st.invitesLock.Lock()
|
||||||
|
defer st.invitesLock.Unlock()
|
||||||
return storeJSON(st.invite_path, st.invites)
|
return storeJSON(st.invite_path, st.invites)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -56,7 +56,7 @@ export const loadLangSelector = (page: string) => _get("/lang/" + page, null, (r
|
|||||||
const list = document.getElementById("lang-list") as HTMLDivElement;
|
const list = document.getElementById("lang-list") as HTMLDivElement;
|
||||||
let innerHTML = '';
|
let innerHTML = '';
|
||||||
for (let code in req.response) {
|
for (let code in req.response) {
|
||||||
innerHTML += `<a href="?lang=${code}" class="button input ~neutral field mb-half">${req.response[code]}</a>`;
|
innerHTML += `<a href="?lang=${code}" class="button input ~neutral field mb-half lang-link">${req.response[code]}</a>`;
|
||||||
}
|
}
|
||||||
list.innerHTML = innerHTML;
|
list.innerHTML = innerHTML;
|
||||||
}
|
}
|
||||||
|
@ -173,7 +173,9 @@ class Select {
|
|||||||
if (depends) {
|
if (depends) {
|
||||||
document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => {
|
document.addEventListener(`settings-${section}-${depends}`, (event: boolEvent) => {
|
||||||
let el = this._el as HTMLElement;
|
let el = this._el as HTMLElement;
|
||||||
if (el.parentElement.tagName == "LABEL") { el = el.parentElement; }
|
while (el.tagName != "LABEL") {
|
||||||
|
el = el.parentElement;
|
||||||
|
}
|
||||||
if (event.detail !== dependsTrue) {
|
if (event.detail !== dependsTrue) {
|
||||||
el.classList.add("unfocused");
|
el.classList.add("unfocused");
|
||||||
} else {
|
} else {
|
||||||
@ -202,6 +204,7 @@ window.lang = new lang(window.langFile as LangFile);
|
|||||||
html("language-description", window.lang.var("language", "description", `<a href="https://weblate.hrfee.pw">Weblate</a>`));
|
html("language-description", window.lang.var("language", "description", `<a href="https://weblate.hrfee.pw">Weblate</a>`));
|
||||||
html("email-description", window.lang.var("email", "description", `<a href="https://mailgun.com">Mailgun</a>`));
|
html("email-description", window.lang.var("email", "description", `<a href="https://mailgun.com">Mailgun</a>`));
|
||||||
html("email-dateformat-notice", window.lang.var("email", "dateFormatNotice", `<a href="https://strftime.ninja/">strftime.ninja</a>`));
|
html("email-dateformat-notice", window.lang.var("email", "dateFormatNotice", `<a href="https://strftime.ninja/">strftime.ninja</a>`));
|
||||||
|
html("updates-description", window.lang.var("updates", "description", `<a href="https://builds.hrfee.dev/view/hrfee/jfa-go">buildrone</a>`));
|
||||||
|
|
||||||
const settings = {
|
const settings = {
|
||||||
"jellyfin": {
|
"jellyfin": {
|
||||||
@ -212,6 +215,10 @@ const settings = {
|
|||||||
"password": new Input(get("jellyfin-password")),
|
"password": new Input(get("jellyfin-password")),
|
||||||
"substitute_jellyfin_strings": new Input(get("jellyfin-substitute_jellyfin_strings"))
|
"substitute_jellyfin_strings": new Input(get("jellyfin-substitute_jellyfin_strings"))
|
||||||
},
|
},
|
||||||
|
"updates": {
|
||||||
|
"enabled": new Checkbox(get("updates-enabled"), "", false, "updates", "enabled"),
|
||||||
|
"channel": new Select(get("updates-channel"), "enabled", true, "updates")
|
||||||
|
},
|
||||||
"ui": {
|
"ui": {
|
||||||
"host": new Input(get("ui-host")),
|
"host": new Input(get("ui-host")),
|
||||||
"port": new Input(get("ui-port")),
|
"port": new Input(get("ui-port")),
|
||||||
|
@ -467,7 +467,7 @@ func (app *appContext) checkForUpdates() {
|
|||||||
if status != 200 || err != nil {
|
if status != 200 || err != nil {
|
||||||
if err != nil && strings.Contains(err.Error(), "strconv.ParseInt") {
|
if err != nil && strings.Contains(err.Error(), "strconv.ParseInt") {
|
||||||
app.err.Println("No new updates available.")
|
app.err.Println("No new updates available.")
|
||||||
} else {
|
} else if status != -1 { // -1 means updates disabled, we don't need to log it.
|
||||||
app.err.Printf("Failed to get latest tag (%d): %v", status, err)
|
app.err.Printf("Failed to get latest tag (%d): %v", status, err)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|