Merge pull request #736 from sbondCo/proxy-auth-flow

Trusted header authentication
This commit is contained in:
Mr
2024-12-29 22:51:33 +00:00
committed by GitHub
15 changed files with 639 additions and 34 deletions
+4 -1
View File
@@ -25,10 +25,12 @@ import (
type UserType uint8
// Assume watcharr user if none of these...
var (
// Assume watcharr user if none of these...
JELLYFIN_USER UserType = 1
PLEX_USER UserType = 2
// Registered via trusted header auth
PROXY_USER UserType = 3
)
// User Perms
@@ -160,6 +162,7 @@ type AvailableAuthProvidersResponse struct {
SignupEnabled bool `json:"signupEnabled"`
IsInSetup bool `json:"isInSetup"`
UseEmby bool `json:"useEmby"`
HeaderAuthAutoLogin bool `json:"headerAuthAutoLogin"`
}
type ArgonParams struct {
+93
View File
@@ -0,0 +1,93 @@
// auth_proxy contains the logic for Trusted Header Authentication/SSO.
//
// This code is inherently dangerous since we are implicitly trusting
// a header for auth, so this should only be configured if you are
// certain your watcharr instance is only available behind your proxy.
package main
import (
"errors"
"log/slog"
"gorm.io/gorm"
)
type TrustedHeaderAuthSetting struct {
// Required: Should header auth be enabled?
// This bool exists so header auth can be toggled
// easily without having to remove configuration.
// To be actually enabled, HEADER_NAME must also
// be set.
Enabled bool `json:"enabled,omitempty"`
// Required: What is the name of the trusted header
// that will contain the logged in users username?
HeaderName string `json:"headerName,omitempty"`
// Should the frontend attempt auto login if
// trusted header auth is enabled.
AutoLogin bool `json:"autoLogin,omitempty"`
// Where can we redirect the user to logout
// of the auth service?
LogoutUrl string `json:"logoutUrl,omitempty"`
}
type TrustedHeaderAuthLogoutDetailsResponse struct {
LogoutUrl string `json:"logoutUrl,omitempty"`
}
// Is trusted header auth configured on this server?
func trustedHeaderAuthIsEnabled() bool {
return Config.HEADER_AUTH.Enabled && Config.HEADER_AUTH.HeaderName != ""
}
func setTrustedHeaderAuthSetting(has TrustedHeaderAuthSetting) error {
slog.Debug("setTrustedHeaderAuthSetting: Attempting to update to new provided value", "new_value", has)
Config.HEADER_AUTH = has
err := writeConfig()
if err != nil {
slog.Error("setTrustedHeaderAuthSetting: Failed to write updated config!", "error", err)
return errors.New("failed to write config")
}
return nil
}
// Gets proxy logout details.
// Details are accessible to any user for the logout flow.
// If proxy configured should be checked before using this.
func getTrustedHeaderAuthLogoutDetails() *TrustedHeaderAuthLogoutDetailsResponse {
return &TrustedHeaderAuthLogoutDetailsResponse{
LogoutUrl: Config.HEADER_AUTH.LogoutUrl,
}
}
// Login via header sso
func loginTrustedHeaderAuth(user *User, db *gorm.DB) (AuthResponse, error) {
slog.Debug("loginTrustedHeaderAuth: A user is logging in", "username_from_header", user.Username)
dbUser := new(User)
res := db.Where("username = ? AND type = ?", user.Username, PROXY_USER).Take(&dbUser)
if res.Error != nil {
if errors.Is(res.Error, gorm.ErrRecordNotFound) {
slog.Info("loginTrustedHeaderAuth: Creating new User from authentication header", "username_from_header", user.Username)
// Record not found, so we should create the user (if configured to do so)
// dbUser will be empty, so we can just reuse it for this purpose.
dbUser.Username = user.Username
dbUser.Type = PROXY_USER
dbUser.Country = &Config.DEFAULT_COUNTRY
res = db.Create(&dbUser)
if res.Error != nil {
slog.Error("loginTrustedHeaderAuth: Failed to create new user in db", "error", res.Error)
return AuthResponse{}, errors.New("failed to create new user")
}
} else {
slog.Error("loginTrustedHeaderAuth: An error occurred when looking up user in db", "error", res.Error)
return AuthResponse{}, errors.New("error locating user in db")
}
}
token, err := signJWT(dbUser)
if err != nil {
slog.Error("loginTrustedHeaderAuth: Failed to sign new jwt", "error", err)
return AuthResponse{}, errors.New("failed to get auth token")
}
return AuthResponse{Token: token}, nil
}
+39 -1
View File
@@ -54,6 +54,10 @@ type ServerConfig struct {
// Will be fetched automatically when PLEX_HOST is provided via web ui.
PLEX_MACHINE_ID string `json:",omitempty"`
// Optional: Trusted header authentication configuration.
// VERY DANGEROUS if access is not controlled correctly!
HEADER_AUTH TrustedHeaderAuthSetting `json:",omitempty"`
SONARR []SonarrSettings `json:",omitempty"`
RADARR []RadarrSettings `json:",omitempty"`
TWITCH game.IGDB `json:",omitempty"`
@@ -93,6 +97,35 @@ func (c *ServerConfig) GetSafe() ServerConfig {
}
}
type ServerConfigGetByName struct {
Value any `json:"value"`
}
// Get config item by name.
func (c *ServerConfig) Get(s string) (ServerConfigGetByName, error) {
switch s {
case "DEFAULT_COUNTRY":
return ServerConfigGetByName{Value: c.DEFAULT_COUNTRY}, nil
case "JELLYFIN_HOST":
return ServerConfigGetByName{Value: c.JELLYFIN_HOST}, nil
case "USE_EMBY":
return ServerConfigGetByName{Value: c.USE_EMBY}, nil
case "SIGNUP_ENABLED":
return ServerConfigGetByName{Value: c.SIGNUP_ENABLED}, nil
case "TMDB_KEY":
return ServerConfigGetByName{Value: c.TMDB_KEY}, nil
case "PLEX_HOST":
return ServerConfigGetByName{Value: c.PLEX_HOST}, nil
case "PLEX_MACHINE_ID":
return ServerConfigGetByName{Value: c.PLEX_MACHINE_ID}, nil
case "HEADER_AUTH":
return ServerConfigGetByName{Value: c.HEADER_AUTH}, nil
case "DEBUG":
return ServerConfigGetByName{Value: c.DEBUG}, nil
}
return ServerConfigGetByName{}, errors.New("invalid setting")
}
var (
// Our server config.. `readConfig` will overwrite from watcharr.json cfg file.
Config = ServerConfig{}
@@ -171,7 +204,12 @@ func updateConfig(k string, v any) error {
} else {
return errors.New("invalid setting")
}
return writeConfig()
err := writeConfig()
if err != nil {
slog.Error("updateConfig: Failed to write updated config!", "error", err)
return errors.New("failed to write config")
}
return nil
}
// Write current Config to file
+89 -11
View File
@@ -761,6 +761,28 @@ func (b *BaseRouter) addAuthRoutes() {
c.Status(400)
})
// Proxy Login
auth.POST("/proxy", func(c *gin.Context) {
var user User
if !trustedHeaderAuthIsEnabled() {
slog.Error("ProxyLogin: SSO has not been configured.")
c.JSON(http.StatusForbidden, ErrorResponse{Error: "proxy authentication is disabled"})
return
}
user.Username = c.GetHeader(Config.HEADER_AUTH.HeaderName)
if user.Username == "" {
slog.Error("ProxyLogin: Authentication header is missing.")
c.JSON(http.StatusForbidden, ErrorResponse{Error: "authentication header missing"})
return
}
response, err := loginTrustedHeaderAuth(&user, b.db)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, response)
})
// Register
auth.POST("/register", func(c *gin.Context) {
var user UserRegisterRequest
@@ -778,24 +800,45 @@ func (b *BaseRouter) addAuthRoutes() {
// Get available auth providers
auth.GET("/available", func(c *gin.Context) {
availableAuthProviders := []string{}
if Config.JELLYFIN_HOST != "" {
availableAuthProviders = append(availableAuthProviders, "jellyfin")
}
if Config.PLEX_HOST != "" && Config.PLEX_MACHINE_ID != "" {
availableAuthProviders = append(availableAuthProviders, "plex")
}
c.JSON(http.StatusOK, &AvailableAuthProvidersResponse{
AvailableAuthProviders: availableAuthProviders,
resp := &AvailableAuthProvidersResponse{
AvailableAuthProviders: []string{},
SignupEnabled: Config.SIGNUP_ENABLED,
IsInSetup: ServerInSetup,
UseEmby: Config.USE_EMBY,
})
}
if Config.JELLYFIN_HOST != "" {
resp.AvailableAuthProviders = append(resp.AvailableAuthProviders, "jellyfin")
}
if Config.PLEX_HOST != "" && Config.PLEX_MACHINE_ID != "" {
resp.AvailableAuthProviders = append(resp.AvailableAuthProviders, "plex")
}
if trustedHeaderAuthIsEnabled() {
resp.AvailableAuthProviders = append(resp.AvailableAuthProviders, "header")
resp.HeaderAuthAutoLogin = Config.HEADER_AUTH.AutoLogin
}
c.JSON(http.StatusOK, resp)
})
// IMPORTANT: Routes below here must be authenticated.
auth.Use(AuthRequired(nil))
{
// Request details for logout process for proxy users.
// Any proxy user can request this for logout.
auth.GET("/proxy_logout_details", func(c *gin.Context) {
if !trustedHeaderAuthIsEnabled() {
slog.Error("GetProxy: SSO has not been configured.")
c.JSON(http.StatusForbidden, ErrorResponse{Error: "proxy authentication is disabled"})
return
}
userType := c.MustGet("userType").(UserType)
if userType != PROXY_USER {
slog.Error("GetProxy: Non proxy user attempted to fetch proxy logout details.")
c.JSON(http.StatusForbidden, ErrorResponse{Error: "you are not a proxy user"})
return
}
c.JSON(http.StatusOK, getTrustedHeaderAuthLogoutDetails())
})
// Request admin token
auth.GET("/admin_token", func(c *gin.Context) {
userId := c.MustGet("userId").(uint)
@@ -1116,18 +1159,53 @@ func (b *BaseRouter) addServerRoutes() {
// Get server config (minus very sensitive fields, like JWT_SECRET)
server.GET("/config", func(c *gin.Context) {
// s should be provided when asking for the value of just one setting.
s := c.Query("s")
if s != "" {
val, err := Config.Get(s)
if err != nil {
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.JSON(http.StatusOK, val)
return
}
// Return new ServerConfig with only the fields we want to show in settings ui
c.JSON(http.StatusOK, Config.GetSafe())
})
// Update config
server.POST("/config", func(c *gin.Context) {
// If query param `s` provided, handle specific setting.
// In this case, request body should be new setting value.
s := c.Query("s")
if s != "" {
switch s {
case "HEADER_AUTH":
var ur TrustedHeaderAuthSetting
err := c.ShouldBindJSON(&ur)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
err = setTrustedHeaderAuthSetting(ur)
if err != nil {
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
return
}
c.AbortWithStatusJSON(http.StatusBadRequest, ErrorResponse{Error: "unsupported setting"})
return
}
// No `s` param.. handle normally with `updateConfig` func.
var ur KeyValueRequest
err := c.ShouldBindJSON(&ur)
if err == nil {
err := updateConfig(ur.Key, ur.Value)
if err != nil {
c.JSON(http.StatusForbidden, ErrorResponse{Error: err.Error()})
c.JSON(http.StatusBadRequest, ErrorResponse{Error: err.Error()})
return
}
c.Status(http.StatusOK)
+6
View File
@@ -447,6 +447,12 @@
d="M490.18 181.4l-44.13-44.13a20 20 0 00-27-1 30.81 30.81 0 01-41.68-1.6 30.81 30.81 0 01-1.6-41.67 20 20 0 00-1-27L330.6 21.82a19.91 19.91 0 00-28.13 0l-70.35 70.34a39.87 39.87 0 00-9.57 15.5 7.71 7.71 0 01-4.83 4.83 39.78 39.78 0 00-15.5 9.58l-180.4 180.4a19.91 19.91 0 000 28.13L66 374.73a20 20 0 0027 1 30.69 30.69 0 0143.28 43.28 20 20 0 001 27l44.13 44.13a19.91 19.91 0 0028.13 0l180.4-180.4a39.82 39.82 0 009.58-15.49 7.69 7.69 0 014.84-4.84 39.84 39.84 0 0015.49-9.57l70.34-70.35a19.91 19.91 0 00-.01-28.09zm-228.37-29.65a16 16 0 01-22.63 0l-11.51-11.51a16 16 0 0122.63-22.62l11.51 11.5a16 16 0 010 22.63zm44 44a16 16 0 01-22.62 0l-11-11a16 16 0 1122.63-22.63l11 11a16 16 0 01.01 22.66zm44 44a16 16 0 01-22.63 0l-11-11a16 16 0 0122.63-22.62l11 11a16 16 0 01.05 22.67zm44.43 44.54a16 16 0 01-22.63 0l-11.44-11.5a16 16 0 1122.68-22.57l11.45 11.49a16 16 0 01-.01 22.63z"
/>
</svg>
{:else if i === "lock-closed"}
<svg xmlns="http://www.w3.org/2000/svg" width={wh} height={wh} viewBox="0 0 512 512">
<path
d="M368 192h-16v-80a96 96 0 10-192 0v80h-16a64.07 64.07 0 00-64 64v176a64.07 64.07 0 0064 64h224a64.07 64.07 0 0064-64V256a64.07 64.07 0 00-64-64zm-48 0H192v-80a64 64 0 11128 0z"
/>
</svg>
{/if}
<style lang="scss">
+21
View File
@@ -0,0 +1,21 @@
<script lang="ts">
export let title: string;
export let desc: string;
export let type: "warn";
</script>
<div class={type}>
<h4 class="norm">{title}</h4>
<p>{desc}</p>
</div>
<style lang="scss">
div {
padding: 10px 12px;
border-radius: 10px;
&.warn {
background-color: $warn;
}
}
</style>
@@ -0,0 +1,91 @@
<script lang="ts">
import { onMount } from "svelte";
import Modal from "../Modal.svelte";
import axios from "axios";
import type { TrustedHeaderAuthLogoutDetailsResponse } from "@/types";
import Spinner from "../Spinner.svelte";
import { clearWatcharrData } from ".";
import { goto } from "$app/navigation";
export let onClose: () => void;
let loadingBtns = true;
let logoutUrl: string | undefined = undefined;
onMount(async () => {
try {
const r = await axios.get<TrustedHeaderAuthLogoutDetailsResponse>(
"/auth/proxy_logout_details"
);
logoutUrl = r?.data?.logoutUrl;
if (!logoutUrl?.toLowerCase()?.startsWith("http")) {
// If no protocol in logoutUrl, set https
logoutUrl = `https://${logoutUrl}`;
}
} catch (err) {
console.error("Failed to get proxy logout details!", err);
}
loadingBtns = false;
});
function logout() {
clearWatcharrData();
goto("/login?noAuto=1");
}
function proxyLogout() {
if (!logoutUrl) {
console.error("proxyLogout: Not supported without a configured logoutUrl.");
return;
}
clearWatcharrData();
window.location.replace(logoutUrl);
}
</script>
<Modal
title="Logout"
desc="You logged in via single sign-on. Did you click log out by mistake?"
{onClose}
>
{#if loadingBtns}
<Spinner />
{:else}
<div>
<p>
{#if logoutUrl}
This is likely what you want: <b
>Fully logout of your single sign-on service and Watcharr</b
>.
{:else}
Single sign-on logout has not been configured on this server. If you are not the server
operator, let them know!
{/if}
</p>
<button disabled={!logoutUrl} on:click={proxyLogout}>Log out of Single Sign-On Service</button
>
<p>
Logging out of Watcharr will clear your local credentials and data, but <b
>you will still be logged in to your single sign-on service</b
>! Do <b>not</b> do this on a public machine and assume you are logged out, this account could
still be accessible.
</p>
<button on:click={logout}>Log out of Watcharr</button>
</div>
{/if}
</Modal>
<style lang="scss">
div {
display: flex;
flex-flow: column;
p {
margin-bottom: 5px;
}
button:first-of-type {
margin-bottom: 15px;
}
}
</style>
+14
View File
@@ -0,0 +1,14 @@
import { clearAllStores } from "@/store";
/**
* Helper to clear local data in client for logout
* process. Logic is here so it's not duplciated,
* this should help avoid forgetting to copy any
* new logic to other places, which could break stuff.
* Since this is reusable, technically unrelated logic
* should not be included here (eg: redirecting to /login).
*/
export function clearWatcharrData() {
localStorage.removeItem("token");
clearAllStores();
}
+13 -3
View File
@@ -5,6 +5,8 @@
import PageError from "@/lib/PageError.svelte";
import Spinner from "@/lib/Spinner.svelte";
import tooltip from "@/lib/actions/tooltip";
import { clearWatcharrData } from "@/lib/logout";
import ProxyUserLogoutModal from "@/lib/logout/ProxyUserLogoutModal.svelte";
import DetailedMenu from "@/lib/nav/DetailedMenu.svelte";
import FilterMenu from "@/lib/nav/FilterMenu.svelte";
import FollowingMenu from "@/lib/nav/FollowingMenu.svelte";
@@ -25,7 +27,7 @@
userSettings,
watchedList
} from "@/store";
import { UserPermission } from "@/types";
import { UserPermission, UserType } from "@/types";
import axios from "axios";
import { onMount } from "svelte";
@@ -38,6 +40,7 @@
let followingMenuShown = false;
let detailedMenuShown = false;
let tagMenuShown = false;
let proxyUserLogoutShown = false;
$: settings = $userSettings;
$: user = $userInfo;
@@ -91,8 +94,12 @@
}
function logout() {
localStorage.removeItem("token");
clearAllStores();
if (user?.type === UserType.Proxy) {
// Proxy users logout flow is different.
proxyUserLogoutShown = true;
return;
}
clearWatcharrData();
goto("/login");
}
@@ -365,6 +372,9 @@
{/if}
{/if}
<button class="plain" on:click={() => logout()}>Logout</button>
{#if proxyUserLogoutShown}
<ProxyUserLogoutModal onClose={() => (proxyUserLogoutShown = false)} />
{/if}
<!-- svelte-ignore missing-declaration -->
<span>v{__WATCHARR_VERSION__}</span>
</div>
+2 -1
View File
@@ -6,6 +6,7 @@ import { goto } from "$app/navigation";
import axios from "axios";
import { baseURL } from "@/lib/util/api";
import { notify } from "@/lib/util/notify";
import { clearWatcharrData } from "@/lib/logout";
axios.interceptors.request.use(
(config) => {
@@ -38,7 +39,7 @@ axios.interceptors.response.use(
if (error.response?.status === 401) {
console.error("Recieved 401 response, going to login.");
notify({ text: "Request Authorization Failed!", type: "error" });
localStorage.removeItem("token");
clearWatcharrData();
goto("/login?again=1");
}
return Promise.reject(error);
+18 -2
View File
@@ -24,6 +24,7 @@
import TwitchModal from "./modals/TwitchModal.svelte";
import RegionDropDown from "@/lib/RegionDropDown.svelte";
import TaskScheduleModal from "./modals/TaskScheduleModal.svelte";
import TrustedHeaderAuthModal from "./modals/TrustedHeaderAuthModal.svelte";
let serverConfig: ServerConfig;
let sonarrModalOpen = false;
@@ -34,6 +35,7 @@
let radarrModalEditing = false;
let twitchModalOpen = false;
let taskScheduleModalOpen = false;
let headerSSOModalOpen = false;
// Disabled vars for disabling inputs until api request completes
let signupDisabled = false;
let debugDisabled = false;
@@ -226,7 +228,7 @@
disabled={tmdbkDisabled}
/>
</Setting>
<Setting title="Signup" desc="Allow signing up with web ui" row>
<Setting title="Signup" desc="Allow signing up with Watcharr credentials." row>
<Checkbox
name="SIGNUP_ENABLED"
disabled={signupDisabled}
@@ -239,7 +241,7 @@
}}
/>
</Setting>
<Setting title="Debug" desc="Enable debug logging" row>
<Setting title="Debug" desc="Enable debug logging." row>
<Checkbox
name="DEBUG"
disabled={debugDisabled}
@@ -265,6 +267,20 @@
{#if taskScheduleModalOpen}
<TaskScheduleModal onClose={() => (taskScheduleModalOpen = false)}></TaskScheduleModal>
{/if}
<Setting>
<SettingButton
title="Trusted Header Authentication"
desc="Configure trusted header single sign-on."
icon={"arrow"}
onClick={() => {
headerSSOModalOpen = true;
}}
/>
</Setting>
{#if headerSSOModalOpen}
<TrustedHeaderAuthModal onClose={() => (headerSSOModalOpen = false)}
></TrustedHeaderAuthModal>
{/if}
<div>
<h3>Services</h3>
<h5 class="norm">
@@ -0,0 +1,155 @@
<script lang="ts">
import Checkbox from "@/lib/Checkbox.svelte";
import Modal from "@/lib/Modal.svelte";
import Notice from "@/lib/Notice.svelte";
import Spinner from "@/lib/Spinner.svelte";
import Setting from "@/lib/settings/Setting.svelte";
import SettingsList from "@/lib/settings/SettingsList.svelte";
import { notify } from "@/lib/util/notify";
import type { ServerConfigByName, TrustedHeaderAuthSetting } from "@/types";
import axios from "axios";
import { onMount } from "svelte";
export let onClose: () => void;
let headerCfg: TrustedHeaderAuthSetting = {
enabled: false,
headerName: ""
};
let formDisabled = false;
let loadingCfg = false;
let error = "";
async function getHeaderCfg() {
try {
loadingCfg = true;
const res = await axios.get<ServerConfigByName<TrustedHeaderAuthSetting>>("/server/config", {
params: { s: "HEADER_AUTH" }
});
headerCfg = res.data.value;
loadingCfg = false;
} catch (err) {
console.error("getHeaderCfg failed!", err);
notify({ type: "error", text: "Failed to get configuration from server.", time: 6000 });
}
}
async function save() {
const nid = notify({
type: "loading",
text: "Saving.."
});
try {
await axios.post("/server/config", headerCfg, { params: { s: "HEADER_AUTH" } });
notify({
id: nid,
type: "success",
text: "Changes saved!"
});
onClose();
} catch (err) {
console.error("save failed!", err);
notify({
id: nid,
type: "error",
text: "Failed to save config. Please try again!",
time: 6000
});
}
}
onMount(() => {
getHeaderCfg();
});
</script>
<Modal
title="Trusted Header Authentication"
desc="Configure trusted header single sign-on."
{onClose}
>
{#if error}
<span class="error">{error}!</span>
{/if}
{#if loadingCfg}
<Spinner />
{:else}
<SettingsList>
<Notice
title="This is dangerous!"
desc="If setup incorrectly, the authorization module for your server could be easily comprimised. If configured, please ensure your Watcharr instance is only available through your proxy and not available directly."
type="warn"
/>
<Setting title="Header Name" desc="Name of the header used for authentication.">
<input
type="text"
placeholder="X-User"
on:blur={() => {}}
disabled={formDisabled}
bind:value={headerCfg.headerName}
/>
</Setting>
<Setting title="Logout URL" desc="Where can we redirect so that the user can logout?">
<input
type="text"
placeholder="https://auth.example.com/logout"
on:blur={() => {}}
disabled={formDisabled}
bind:value={headerCfg.logoutUrl}
/>
</Setting>
<Setting
title="Auto Login"
desc="Should we auto login users as soon as they reach our login page?"
row
>
<Checkbox
name="HeaderAuthAutoLogin"
disabled={formDisabled}
bind:value={headerCfg.autoLogin}
/>
</Setting>
<Setting
title="Enabled"
desc="Enable Trusted Header Single Sign-On? Toggle on to activate above configuration."
row
>
<Checkbox name="HeaderAuthEnabled" disabled={formDisabled} bind:value={headerCfg.enabled} />
</Setting>
<div class="btns">
<button on:click={() => save()}>Save</button>
</div>
</SettingsList>
{/if}
</Modal>
<style lang="scss">
.btns {
display: flex;
flex-flow: row;
gap: 10px;
button {
width: max-content;
padding-left: 15px;
padding-right: 15px;
&:nth-child(1) {
margin-left: auto;
}
}
}
.error {
position: sticky;
top: 0;
display: flex;
justify-content: center;
width: 100%;
padding: 10px;
background-color: rgb(221, 48, 48);
text-transform: capitalize;
color: white;
margin-bottom: 15px;
}
</style>
+73 -13
View File
@@ -10,8 +10,11 @@
let error: string;
let login = true;
let availableProviders: string[] = [];
let apHeader = false;
let apPlex = false;
let signupEnabled = true;
let useEmby = false;
let noAuto = false;
onMount(() => {
if (localStorage.getItem("token")) {
@@ -25,8 +28,14 @@
goto("/setup");
}
availableProviders = r.data.available;
apHeader = availableProviders?.includes("header");
apPlex = availableProviders?.includes("plex");
signupEnabled = r.data.signupEnabled;
useEmby = r.data.useEmby;
if (r.data.headerAuthAutoLogin && !noAuto) {
console.log("handling headerAuthAutoLogin.. calling proxyLogin automatically now.");
proxyLogin(true);
}
}
});
});
@@ -35,6 +44,10 @@
if (!error && $page.url.searchParams.get("again")) {
error = "Please Login Again";
}
if ($page.url.searchParams.get("noAuto") == "1") {
console.info("login: Found noAuto param.. auto logins should be disabled now.");
noAuto = true;
}
});
function handleLogin(ev: SubmitEvent) {
@@ -121,6 +134,32 @@
error = "Plex login failed";
}
}
function proxyLogin(auto = false) {
const nid = notify({ text: "Logging in", type: "loading" });
noAuthAxios
.post(`/auth/proxy`)
.then((resp) => {
if (resp.data?.token) {
console.log("Received token... logging in.");
localStorage.setItem("token", resp.data.token);
goto("/");
notify({ id: nid, text: `Welcome!`, type: "success" });
}
})
.catch((err) => {
if (err.response) {
error = err.response.data.error;
} else {
error = err.message;
}
if (auto) {
notify({ id: nid, text: `Automatic SSO Login Failed!`, type: "error" });
} else {
unNotify(nid);
}
});
}
</script>
<div>
@@ -164,20 +203,36 @@
{/if}
{/if}
</div>
{#if availableProviders?.findIndex((provider) => provider == "plex") > -1}
{#if apHeader || apPlex}
<p style="font-weight: bold; font-size: 14px;">or</p>
<div class="login-btns">
<button
type="button"
on:click={() => {
plexLogin();
}}
name="plex"
class="plex other"
>
<Icon i="plex" wh={18} />Continue with Plex
</button>
</div>
{#if apHeader}
<div class="login-btns">
<button
type="button"
name="proxy"
class="proxy other"
on:click={() => {
proxyLogin();
}}
>
<Icon i="lock-closed" wh={18} />Continue with Single Sign-On
</button>
</div>
{/if}
{#if apPlex}
<div class="login-btns">
<button
type="button"
on:click={() => {
plexLogin();
}}
name="plex"
class="plex other"
>
<Icon i="plex" wh={18} />Continue with Plex
</button>
</div>
{/if}
{/if}
{:else}
<div class="login-btns">
@@ -238,6 +293,11 @@
gap: 10px;
width: 100%;
/* Hardcoded point for when main watcharr/jellyfin btns break. */
@media screen and (max-width: 320px) {
flex-wrap: wrap;
}
button {
display: flex;
flex-flow: row;
+20 -2
View File
@@ -51,7 +51,8 @@ export type Icon =
| "unpin"
| "sparkles"
| "tag"
| "ticket";
| "ticket"
| "lock-closed";
export type Theme = "light" | "dark";
@@ -68,7 +69,8 @@ export type PosterExtraDetails = {
export enum UserType {
// Assume watcharr user if none of these...
Jellyfin = 1,
Plex = 2
Plex = 2,
Proxy = 3
}
interface dbModel {
@@ -258,6 +260,7 @@ export interface AvailableAuthProviders {
signupEnabled: boolean;
isInSetup: boolean;
useEmby: boolean;
headerAuthAutoLogin: boolean;
}
export interface TokenClaims {
@@ -933,6 +936,10 @@ export interface ServerConfig {
DEBUG: boolean;
}
export interface ServerConfigByName<T> {
value: T;
}
export interface SonarrSettings {
name: string;
host?: string;
@@ -957,6 +964,17 @@ export interface TwitchSettings {
clientSecret: string;
}
export interface TrustedHeaderAuthSetting {
enabled: boolean;
headerName: string;
autoLogin?: boolean;
logoutUrl?: string;
}
export interface TrustedHeaderAuthLogoutDetailsResponse {
logoutUrl?: string;
}
export interface DropDownItem {
id: number | string;
value: string;
+1
View File
@@ -40,6 +40,7 @@ $poster-rating-color: var(--poster-rating-color);
$poster-extra-detail-bg-color: rgba(46, 46, 46, 0.5);
// Bg col of elements with image behind with mix-blend-mode: multiply where we want a little of image to come through.
$img-blend-multiply-bg-col: var(--img-blend-multiply-bg-col);
$warn: #f38755;
$error: #f3555a;
$success: #28a745;
$success-hover: #1e7e34;