sources/plex: initial plex source implementation
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
parent
19708bc67b
commit
f1b100c8a5
|
@ -63,6 +63,7 @@ from authentik.sources.oauth.api.source import OAuthSourceViewSet
|
|||
from authentik.sources.oauth.api.source_connection import (
|
||||
UserOAuthSourceConnectionViewSet,
|
||||
)
|
||||
from authentik.sources.plex.api import PlexSourceViewSet
|
||||
from authentik.sources.saml.api import SAMLSourceViewSet
|
||||
from authentik.stages.authenticator_static.api import (
|
||||
AuthenticatorStaticStageViewSet,
|
||||
|
@ -136,6 +137,7 @@ router.register("sources/oauth_user_connections", UserOAuthSourceConnectionViewS
|
|||
router.register("sources/ldap", LDAPSourceViewSet)
|
||||
router.register("sources/saml", SAMLSourceViewSet)
|
||||
router.register("sources/oauth", OAuthSourceViewSet)
|
||||
router.register("sources/plex", PlexSourceViewSet)
|
||||
|
||||
router.register("policies/all", PolicyViewSet)
|
||||
router.register("policies/bindings", PolicyBindingViewSet)
|
||||
|
|
|
@ -107,6 +107,7 @@ INSTALLED_APPS = [
|
|||
"authentik.recovery",
|
||||
"authentik.sources.ldap",
|
||||
"authentik.sources.oauth",
|
||||
"authentik.sources.plex",
|
||||
"authentik.sources.saml",
|
||||
"authentik.stages.authenticator_static",
|
||||
"authentik.stages.authenticator_totp",
|
||||
|
|
|
@ -2,11 +2,21 @@
|
|||
from importlib import import_module
|
||||
|
||||
from django.apps import AppConfig
|
||||
from django.conf import settings
|
||||
from structlog.stdlib import get_logger
|
||||
|
||||
LOGGER = get_logger()
|
||||
|
||||
AUTHENTIK_SOURCES_OAUTH_TYPES = [
|
||||
"authentik.sources.oauth.types.discord",
|
||||
"authentik.sources.oauth.types.facebook",
|
||||
"authentik.sources.oauth.types.github",
|
||||
"authentik.sources.oauth.types.google",
|
||||
"authentik.sources.oauth.types.reddit",
|
||||
"authentik.sources.oauth.types.twitter",
|
||||
"authentik.sources.oauth.types.azure_ad",
|
||||
"authentik.sources.oauth.types.oidc",
|
||||
]
|
||||
|
||||
|
||||
class AuthentikSourceOAuthConfig(AppConfig):
|
||||
"""authentik source.oauth config"""
|
||||
|
@ -18,7 +28,7 @@ class AuthentikSourceOAuthConfig(AppConfig):
|
|||
|
||||
def ready(self):
|
||||
"""Load source_types from config file"""
|
||||
for source_type in settings.AUTHENTIK_SOURCES_OAUTH_TYPES:
|
||||
for source_type in AUTHENTIK_SOURCES_OAUTH_TYPES:
|
||||
try:
|
||||
import_module(source_type)
|
||||
LOGGER.debug("Loaded OAuth Source Type", type=source_type)
|
||||
|
|
|
@ -163,16 +163,6 @@ class OpenIDOAuthSource(OAuthSource):
|
|||
verbose_name_plural = _("OpenID OAuth Sources")
|
||||
|
||||
|
||||
class PlexOAuthSource(OAuthSource):
|
||||
"""Login using plex.tv."""
|
||||
|
||||
class Meta:
|
||||
|
||||
abstract = True
|
||||
verbose_name = _("Plex OAuth Source")
|
||||
verbose_name_plural = _("Plex OAuth Sources")
|
||||
|
||||
|
||||
class UserOAuthSourceConnection(UserSourceConnection):
|
||||
"""Authorized remote OAuth provider."""
|
||||
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
"""Oauth2 Client Settings"""
|
||||
|
||||
AUTHENTIK_SOURCES_OAUTH_TYPES = [
|
||||
"authentik.sources.oauth.types.discord",
|
||||
"authentik.sources.oauth.types.facebook",
|
||||
"authentik.sources.oauth.types.github",
|
||||
"authentik.sources.oauth.types.google",
|
||||
"authentik.sources.oauth.types.reddit",
|
||||
"authentik.sources.oauth.types.twitter",
|
||||
"authentik.sources.oauth.types.azure_ad",
|
||||
"authentik.sources.oauth.types.oidc",
|
||||
"authentik.sources.oauth.types.plex",
|
||||
]
|
|
@ -0,0 +1,21 @@
|
|||
"""Plex Source Serializer"""
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from authentik.core.api.sources import SourceSerializer
|
||||
from authentik.sources.plex.models import PlexSource
|
||||
|
||||
|
||||
class PlexSourceSerializer(SourceSerializer):
|
||||
"""Plex Source Serializer"""
|
||||
|
||||
class Meta:
|
||||
model = PlexSource
|
||||
fields = SourceSerializer.Meta.fields + ["client_id", "allowed_servers"]
|
||||
|
||||
|
||||
class PlexSourceViewSet(ModelViewSet):
|
||||
"""Plex source Viewset"""
|
||||
|
||||
queryset = PlexSource.objects.all()
|
||||
serializer_class = PlexSourceSerializer
|
||||
lookup_field = "slug"
|
|
@ -0,0 +1,10 @@
|
|||
"""authentik plex config"""
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class AuthentikSourcePlexConfig(AppConfig):
|
||||
"""authentik source plex config"""
|
||||
|
||||
name = "authentik.sources.plex"
|
||||
label = "authentik_sources_plex"
|
||||
verbose_name = "authentik Sources.Plex"
|
|
@ -0,0 +1,45 @@
|
|||
# Generated by Django 3.2 on 2021-05-02 12:34
|
||||
|
||||
import django.contrib.postgres.fields
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
("authentik_core", "0019_source_managed"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="PlexSource",
|
||||
fields=[
|
||||
(
|
||||
"source_ptr",
|
||||
models.OneToOneField(
|
||||
auto_created=True,
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
parent_link=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
to="authentik_core.source",
|
||||
),
|
||||
),
|
||||
("client_id", models.TextField()),
|
||||
(
|
||||
"allowed_servers",
|
||||
django.contrib.postgres.fields.ArrayField(
|
||||
base_field=models.TextField(), size=None
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"verbose_name": "Plex Source",
|
||||
"verbose_name_plural": "Plex Sources",
|
||||
},
|
||||
bases=("authentik_core.source",),
|
||||
),
|
||||
]
|
|
@ -0,0 +1,42 @@
|
|||
"""Plex source"""
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
from django.db import models
|
||||
from django.templatetags.static import static
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from rest_framework.serializers import BaseSerializer
|
||||
|
||||
from authentik.core.models import Source
|
||||
from authentik.core.types import UILoginButton
|
||||
|
||||
|
||||
class PlexSource(Source):
|
||||
"""Authenticate against plex.tv"""
|
||||
|
||||
client_id = models.TextField()
|
||||
allowed_servers = ArrayField(models.TextField())
|
||||
|
||||
@property
|
||||
def component(self) -> str:
|
||||
return "ak-source-plex-form"
|
||||
|
||||
@property
|
||||
def serializer(self) -> BaseSerializer:
|
||||
from authentik.sources.plex.api import PlexSourceSerializer
|
||||
|
||||
return PlexSourceSerializer
|
||||
|
||||
@property
|
||||
def ui_login_button(self) -> UILoginButton:
|
||||
return UILoginButton(
|
||||
url="",
|
||||
icon_url=static("authentik/sources/plex.svg"),
|
||||
name=self.name,
|
||||
additional_data={
|
||||
"client_id": self.client_id,
|
||||
},
|
||||
)
|
||||
|
||||
class Meta:
|
||||
|
||||
verbose_name = _("Plex Source")
|
||||
verbose_name_plural = _("Plex Sources")
|
|
@ -85,6 +85,7 @@ class PlexOAuthClient(OAuth2Client):
|
|||
def get_profile_info(self, token: dict[str, str]) -> Optional[dict[str, Any]]:
|
||||
"Fetch user profile information."
|
||||
qs = {"X-Plex-Token": token["plex_token"]}
|
||||
print(token)
|
||||
try:
|
||||
response = self.do_request(
|
||||
"get", f"https://plex.tv/users/account.json?{urlencode(qs)}"
|
||||
|
@ -94,7 +95,8 @@ class PlexOAuthClient(OAuth2Client):
|
|||
LOGGER.warning("Unable to fetch user profile", exc=exc)
|
||||
return None
|
||||
else:
|
||||
return response.json().get("user", {})
|
||||
info = response.json()
|
||||
return info.get("user", {})
|
||||
|
||||
|
||||
class PlexOAuth2Callback(OAuthCallback):
|
269
swagger.yaml
269
swagger.yaml
|
@ -10213,6 +10213,205 @@ paths:
|
|||
description: A unique integer value identifying this User OAuth Source Connection.
|
||||
required: true
|
||||
type: integer
|
||||
/sources/plex/:
|
||||
get:
|
||||
operationId: sources_plex_list
|
||||
description: Plex source Viewset
|
||||
parameters:
|
||||
- name: ordering
|
||||
in: query
|
||||
description: Which field to use when ordering the results.
|
||||
required: false
|
||||
type: string
|
||||
- name: search
|
||||
in: query
|
||||
description: A search term.
|
||||
required: false
|
||||
type: string
|
||||
- name: page
|
||||
in: query
|
||||
description: Page Index
|
||||
required: false
|
||||
type: integer
|
||||
- name: page_size
|
||||
in: query
|
||||
description: Page Size
|
||||
required: false
|
||||
type: integer
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
required:
|
||||
- results
|
||||
- pagination
|
||||
type: object
|
||||
properties:
|
||||
pagination:
|
||||
required:
|
||||
- next
|
||||
- previous
|
||||
- count
|
||||
- current
|
||||
- total_pages
|
||||
- start_index
|
||||
- end_index
|
||||
type: object
|
||||
properties:
|
||||
next:
|
||||
type: number
|
||||
previous:
|
||||
type: number
|
||||
count:
|
||||
type: number
|
||||
current:
|
||||
type: number
|
||||
total_pages:
|
||||
type: number
|
||||
start_index:
|
||||
type: number
|
||||
end_index:
|
||||
type: number
|
||||
results:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
tags:
|
||||
- sources
|
||||
post:
|
||||
operationId: sources_plex_create
|
||||
description: Plex source Viewset
|
||||
parameters:
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
responses:
|
||||
'201':
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
'400':
|
||||
description: Invalid input.
|
||||
schema:
|
||||
$ref: '#/definitions/ValidationError'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
tags:
|
||||
- sources
|
||||
parameters: []
|
||||
/sources/plex/{slug}/:
|
||||
get:
|
||||
operationId: sources_plex_read
|
||||
description: Plex source Viewset
|
||||
parameters: []
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
'404':
|
||||
description: Object does not exist or caller has insufficient permissions
|
||||
to access it.
|
||||
schema:
|
||||
$ref: '#/definitions/APIException'
|
||||
tags:
|
||||
- sources
|
||||
put:
|
||||
operationId: sources_plex_update
|
||||
description: Plex source Viewset
|
||||
parameters:
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
'400':
|
||||
description: Invalid input.
|
||||
schema:
|
||||
$ref: '#/definitions/ValidationError'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
'404':
|
||||
description: Object does not exist or caller has insufficient permissions
|
||||
to access it.
|
||||
schema:
|
||||
$ref: '#/definitions/APIException'
|
||||
tags:
|
||||
- sources
|
||||
patch:
|
||||
operationId: sources_plex_partial_update
|
||||
description: Plex source Viewset
|
||||
parameters:
|
||||
- name: data
|
||||
in: body
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
responses:
|
||||
'200':
|
||||
description: ''
|
||||
schema:
|
||||
$ref: '#/definitions/PlexSource'
|
||||
'400':
|
||||
description: Invalid input.
|
||||
schema:
|
||||
$ref: '#/definitions/ValidationError'
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
'404':
|
||||
description: Object does not exist or caller has insufficient permissions
|
||||
to access it.
|
||||
schema:
|
||||
$ref: '#/definitions/APIException'
|
||||
tags:
|
||||
- sources
|
||||
delete:
|
||||
operationId: sources_plex_delete
|
||||
description: Plex source Viewset
|
||||
parameters: []
|
||||
responses:
|
||||
'204':
|
||||
description: ''
|
||||
'403':
|
||||
description: Authentication credentials were invalid, absent or insufficient.
|
||||
schema:
|
||||
$ref: '#/definitions/GenericError'
|
||||
'404':
|
||||
description: Object does not exist or caller has insufficient permissions
|
||||
to access it.
|
||||
schema:
|
||||
$ref: '#/definitions/APIException'
|
||||
tags:
|
||||
- sources
|
||||
parameters:
|
||||
- name: slug
|
||||
in: path
|
||||
description: Internal source name, used in URLs.
|
||||
required: true
|
||||
type: string
|
||||
format: slug
|
||||
pattern: ^[-a-zA-Z0-9_]+$
|
||||
/sources/saml/:
|
||||
get:
|
||||
operationId: sources_saml_list
|
||||
|
@ -16210,6 +16409,7 @@ definitions:
|
|||
- authentik.recovery
|
||||
- authentik.sources.ldap
|
||||
- authentik.sources.oauth
|
||||
- authentik.sources.plex
|
||||
- authentik.sources.saml
|
||||
- authentik.stages.authenticator_static
|
||||
- authentik.stages.authenticator_totp
|
||||
|
@ -17386,6 +17586,75 @@ definitions:
|
|||
type: string
|
||||
maxLength: 255
|
||||
minLength: 1
|
||||
PlexSource:
|
||||
required:
|
||||
- name
|
||||
- slug
|
||||
- client_id
|
||||
- allowed_servers
|
||||
type: object
|
||||
properties:
|
||||
pk:
|
||||
title: Pbm uuid
|
||||
type: string
|
||||
format: uuid
|
||||
readOnly: true
|
||||
name:
|
||||
title: Name
|
||||
description: Source's display Name.
|
||||
type: string
|
||||
minLength: 1
|
||||
slug:
|
||||
title: Slug
|
||||
description: Internal source name, used in URLs.
|
||||
type: string
|
||||
format: slug
|
||||
pattern: ^[-a-zA-Z0-9_]+$
|
||||
maxLength: 50
|
||||
minLength: 1
|
||||
enabled:
|
||||
title: Enabled
|
||||
type: boolean
|
||||
authentication_flow:
|
||||
title: Authentication flow
|
||||
description: Flow to use when authenticating existing users.
|
||||
type: string
|
||||
format: uuid
|
||||
x-nullable: true
|
||||
enrollment_flow:
|
||||
title: Enrollment flow
|
||||
description: Flow to use when enrolling new users.
|
||||
type: string
|
||||
format: uuid
|
||||
x-nullable: true
|
||||
component:
|
||||
title: Component
|
||||
type: string
|
||||
readOnly: true
|
||||
verbose_name:
|
||||
title: Verbose name
|
||||
type: string
|
||||
readOnly: true
|
||||
verbose_name_plural:
|
||||
title: Verbose name plural
|
||||
type: string
|
||||
readOnly: true
|
||||
policy_engine_mode:
|
||||
title: Policy engine mode
|
||||
type: string
|
||||
enum:
|
||||
- all
|
||||
- any
|
||||
client_id:
|
||||
title: Client id
|
||||
type: string
|
||||
minLength: 1
|
||||
allowed_servers:
|
||||
type: array
|
||||
items:
|
||||
title: Allowed servers
|
||||
type: string
|
||||
minLength: 1
|
||||
SAMLSource:
|
||||
required:
|
||||
- name
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import { VERSION } from "../../../constants";
|
||||
|
||||
export interface PlexPinResponse {
|
||||
// Only has the fields we care about
|
||||
authToken?: string;
|
||||
code: string;
|
||||
id: number;
|
||||
}
|
||||
|
||||
export interface PlexResource {
|
||||
name: string;
|
||||
provides: string;
|
||||
clientIdentifier: string;
|
||||
}
|
||||
|
||||
export const DEFAULT_HEADERS = {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"X-Plex-Product": "authentik",
|
||||
"X-Plex-Version": VERSION,
|
||||
"X-Plex-Device-Vendor": "BeryJu.org",
|
||||
};
|
||||
|
||||
export class PlexAPIClient {
|
||||
|
||||
token: string;
|
||||
|
||||
constructor(token: string) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
static async getPin(clientIdentifier: string): Promise<{ authUrl: string, pin: PlexPinResponse }> {
|
||||
const headers = { ...DEFAULT_HEADERS, ...{
|
||||
"X-Plex-Client-Identifier": clientIdentifier
|
||||
}};
|
||||
const pinResponse = await fetch("https://plex.tv/api/v2/pins.json?strong=true", {
|
||||
method: "POST",
|
||||
headers: headers
|
||||
});
|
||||
const pin: PlexPinResponse = await pinResponse.json();
|
||||
return {
|
||||
authUrl: `https://app.plex.tv/auth#!?clientID=${encodeURIComponent(clientIdentifier)}&code=${pin.code}`,
|
||||
pin: pin
|
||||
};
|
||||
}
|
||||
|
||||
static async pinStatus(id: number): Promise<string> {
|
||||
const pinResponse = await fetch(`https://plex.tv/api/v2/pins/${id}`, {
|
||||
headers: DEFAULT_HEADERS
|
||||
});
|
||||
const pin: PlexPinResponse = await pinResponse.json();
|
||||
return pin.authToken || "";
|
||||
}
|
||||
|
||||
async getServers(): Promise<PlexResource[]> {
|
||||
const resourcesResponse = await fetch(`https://plex.tv/api/v2/resources?X-Plex-Token=${this.token}&X-Plex-Client-Identifier=authentik`, {
|
||||
headers: DEFAULT_HEADERS
|
||||
});
|
||||
const resources: PlexResource[] = await resourcesResponse.json();
|
||||
return resources.filter(r => {
|
||||
return r.provides === "server";
|
||||
});
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
import {customElement, LitElement} from "lit-element";
|
||||
import {html, TemplateResult} from "lit-html";
|
||||
|
||||
@customElement("ak-flow-sources-plex")
|
||||
export class PlexLoginInit extends LitElement {
|
||||
|
||||
render(): TemplateResult {
|
||||
return html``;
|
||||
}
|
||||
|
||||
}
|
|
@ -17,6 +17,7 @@ import { ifDefined } from "lit-html/directives/if-defined";
|
|||
import "./ldap/LDAPSourceForm";
|
||||
import "./saml/SAMLSourceForm";
|
||||
import "./oauth/OAuthSourceForm";
|
||||
import "./plex/PlexSourceForm";
|
||||
|
||||
@customElement("ak-source-list")
|
||||
export class SourceListPage extends TablePage<Source> {
|
||||
|
|
|
@ -0,0 +1,193 @@
|
|||
import { PlexSource, SourcesApi, FlowsApi, FlowDesignationEnum } from "authentik-api";
|
||||
import { t } from "@lingui/macro";
|
||||
import { customElement, property } from "lit-element";
|
||||
import { html, TemplateResult } from "lit-html";
|
||||
import { DEFAULT_CONFIG } from "../../../api/Config";
|
||||
import { Form } from "../../../elements/forms/Form";
|
||||
import "../../../elements/forms/FormGroup";
|
||||
import "../../../elements/forms/HorizontalFormElement";
|
||||
import { ifDefined } from "lit-html/directives/if-defined";
|
||||
import { until } from "lit-html/directives/until";
|
||||
import { first, randomString } from "../../../utils";
|
||||
import { PlexAPIClient, PlexResource} from "../../../flows/sources/plex/API";
|
||||
|
||||
|
||||
function popupCenterScreen(url: string, title: string, w: number, h: number): Window | null {
|
||||
const top = (screen.height - h) / 4, left = (screen.width - w) / 2;
|
||||
const popup = window.open(url, title, `scrollbars=yes,width=${w},height=${h},top=${top},left=${left}`);
|
||||
return popup;
|
||||
}
|
||||
|
||||
@customElement("ak-source-plex-form")
|
||||
export class PlexSourceForm extends Form<PlexSource> {
|
||||
|
||||
set sourceSlug(value: string) {
|
||||
new SourcesApi(DEFAULT_CONFIG).sourcesPlexRead({
|
||||
slug: value,
|
||||
}).then(source => {
|
||||
this.source = source;
|
||||
});
|
||||
}
|
||||
|
||||
@property({attribute: false})
|
||||
source: PlexSource = {
|
||||
clientId: randomString(40)
|
||||
} as PlexSource;
|
||||
|
||||
@property()
|
||||
plexToken?: string;
|
||||
|
||||
@property({attribute: false})
|
||||
plexResources?: PlexResource[];
|
||||
|
||||
getSuccessMessage(): string {
|
||||
if (this.source) {
|
||||
return t`Successfully updated source.`;
|
||||
} else {
|
||||
return t`Successfully created source.`;
|
||||
}
|
||||
}
|
||||
|
||||
send = (data: PlexSource): Promise<PlexSource> => {
|
||||
if (this.source.slug) {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesPlexUpdate({
|
||||
slug: this.source.slug,
|
||||
data: data
|
||||
});
|
||||
} else {
|
||||
return new SourcesApi(DEFAULT_CONFIG).sourcesPlexCreate({
|
||||
data: data
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
async doAuth(): Promise<void> {
|
||||
const authInfo = await PlexAPIClient.getPin(this.source?.clientId);
|
||||
const authWindow = popupCenterScreen(authInfo.authUrl, "plex auth", 550, 700);
|
||||
const timer = setInterval(() => {
|
||||
if (authWindow?.closed) {
|
||||
clearInterval(timer);
|
||||
PlexAPIClient.pinStatus(authInfo.pin.id).then((token: string) => {
|
||||
this.plexToken = token;
|
||||
this.loadServers();
|
||||
});
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
|
||||
async loadServers(): Promise<void> {
|
||||
if (!this.plexToken) {
|
||||
return;
|
||||
}
|
||||
this.plexResources = await new PlexAPIClient(this.plexToken).getServers();
|
||||
}
|
||||
|
||||
renderForm(): TemplateResult {
|
||||
return html`<form class="pf-c-form pf-m-horizontal">
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Name`}
|
||||
?required=${true}
|
||||
name="name">
|
||||
<input type="text" value="${ifDefined(this.source?.name)}" class="pf-c-form-control" required>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Slug`}
|
||||
?required=${true}
|
||||
name="slug">
|
||||
<input type="text" value="${ifDefined(this.source?.slug)}" class="pf-c-form-control" required>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal name="enabled">
|
||||
<div class="pf-c-check">
|
||||
<input type="checkbox" class="pf-c-check__input" ?checked=${first(this.source?.enabled, true)}>
|
||||
<label class="pf-c-check__label">
|
||||
${t`Enabled`}
|
||||
</label>
|
||||
</div>
|
||||
</ak-form-element-horizontal>
|
||||
|
||||
<ak-form-group .expanded=${true}>
|
||||
<span slot="header">
|
||||
${t`Protocol settings`}
|
||||
</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Client ID`}
|
||||
?required=${true}
|
||||
name="clientId">
|
||||
<input type="text" value="${first(this.source?.clientId)}" class="pf-c-form-control" required>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Allowed servers`}
|
||||
?required=${true}
|
||||
name="allowedServers">
|
||||
<select class="pf-c-form-control" multiple>
|
||||
${this.plexResources?.map(r => {
|
||||
const selected = Array.from(this.source?.allowedServers || []).some(server => {
|
||||
return server == r.clientIdentifier;
|
||||
});
|
||||
return html`<option value=${r.clientIdentifier} ?selected=${selected}>${r.name}</option>`;
|
||||
})}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">${t`Select which server a user has to be a member of to be allowed to authenticate.`}</p>
|
||||
<p class="pf-c-form__helper-text">${t`Hold control/command to select multiple items.`}</p>
|
||||
<p class="pf-c-form__helper-text">
|
||||
<button class="pf-c-button pf-m-primary" type="button" @click=${() => {
|
||||
this.doAuth();
|
||||
}}>
|
||||
${t`Load servers`}
|
||||
</button>
|
||||
</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
<ak-form-group>
|
||||
<span slot="header">
|
||||
${t`Flow settings`}
|
||||
</span>
|
||||
<div slot="body" class="pf-c-form">
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Authentication flow`}
|
||||
?required=${true}
|
||||
name="authenticationFlow">
|
||||
<select class="pf-c-form-control">
|
||||
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
|
||||
ordering: "pk",
|
||||
designation: FlowDesignationEnum.Authentication,
|
||||
}).then(flows => {
|
||||
return flows.results.map(flow => {
|
||||
let selected = this.source?.authenticationFlow === flow.pk;
|
||||
if (!this.source?.pk && !this.source?.authenticationFlow && flow.slug === "default-source-authentication") {
|
||||
selected = true;
|
||||
}
|
||||
return html`<option value=${ifDefined(flow.pk)} ?selected=${selected}>${flow.name} (${flow.slug})</option>`;
|
||||
});
|
||||
}), html`<option>${t`Loading...`}</option>`)}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">${t`Flow to use when authenticating existing users.`}</p>
|
||||
</ak-form-element-horizontal>
|
||||
<ak-form-element-horizontal
|
||||
label=${t`Enrollment flow`}
|
||||
?required=${true}
|
||||
name="enrollmentFlow">
|
||||
<select class="pf-c-form-control">
|
||||
${until(new FlowsApi(DEFAULT_CONFIG).flowsInstancesList({
|
||||
ordering: "pk",
|
||||
designation: FlowDesignationEnum.Enrollment,
|
||||
}).then(flows => {
|
||||
return flows.results.map(flow => {
|
||||
let selected = this.source?.enrollmentFlow === flow.pk;
|
||||
if (!this.source?.pk && !this.source?.enrollmentFlow && flow.slug === "default-source-enrollment") {
|
||||
selected = true;
|
||||
}
|
||||
return html`<option value=${ifDefined(flow.pk)} ?selected=${selected}>${flow.name} (${flow.slug})</option>`;
|
||||
});
|
||||
}), html`<option>${t`Loading...`}</option>`)}
|
||||
</select>
|
||||
<p class="pf-c-form__helper-text">${t`Flow to use when enrolling new users.`}</p>
|
||||
</ak-form-element-horizontal>
|
||||
</div>
|
||||
</ak-form-group>
|
||||
</form>`;
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue