sources/plex: initial plex source implementation
Signed-off-by: Jens Langhammer <jens.langhammer@beryju.org>
This commit is contained in:
		| @ -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
									
								
								authentik/sources/plex/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/sources/plex/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										21
									
								
								authentik/sources/plex/api.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								authentik/sources/plex/api.py
									
									
									
									
									
										Normal file
									
								
							| @ -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" | ||||
							
								
								
									
										10
									
								
								authentik/sources/plex/apps.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								authentik/sources/plex/apps.py
									
									
									
									
									
										Normal file
									
								
							| @ -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" | ||||
							
								
								
									
										45
									
								
								authentik/sources/plex/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								authentik/sources/plex/migrations/0001_initial.py
									
									
									
									
									
										Normal file
									
								
							| @ -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
									
								
								authentik/sources/plex/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								authentik/sources/plex/migrations/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										42
									
								
								authentik/sources/plex/models.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								authentik/sources/plex/models.py
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||
|  | ||||
							
								
								
									
										65
									
								
								web/src/flows/sources/plex/API.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										65
									
								
								web/src/flows/sources/plex/API.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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"; | ||||
|         }); | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										11
									
								
								web/src/flows/sources/plex/PlexLoginInit.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								web/src/flows/sources/plex/PlexLoginInit.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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> { | ||||
|  | ||||
							
								
								
									
										193
									
								
								web/src/pages/sources/plex/PlexSourceForm.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										193
									
								
								web/src/pages/sources/plex/PlexSourceForm.ts
									
									
									
									
									
										Normal file
									
								
							| @ -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
	
	Block a user
	 Jens Langhammer
					Jens Langhammer