Compare commits
18 Commits
renovate/e
...
preview-em
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32d8c07daf | ||
|
|
47cc0c6079 | ||
|
|
8de83b5752 | ||
|
|
d9f8d200d9 | ||
|
|
e39ad0acfa | ||
|
|
3936f3703f | ||
|
|
23bc1bd44b | ||
|
|
724727483c | ||
|
|
527bb1c2da | ||
|
|
7ae5eec648 | ||
|
|
5cc6b0aeb9 | ||
|
|
7973ad0ecd | ||
|
|
4c9b5484a8 | ||
|
|
d1cec17e0b | ||
|
|
844bfe65a2 | ||
|
|
a5979933f8 | ||
|
|
0e588bf315 | ||
|
|
f5b3b06dfe |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -71,3 +71,6 @@ tsconfig.tsbuildinfo
|
|||||||
|
|
||||||
# Config Cache Directory
|
# Config Cache Directory
|
||||||
config/cache
|
config/cache
|
||||||
|
|
||||||
|
# Mise
|
||||||
|
mise.toml
|
||||||
|
|||||||
211
server/api/embyconnect.ts
Normal file
211
server/api/embyconnect.ts
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
|
import { ApiErrorCode } from '@server/constants/error';
|
||||||
|
import { MediaServerType } from '@server/constants/server';
|
||||||
|
import { getSettings } from '@server/lib/settings';
|
||||||
|
import logger from '@server/logger';
|
||||||
|
import { ApiError } from '@server/types/error';
|
||||||
|
import { getAppVersion } from '@server/utils/appVersion';
|
||||||
|
import { getHostname } from '@server/utils/getHostname';
|
||||||
|
import { uniqueId } from 'lodash';
|
||||||
|
import type { JellyfinLoginResponse } from './jellyfin';
|
||||||
|
|
||||||
|
export interface ConnectAuthResponse {
|
||||||
|
AccessToken: string;
|
||||||
|
User: {
|
||||||
|
Id: string;
|
||||||
|
Name: string;
|
||||||
|
Email: string;
|
||||||
|
IsActive: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LinkedServer {
|
||||||
|
Id: string;
|
||||||
|
Url: string;
|
||||||
|
Name: string;
|
||||||
|
SystemId: string;
|
||||||
|
AccessKey: string;
|
||||||
|
LocalAddress: string;
|
||||||
|
UserType: string;
|
||||||
|
SupporterKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LocalUserAuthExchangeResponse {
|
||||||
|
LocalUserId: string;
|
||||||
|
AccessToken: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface EmbyConnectOptions {
|
||||||
|
ClientIP?: string;
|
||||||
|
DeviceId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const EMBY_CONNECT_URL = 'https://connect.emby.media';
|
||||||
|
|
||||||
|
class EmbyConnectAPI extends ExternalAPI {
|
||||||
|
private ClientIP?: string;
|
||||||
|
private DeviceId?: string;
|
||||||
|
|
||||||
|
constructor(options: EmbyConnectOptions = {}) {
|
||||||
|
super(
|
||||||
|
EMBY_CONNECT_URL,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'X-Application': `Jellyseerr/${getAppVersion()}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Accept: 'application/json',
|
||||||
|
...(getSettings().main.mediaServerType === MediaServerType.EMBY &&
|
||||||
|
{}),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
this.ClientIP = options.ClientIP;
|
||||||
|
this.DeviceId = options.DeviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async authenticateConnectUser(Email?: string, Password?: string) {
|
||||||
|
logger.debug(`Attempting to authenticate via EmbyConnect with email:`, {
|
||||||
|
Email,
|
||||||
|
});
|
||||||
|
|
||||||
|
const connectAuthResponse = await this.getConnectUserAccessToken(
|
||||||
|
Email,
|
||||||
|
Password
|
||||||
|
);
|
||||||
|
|
||||||
|
const linkedServers = await this.getValidServers(
|
||||||
|
connectAuthResponse.User.Id,
|
||||||
|
connectAuthResponse.AccessToken
|
||||||
|
);
|
||||||
|
|
||||||
|
const matchingServer = this.findMatchingServer(linkedServers);
|
||||||
|
|
||||||
|
const localUserExchangeResponse = await this.localAuthExchange(
|
||||||
|
matchingServer.AccessKey,
|
||||||
|
connectAuthResponse.User.Id,
|
||||||
|
this.DeviceId
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
User: {
|
||||||
|
Name: connectAuthResponse.User.Name,
|
||||||
|
Email: connectAuthResponse.User.Email,
|
||||||
|
ServerId: matchingServer.SystemId,
|
||||||
|
ServerName: matchingServer.Name,
|
||||||
|
Id: localUserExchangeResponse.LocalUserId,
|
||||||
|
Configuration: {
|
||||||
|
GroupedFolders: [],
|
||||||
|
},
|
||||||
|
Policy: {
|
||||||
|
IsAdministrator: false, // This requires an additional EmbyServer API call, skipping for now
|
||||||
|
},
|
||||||
|
},
|
||||||
|
AccessToken: localUserExchangeResponse.AccessToken,
|
||||||
|
} as JellyfinLoginResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getConnectUserAccessToken(
|
||||||
|
Email?: string,
|
||||||
|
Password?: string
|
||||||
|
): Promise<ConnectAuthResponse> {
|
||||||
|
try {
|
||||||
|
const response = await this.post<ConnectAuthResponse>(
|
||||||
|
'/service/user/authenticate',
|
||||||
|
{ nameOrEmail: Email, rawpw: Password },
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug(`Failed to authenticate using EmbyConnect:`, {
|
||||||
|
label: 'EmbyConnect API',
|
||||||
|
ip: this.ClientIP,
|
||||||
|
error: e.message,
|
||||||
|
});
|
||||||
|
throw new ApiError(
|
||||||
|
e.cause?.status ?? 401,
|
||||||
|
ApiErrorCode.InvalidCredentials
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getValidServers(
|
||||||
|
ConnectUserId: string,
|
||||||
|
AccessToken: string
|
||||||
|
): Promise<LinkedServer[]> {
|
||||||
|
try {
|
||||||
|
const response = await this.get<LinkedServer[]>(`/service/servers`, {
|
||||||
|
params: { userId: ConnectUserId },
|
||||||
|
headers: {
|
||||||
|
'X-Connect-UserToken': AccessToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return response;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(`Failed to retrieve EmbyConnect user server list: `, {
|
||||||
|
label: 'EmbyConnect API',
|
||||||
|
ip: this.ClientIP,
|
||||||
|
error: e.message,
|
||||||
|
});
|
||||||
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private findMatchingServer(linkedEmbyServers: LinkedServer[]): LinkedServer {
|
||||||
|
const settings = getSettings();
|
||||||
|
const matchingServer = linkedEmbyServers.find(
|
||||||
|
(server) => server.SystemId === settings.jellyfin.serverId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!matchingServer) {
|
||||||
|
throw new Error(
|
||||||
|
`No matching linked Emby server found for serverId: ${settings.jellyfin.serverId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return matchingServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async localAuthExchange(
|
||||||
|
accessKey: string,
|
||||||
|
userId: string,
|
||||||
|
deviceId?: string
|
||||||
|
): Promise<LocalUserAuthExchangeResponse> {
|
||||||
|
try {
|
||||||
|
const params = new URLSearchParams({
|
||||||
|
format: 'json',
|
||||||
|
ConnectUserId: userId,
|
||||||
|
'X-Emby-Client': 'Jellyseerr',
|
||||||
|
'X-Emby-Device-Id': deviceId ?? uniqueId(),
|
||||||
|
'X-Emby-Client-Version': getAppVersion(),
|
||||||
|
'X-Emby-Device-Name': 'Jellyseerr',
|
||||||
|
'X-Emby-Token': accessKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
const response = await fetch(
|
||||||
|
`${getHostname()}/emby/Connect/Exchange?${params}`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(response.statusText, { cause: response });
|
||||||
|
}
|
||||||
|
|
||||||
|
return await response.json();
|
||||||
|
} catch (e) {
|
||||||
|
logger.debug('Failed local user auth exchange', e.cause);
|
||||||
|
throw new ApiError(e.cause?.status, ApiErrorCode.InvalidAuthToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default EmbyConnectAPI;
|
||||||
@@ -3,6 +3,7 @@ import type { AxiosInstance, AxiosRequestConfig } from 'axios';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import rateLimit from 'axios-rate-limit';
|
import rateLimit from 'axios-rate-limit';
|
||||||
import type NodeCache from 'node-cache';
|
import type NodeCache from 'node-cache';
|
||||||
|
import querystring from 'querystring';
|
||||||
|
|
||||||
// 5 minute default TTL (in seconds)
|
// 5 minute default TTL (in seconds)
|
||||||
const DEFAULT_TTL = 300;
|
const DEFAULT_TTL = 300;
|
||||||
@@ -82,6 +83,7 @@ class ExternalAPI {
|
|||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const cacheKey = this.serializeCacheKey(endpoint, {
|
const cacheKey = this.serializeCacheKey(endpoint, {
|
||||||
config: config?.params,
|
config: config?.params,
|
||||||
|
headers: config?.headers,
|
||||||
...(data ? { data } : {}),
|
...(data ? { data } : {}),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -90,7 +92,16 @@ class ExternalAPI {
|
|||||||
return cachedItem;
|
return cachedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this.axios.post<T>(endpoint, data, config);
|
const isFormUrlEncoded = (
|
||||||
|
config?.headers?.['Content-Type'] as string
|
||||||
|
)?.includes('application/x-www-form-urlencoded');
|
||||||
|
|
||||||
|
const body =
|
||||||
|
data && isFormUrlEncoded
|
||||||
|
? querystring.stringify(data as Record<string, string>)
|
||||||
|
: data;
|
||||||
|
|
||||||
|
const response = await this.axios.post<T>(endpoint, body, config);
|
||||||
|
|
||||||
if (this.cache && ttl !== 0) {
|
if (this.cache && ttl !== 0) {
|
||||||
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
this.cache.set(cacheKey, response.data, ttl ?? DEFAULT_TTL);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import EmbyConnectAPI from '@server/api/embyconnect';
|
||||||
import ExternalAPI from '@server/api/externalapi';
|
import ExternalAPI from '@server/api/externalapi';
|
||||||
import { ApiErrorCode } from '@server/constants/error';
|
import { ApiErrorCode } from '@server/constants/error';
|
||||||
import { MediaServerType } from '@server/constants/server';
|
import { MediaServerType } from '@server/constants/server';
|
||||||
@@ -7,9 +8,11 @@ import { getSettings } from '@server/lib/settings';
|
|||||||
import logger from '@server/logger';
|
import logger from '@server/logger';
|
||||||
import { ApiError } from '@server/types/error';
|
import { ApiError } from '@server/types/error';
|
||||||
import { getAppVersion } from '@server/utils/appVersion';
|
import { getAppVersion } from '@server/utils/appVersion';
|
||||||
|
import * as EmailValidator from 'email-validator';
|
||||||
|
|
||||||
export interface JellyfinUserResponse {
|
export interface JellyfinUserResponse {
|
||||||
Name: string;
|
Name: string;
|
||||||
|
Email?: string;
|
||||||
ServerId: string;
|
ServerId: string;
|
||||||
ServerName: string;
|
ServerName: string;
|
||||||
Id: string;
|
Id: string;
|
||||||
@@ -119,6 +122,7 @@ export interface JellyfinItemsReponse {
|
|||||||
|
|
||||||
class JellyfinAPI extends ExternalAPI {
|
class JellyfinAPI extends ExternalAPI {
|
||||||
private userId?: string;
|
private userId?: string;
|
||||||
|
private deviceId?: string;
|
||||||
private mediaServerType: MediaServerType;
|
private mediaServerType: MediaServerType;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -150,7 +154,7 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
this.deviceId = deviceId ? deviceId : undefined;
|
||||||
this.mediaServerType = settings.main.mediaServerType;
|
this.mediaServerType = settings.main.mediaServerType;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,6 +177,31 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
getSettings().main.mediaServerType === MediaServerType.EMBY &&
|
||||||
|
Username &&
|
||||||
|
EmailValidator.validate(Username)
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const connectApi = new EmbyConnectAPI({
|
||||||
|
ClientIP: ClientIP,
|
||||||
|
DeviceId: this.deviceId,
|
||||||
|
});
|
||||||
|
return await connectApi.authenticateConnectUser(Username, Password);
|
||||||
|
} catch (e) {
|
||||||
|
// Possible local Emby user with email as username
|
||||||
|
logger.warn(
|
||||||
|
`Emby Connect authentication failed: ${e}, attempting local Emby server authentication`,
|
||||||
|
{
|
||||||
|
label: 'Jellyfin API',
|
||||||
|
error:
|
||||||
|
e.cause?.message ?? e.cause?.statusText ?? ApiErrorCode.Unknown,
|
||||||
|
ip: ClientIP,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return await authenticate(true);
|
return await authenticate(true);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -245,9 +274,9 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
|
|
||||||
public async getUsers(): Promise<JellyfinUserListResponse> {
|
public async getUsers(): Promise<JellyfinUserListResponse> {
|
||||||
try {
|
try {
|
||||||
const userReponse = await this.get<JellyfinUserResponse[]>(`/Users`);
|
const userResponse = await this.get<JellyfinUserResponse[]>(`/Users`);
|
||||||
|
|
||||||
return { users: userReponse };
|
return { users: userResponse };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
|
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
|
||||||
@@ -260,10 +289,10 @@ class JellyfinAPI extends ExternalAPI {
|
|||||||
|
|
||||||
public async getUser(): Promise<JellyfinUserResponse> {
|
public async getUser(): Promise<JellyfinUserResponse> {
|
||||||
try {
|
try {
|
||||||
const userReponse = await this.get<JellyfinUserResponse>(
|
const userResponse = await this.get<JellyfinUserResponse>(
|
||||||
`/Users/${this.userId ?? 'Me'}`
|
`/Users/${this.userId ?? 'Me'}`
|
||||||
);
|
);
|
||||||
return userReponse;
|
return userResponse;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
logger.error(
|
logger.error(
|
||||||
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
|
`Something went wrong while getting the account from the Jellyfin server: ${e.message}`,
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ authRoutes.get('/me', isAuthenticated(), async (req, res) => {
|
|||||||
user.warnings.push('userEmailRequired');
|
user.warnings.push('userEmailRequired');
|
||||||
logger.warn(`User ${user.username} has no valid email address`);
|
logger.warn(`User ${user.username} has no valid email address`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return res.status(200).json(user);
|
return res.status(200).json(user);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -416,25 +415,34 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
}
|
}
|
||||||
// User already exists, let's update their information
|
// User already exists, let's update their information
|
||||||
else if (account.User.Id === user?.jellyfinUserId) {
|
else if (account.User.Id === user?.jellyfinUserId) {
|
||||||
|
const serverType =
|
||||||
|
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||||
|
? ServerType.JELLYFIN
|
||||||
|
: ServerType.EMBY;
|
||||||
|
|
||||||
|
const userType =
|
||||||
|
serverType === ServerType.JELLYFIN ? UserType.JELLYFIN : UserType.EMBY;
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
`Found matching ${
|
`Found matching ${serverType} user; updating user with ${serverType}`,
|
||||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
|
||||||
? ServerType.JELLYFIN
|
|
||||||
: ServerType.EMBY
|
|
||||||
} user; updating user with ${
|
|
||||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
|
||||||
? ServerType.JELLYFIN
|
|
||||||
: ServerType.EMBY
|
|
||||||
}`,
|
|
||||||
{
|
{
|
||||||
label: 'API',
|
label: 'API',
|
||||||
ip: req.ip,
|
ip: req.ip,
|
||||||
jellyfinUsername: account.User.Name,
|
jellyfinUsername: account.User.Name,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
user.userType = userType;
|
||||||
user.avatar = getUserAvatarUrl(user);
|
user.avatar = getUserAvatarUrl(user);
|
||||||
user.jellyfinUsername = account.User.Name;
|
user.jellyfinUsername = account.User.Name;
|
||||||
|
|
||||||
|
if (
|
||||||
|
account.User.Email !== undefined &&
|
||||||
|
user.email !== account.User.Email
|
||||||
|
) {
|
||||||
|
user.email = account.User.Email;
|
||||||
|
}
|
||||||
|
|
||||||
if (user.username === account.User.Name) {
|
if (user.username === account.User.Name) {
|
||||||
user.username = '';
|
user.username = '';
|
||||||
}
|
}
|
||||||
@@ -455,34 +463,59 @@ authRoutes.post('/jellyfin', async (req, res, next) => {
|
|||||||
message: 'Access denied.',
|
message: 'Access denied.',
|
||||||
});
|
});
|
||||||
} else if (!user) {
|
} else if (!user) {
|
||||||
logger.info(
|
// Handle Emby Connect user with unlinked local account
|
||||||
'Sign-in attempt from Jellyfin user with access to the media server; creating new Jellyseerr user',
|
if (
|
||||||
{
|
settings.main.mediaServerType === MediaServerType.EMBY &&
|
||||||
label: 'API',
|
account.User.Email &&
|
||||||
ip: req.ip,
|
account.User.Email.trim() !== ''
|
||||||
jellyfinUsername: account.User.Name,
|
) {
|
||||||
}
|
user = await userRepository.findOne({
|
||||||
);
|
where: { email: account.User.Email },
|
||||||
|
});
|
||||||
user = new User({
|
}
|
||||||
email: body.email,
|
|
||||||
jellyfinUsername: account.User.Name,
|
if (user) {
|
||||||
jellyfinUserId: account.User.Id,
|
logger.info(
|
||||||
jellyfinDeviceId: deviceId,
|
`Sign in attempt from EmbyConnect user with access to the media server, linking users`
|
||||||
permissions: settings.main.defaultPermissions,
|
);
|
||||||
userType:
|
user.avatar = getUserAvatarUrl(user);
|
||||||
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
user.jellyfinUserId = account.User.Id;
|
||||||
? UserType.JELLYFIN
|
user.userType = UserType.EMBY;
|
||||||
: UserType.EMBY,
|
user.username = account.User.Name;
|
||||||
});
|
await userRepository.save(user);
|
||||||
user.avatar = getUserAvatarUrl(user);
|
|
||||||
|
// No user, create new
|
||||||
//initialize Jellyfin/Emby users with local login
|
} else {
|
||||||
const passedExplicitPassword = body.password && body.password.length > 0;
|
logger.info(
|
||||||
if (passedExplicitPassword) {
|
'Sign-in attempt from Jellyfin/Emby user with access to the media server; creating new Jellyseerr user',
|
||||||
await user.setPassword(body.password ?? '');
|
{
|
||||||
|
label: 'API',
|
||||||
|
ip: req.ip,
|
||||||
|
jellyfinUsername: account.User.Name,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
user = new User({
|
||||||
|
email: body.email,
|
||||||
|
jellyfinUsername: account.User.Name,
|
||||||
|
jellyfinUserId: account.User.Id,
|
||||||
|
jellyfinDeviceId: deviceId,
|
||||||
|
permissions: settings.main.defaultPermissions,
|
||||||
|
userType:
|
||||||
|
settings.main.mediaServerType === MediaServerType.JELLYFIN
|
||||||
|
? UserType.JELLYFIN
|
||||||
|
: UserType.EMBY,
|
||||||
|
});
|
||||||
|
user.avatar = getUserAvatarUrl(user);
|
||||||
|
|
||||||
|
//initialize Jellyfin/Emby users with local login
|
||||||
|
const passedExplicitPassword =
|
||||||
|
body.password && body.password.length > 0;
|
||||||
|
if (passedExplicitPassword) {
|
||||||
|
await user.setPassword(body.password ?? '');
|
||||||
|
}
|
||||||
|
await userRepository.save(user);
|
||||||
}
|
}
|
||||||
await userRepository.save(user);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (user && user.jellyfinUserId) {
|
if (user && user.jellyfinUserId) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import * as Yup from 'yup';
|
|||||||
const messages = defineMessages('components.Login', {
|
const messages = defineMessages('components.Login', {
|
||||||
loginwithapp: 'Login with {appName}',
|
loginwithapp: 'Login with {appName}',
|
||||||
username: 'Username',
|
username: 'Username',
|
||||||
|
email: 'Email Address',
|
||||||
password: 'Password',
|
password: 'Password',
|
||||||
validationusernamerequired: 'Username required',
|
validationusernamerequired: 'Username required',
|
||||||
validationpasswordrequired: 'Password required',
|
validationpasswordrequired: 'Password required',
|
||||||
@@ -125,7 +126,9 @@ const JellyfinLogin: React.FC<JellyfinLoginProps> = ({
|
|||||||
id="username"
|
id="username"
|
||||||
name="username"
|
name="username"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={intl.formatMessage(messages.username)}
|
placeholder={`${intl.formatMessage(
|
||||||
|
messages.email
|
||||||
|
)} / ${intl.formatMessage(messages.username)}`}
|
||||||
className="!bg-gray-700/80 placeholder:text-gray-400"
|
className="!bg-gray-700/80 placeholder:text-gray-400"
|
||||||
data-form-type="username"
|
data-form-type="username"
|
||||||
/>
|
/>
|
||||||
|
|||||||
Reference in New Issue
Block a user