Compare commits

...

27 Commits

Author SHA1 Message Date
jc21
f3efaae320 Merge pull request #5141 from NginxProxyManager/develop
v2.13.6
2026-01-14 14:30:49 +10:00
jc21
7b3c1fd061 Merge branch 'master' into develop 2026-01-14 13:47:51 +10:00
Jamie Curnow
ee42202348 Bump version 2026-01-14 13:34:17 +10:00
Jamie Curnow
c1ad7788f1 Changed 2fa delete from body to query for code
as per best practices
2026-01-14 13:24:38 +10:00
Jamie Curnow
d33bb02c74 Add missing params to swagger 2026-01-14 12:46:30 +10:00
Jamie Curnow
462c134751 2fa work slight refactor
- use existing access mechanisms for validation
- adds swagger/schema and validation of incoming payload
2026-01-14 11:45:12 +10:00
jc21
b7dfaddbb1 Merge pull request #4970 from zdzichu6969/develop
All checks were successful
Close stale issues and PRs / stale (push) Successful in 33s
Polish Translation Fixes
2026-01-14 07:33:49 +10:00
jc21
11ee4f0820 Merge pull request #4965 from archettitechnology/develop
Update Italian locale message for empty objects
2026-01-14 07:32:07 +10:00
jc21
19970a4220 Merge pull request #5095 from aindriu80/develop
feat: (i18n) Added Irish translation
2026-01-14 07:26:10 +10:00
jc21
3e3396ba9a Update lang-list.json 2026-01-13 15:05:13 +10:00
jc21
6c0ea835ce Merge branch 'develop' into develop 2026-01-13 14:46:35 +10:00
Aindriú Mac Giolla Eoin
f0c0b465d9 Removiving 0x200b - Zero width space 2025-12-20 17:53:05 +00:00
Aindriú Mac Giolla Eoin
6c2f6a9d39 Fixing plural/iolra issue 2025-12-19 11:43:18 +00:00
Aindriú Mac Giolla Eoin
2f6e3ad804 Added Irish translation 2025-12-18 18:21:14 +00:00
angioletto
7fe5070337 Merge branch 'NginxProxyManager:develop' into develop 2025-12-06 14:56:52 +01:00
Mateusz Gruszczyński
073ee95e56 change 2025-12-02 12:57:09 +01:00
Mateusz Gruszczyński
168078eb40 changes 2025-11-26 10:54:30 +01:00
Mateusz Gruszczyński
2c9f8f4d64 changes 2025-11-26 10:50:41 +01:00
Mateusz Gruszczyński
8403a0c761 changes 2025-11-26 10:42:48 +01:00
angioletto
927e57257b Merge branch 'NginxProxyManager:develop' into develop 2025-11-21 17:03:47 +01:00
Mateusz Gruszczyński
56875bba52 pretty :) 2025-11-19 21:23:23 +01:00
Mateusz Gruszczyński
b55f51bd63 fixes1 in pl 2025-11-19 15:10:56 +01:00
Mateusz Gruszczyński
86b7394620 fixes1 2025-11-19 11:01:25 +01:00
Mateusz Gruszczyński
91a1f39c02 fixes1 2025-11-19 10:53:55 +01:00
angioletto
5c114e9db7 Update Italian locale message for empty objects
Wrong translation of line 431
2025-11-19 09:56:05 +01:00
Mateusz Gruszczyński
fec9bffe29 fixes1 2025-11-19 09:13:55 +01:00
jc21
847c58b170 Merge pull request #4956 from NginxProxyManager/develop
v2.13.5
2025-11-18 21:13:24 +10:00
30 changed files with 1447 additions and 510 deletions

View File

@@ -1 +1 @@
2.13.5
2.13.6

View File

@@ -1,7 +1,7 @@
<p align="center">
<img src="https://nginxproxymanager.com/github.png">
<br><br>
<img src="https://img.shields.io/badge/version-2.13.5-green.svg?style=for-the-badge">
<img src="https://img.shields.io/badge/version-2.13.6-green.svg?style=for-the-badge">
<a href="https://hub.docker.com/repository/docker/jc21/nginx-proxy-manager">
<img src="https://img.shields.io/docker/stars/jc21/nginx-proxy-manager.svg?style=for-the-badge">
</a>

View File

@@ -1,9 +1,9 @@
import bcrypt from "bcrypt";
import crypto from "node:crypto";
import bcrypt from "bcrypt";
import { authenticator } from "otplib";
import authModel from "../models/auth.js";
import userModel from "../models/user.js";
import errs from "../lib/error.js";
import authModel from "../models/auth.js";
import internalUser from "./user.js";
const APP_NAME = "Nginx Proxy Manager";
const BACKUP_CODE_COUNT = 8;
@@ -26,38 +26,7 @@ const generateBackupCodes = async () => {
return { plain, hashed };
};
export default {
/**
* Generate a new TOTP secret
* @returns {string}
*/
generateSecret: () => {
return authenticator.generateSecret();
},
/**
* Generate otpauth URL for QR code
* @param {string} email
* @param {string} secret
* @returns {string}
*/
generateOTPAuthURL: (email, secret) => {
return authenticator.keyuri(email, APP_NAME, secret);
},
/**
* Verify a TOTP code
* @param {string} secret
* @param {string} code
* @returns {boolean}
*/
verifyCode: (secret, code) => {
try {
return authenticator.verify({ token: code, secret });
} catch {
return false;
}
},
const internal2fa = {
/**
* Check if user has 2FA enabled
@@ -65,94 +34,85 @@ export default {
* @returns {Promise<boolean>}
*/
isEnabled: async (userId) => {
const auth = await authModel
.query()
.where("user_id", userId)
.where("type", "password")
.first();
if (!auth || !auth.meta) {
return false;
}
return auth.meta.totp_enabled === true;
const auth = await internal2fa.getUserPasswordAuth(userId);
return auth?.meta?.totp_enabled === true;
},
/**
* Get 2FA status for user
* @param {number} userId
* @returns {Promise<{enabled: boolean, backupCodesRemaining: number}>}
* @param {Access} access
* @param {number} userId
* @returns {Promise<{enabled: boolean, backup_codes_remaining: number}>}
*/
getStatus: async (userId) => {
const auth = await authModel
.query()
.where("user_id", userId)
.where("type", "password")
.first();
getStatus: async (access, userId) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const enabled = auth?.meta?.totp_enabled === true;
let backup_codes_remaining = 0;
if (!auth || !auth.meta || !auth.meta.totp_enabled) {
return { enabled: false, backupCodesRemaining: 0 };
if (enabled) {
const backupCodes = auth.meta.backup_codes || [];
backup_codes_remaining = backupCodes.length;
}
const backupCodes = auth.meta.backup_codes || [];
return {
enabled: true,
backupCodesRemaining: backupCodes.length,
enabled,
backup_codes_remaining,
};
},
/**
* Start 2FA setup - store pending secret
* @param {number} userId
* @returns {Promise<{secret: string, otpauthUrl: string}>}
*
* @param {Access} access
* @param {number} userId
* @returns {Promise<{secret: string, otpauth_url: string}>}
*/
startSetup: async (userId) => {
const user = await userModel.query().where("id", userId).first();
if (!user) {
throw new errs.ItemNotFoundError("User not found");
}
startSetup: async (access, userId) => {
await access.can("users:password", userId);
const user = await internalUser.get(access, { id: userId });
const secret = authenticator.generateSecret();
const otpauthUrl = authenticator.keyuri(user.email, APP_NAME, secret);
const otpauth_url = authenticator.keyuri(user.email, APP_NAME, secret);
const auth = await internal2fa.getUserPasswordAuth(userId);
const auth = await authModel
.query()
.where("user_id", userId)
.where("type", "password")
.first();
if (!auth) {
throw new errs.ItemNotFoundError("Auth record not found");
// ensure user isn't already setup for 2fa
const enabled = auth?.meta?.totp_enabled === true;
if (enabled) {
throw new errs.ValidationError("2FA is already enabled");
}
const meta = auth.meta || {};
meta.totp_pending_secret = secret;
await authModel.query().where("id", auth.id).patch({ meta });
await authModel.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return { secret, otpauthUrl };
return { secret, otpauth_url };
},
/**
* Enable 2FA after verifying code
* @param {number} userId
* @param {string} code
* @returns {Promise<{backupCodes: string[]}>}
*
* @param {Access} access
* @param {number} userId
* @param {string} code
* @returns {Promise<{backup_codes: string[]}>}
*/
enable: async (userId, code) => {
const auth = await authModel
.query()
.where("user_id", userId)
.where("type", "password")
.first();
enable: async (access, userId, code) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const secret = auth?.meta?.totp_pending_secret || false;
if (!auth || !auth.meta || !auth.meta.totp_pending_secret) {
if (!secret) {
throw new errs.ValidationError("No pending 2FA setup found");
}
const secret = auth.meta.totp_pending_secret;
const valid = authenticator.verify({ token: code, secret });
if (!valid) {
throw new errs.ValidationError("Invalid verification code");
}
@@ -168,25 +128,31 @@ export default {
};
delete meta.totp_pending_secret;
await authModel.query().where("id", auth.id).patch({ meta });
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return { backupCodes: plain };
return { backup_codes: plain };
},
/**
* Disable 2FA
* @param {number} userId
* @param {string} code
*
* @param {Access} access
* @param {number} userId
* @param {string} code
* @returns {Promise<void>}
*/
disable: async (userId, code) => {
const auth = await authModel
.query()
.where("user_id", userId)
.where("type", "password")
.first();
disable: async (access, userId, code) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
if (!auth || !auth.meta || !auth.meta.totp_enabled) {
const enabled = auth?.meta?.totp_enabled === true;
if (!enabled) {
throw new errs.ValidationError("2FA is not enabled");
}
@@ -196,7 +162,7 @@ export default {
});
if (!valid) {
throw new errs.ValidationError("Invalid verification code");
throw new errs.AuthError("Invalid verification code");
}
const meta = { ...auth.meta };
@@ -205,30 +171,33 @@ export default {
delete meta.totp_enabled_at;
delete meta.backup_codes;
await authModel.query().where("id", auth.id).patch({ meta });
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
},
/**
* Verify 2FA code for login
* @param {number} userId
* @param {string} code
*
* @param {number} userId
* @param {string} token
* @returns {Promise<boolean>}
*/
verifyForLogin: async (userId, code) => {
const auth = await authModel
.query()
.where("user_id", userId)
.where("type", "password")
.first();
verifyForLogin: async (userId, token) => {
const auth = await internal2fa.getUserPasswordAuth(userId);
const secret = auth?.meta?.totp_secret || false;
if (!auth || !auth.meta || !auth.meta.totp_secret) {
if (!secret) {
return false;
}
// Try TOTP code first
const valid = authenticator.verify({
token: code,
secret: auth.meta.totp_secret,
token,
secret,
});
if (valid) {
@@ -236,7 +205,7 @@ export default {
}
// Try backup codes
const backupCodes = auth.meta.backup_codes || [];
const backupCodes = auth?.meta?.backup_codes || [];
for (let i = 0; i < backupCodes.length; i++) {
const match = await bcrypt.compare(code.toUpperCase(), backupCodes[i]);
if (match) {
@@ -244,7 +213,12 @@ export default {
const updatedCodes = [...backupCodes];
updatedCodes.splice(i, 1);
const meta = { ...auth.meta, backup_codes: updatedCodes };
await authModel.query().where("id", auth.id).patch({ meta });
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return true;
}
}
@@ -254,24 +228,29 @@ export default {
/**
* Regenerate backup codes
* @param {number} userId
* @param {string} code
* @returns {Promise<{backupCodes: string[]}>}
*
* @param {Access} access
* @param {number} userId
* @param {string} token
* @returns {Promise<{backup_codes: string[]}>}
*/
regenerateBackupCodes: async (userId, code) => {
const auth = await authModel
.query()
.where("user_id", userId)
.where("type", "password")
.first();
regenerateBackupCodes: async (access, userId, token) => {
await access.can("users:password", userId);
await internalUser.get(access, { id: userId });
const auth = await internal2fa.getUserPasswordAuth(userId);
const enabled = auth?.meta?.totp_enabled === true;
const secret = auth?.meta?.totp_secret || false;
if (!auth || !auth.meta || !auth.meta.totp_enabled) {
if (!enabled) {
throw new errs.ValidationError("2FA is not enabled");
}
if (!secret) {
throw new errs.ValidationError("No 2FA secret found");
}
const valid = authenticator.verify({
token: code,
secret: auth.meta.totp_secret,
token,
secret,
});
if (!valid) {
@@ -281,8 +260,29 @@ export default {
const { plain, hashed } = await generateBackupCodes();
const meta = { ...auth.meta, backup_codes: hashed };
await authModel.query().where("id", auth.id).patch({ meta });
await authModel
.query()
.where("id", auth.id)
.andWhere("user_id", userId)
.andWhere("type", "password")
.patch({ meta });
return { backupCodes: plain };
return { backup_codes: plain };
},
getUserPasswordAuth: async (userId) => {
const auth = await authModel
.query()
.where("user_id", userId)
.andWhere("type", "password")
.first();
if (!auth) {
throw new errs.ItemNotFoundError("Auth not found");
}
return auth;
},
};
export default internal2fa;

View File

@@ -66,16 +66,7 @@ router
*/
.post(async (req, res, next) => {
try {
const { challenge_token, code } = req.body;
if (!challenge_token || !code) {
return res.status(400).json({
error: {
message: "Missing challenge_token or code",
},
});
}
const { challenge_token, code } = await apiValidator(getValidationSchema("/tokens/2fa", "post"), req.body);
const result = await internalToken.verify2FA(challenge_token, code);
res.status(200).send(result);
} catch (err) {

View File

@@ -339,6 +339,21 @@ router
.all(jwtdecode())
.all(userIdFromMe)
/**
* POST /api/users/123/2fa
*
* Start 2FA setup, returns QR code URL
*/
.post(async (req, res, next) => {
try {
const result = await internal2FA.startSetup(res.locals.access, req.params.user_id);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
})
/**
* GET /api/users/123/2fa
*
@@ -346,15 +361,7 @@ router
*/
.get(async (req, res, next) => {
try {
const userId = Number.parseInt(req.params.user_id, 10);
const access = res.locals.access;
// Users can only view their own 2FA status
if (access.token.getUserId() !== userId && !access.token.hasScope("admin")) {
throw new errs.PermissionError("Cannot view 2FA status for other users");
}
const status = await internal2FA.getStatus(userId);
const status = await internal2FA.getStatus(res.locals.access, req.params.user_id);
res.status(200).send(status);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -363,63 +370,18 @@ router
})
/**
* DELETE /api/users/123/2fa
* DELETE /api/users/123/2fa?code=XXXXXX
*
* Disable 2FA for a user
*/
.delete(async (req, res, next) => {
try {
const userId = Number.parseInt(req.params.user_id, 10);
const access = res.locals.access;
// Users can only disable their own 2FA
if (access.token.getUserId() !== userId && !access.token.hasScope("admin")) {
throw new errs.PermissionError("Cannot disable 2FA for other users");
}
const { code } = req.body;
const code = typeof req.query.code === "string" ? req.query.code : null;
if (!code) {
throw new errs.ValidationError("Verification code is required");
throw new errs.ValidationError("Missing required parameter: code");
}
await internal2FA.disable(userId, code);
res.status(200).send({ success: true });
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
}
});
/**
* User 2FA setup
*
* /api/users/123/2fa/setup
*/
router
.route("/:user_id/2fa/setup")
.options((_, res) => {
res.sendStatus(204);
})
.all(jwtdecode())
.all(userIdFromMe)
/**
* POST /api/users/123/2fa/setup
*
* Start 2FA setup, returns QR code URL
*/
.post(async (req, res, next) => {
try {
const userId = Number.parseInt(req.params.user_id, 10);
const access = res.locals.access;
// Users can only setup their own 2FA
if (access.token.getUserId() !== userId) {
throw new errs.PermissionError("Cannot setup 2FA for other users");
}
const result = await internal2FA.startSetup(userId);
res.status(200).send(result);
await internal2FA.disable(res.locals.access, req.params.user_id, code);
res.status(200).send(true);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
next(err);
@@ -440,26 +402,17 @@ router
.all(userIdFromMe)
/**
* PUT /api/users/123/2fa/enable
* POST /api/users/123/2fa/enable
*
* Verify code and enable 2FA
*/
.put(async (req, res, next) => {
.post(async (req, res, next) => {
try {
const userId = Number.parseInt(req.params.user_id, 10);
const access = res.locals.access;
// Users can only enable their own 2FA
if (access.token.getUserId() !== userId) {
throw new errs.PermissionError("Cannot enable 2FA for other users");
}
const { code } = req.body;
if (!code) {
throw new errs.ValidationError("Verification code is required");
}
const result = await internal2FA.enable(userId, code);
const { code } = await apiValidator(
getValidationSchema("/users/{userID}/2fa/enable", "post"),
req.body,
);
const result = await internal2FA.enable(res.locals.access, req.params.user_id, code);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);
@@ -487,20 +440,11 @@ router
*/
.post(async (req, res, next) => {
try {
const userId = Number.parseInt(req.params.user_id, 10);
const access = res.locals.access;
// Users can only regenerate their own backup codes
if (access.token.getUserId() !== userId) {
throw new errs.PermissionError("Cannot regenerate backup codes for other users");
}
const { code } = req.body;
if (!code) {
throw new errs.ValidationError("Verification code is required");
}
const result = await internal2FA.regenerateBackupCodes(userId, code);
const { code } = await apiValidator(
getValidationSchema("/users/{userID}/2fa/backup-codes", "post"),
req.body,
);
const result = await internal2FA.regenerateBackupCodes(res.locals.access, req.params.user_id, code);
res.status(200).send(result);
} catch (err) {
debug(logger, `${req.method.toUpperCase()} ${req.path}: ${err}`);

View File

@@ -0,0 +1,18 @@
{
"type": "object",
"description": "Token object",
"required": ["requires_2fa", "challenge_token"],
"additionalProperties": false,
"properties": {
"requires_2fa": {
"description": "Whether this token request requires two-factor authentication",
"example": true,
"type": "boolean"
},
"challenge_token": {
"description": "Challenge Token used in subsequent 2FA verification",
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
"type": "string"
}
}
}

View File

@@ -0,0 +1,55 @@
{
"operationId": "loginWith2FA",
"summary": "Verify 2FA code and get full token",
"tags": ["tokens"],
"requestBody": {
"description": "2fa Challenge Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"challenge_token": {
"minLength": 1,
"type": "string",
"example": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
},
"code": {
"minLength": 6,
"maxLength": 6,
"type": "string",
"example": "012345"
}
},
"required": ["challenge_token", "code"],
"type": "object"
},
"example": {
"challenge_token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4",
"code": "012345"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"expires": "2025-02-04T20:40:46.340Z",
"token": "eyJhbGciOiJSUzUxMiIsInR5cCI6IkpXVCJ9.ey...xaHKYr3Kk6MvkUjcC4"
}
}
},
"schema": {
"$ref": "../../../components/token-object.json"
}
}
},
"description": "200 response"
}
}
}

View File

@@ -50,7 +50,14 @@
}
},
"schema": {
"$ref": "../../components/token-object.json"
"oneOf": [
{
"$ref": "../../components/token-object.json"
},
{
"$ref": "../../components/token-challenge.json"
}
]
}
}
},

View File

@@ -0,0 +1,92 @@
{
"operationId": "regenUser2faCodes",
"summary": "Regenerate 2FA backup codes",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"requestBody": {
"description": "Verififcation Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"code": {
"minLength": 6,
"maxLength": 6,
"type": "string",
"example": "123456"
}
},
"required": ["code"],
"type": "object"
},
"example": {
"code": "123456"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"backup_codes": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
]
}
}
},
"schema": {
"type": "object",
"required": ["backup_codes"],
"additionalProperties": false,
"properties": {
"backup_codes": {
"description": "Backup codes",
"example": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
],
"type": "array",
"items": {
"type": "string",
"example": "6CD7CB06"
}
}
}
}
}
},
"description": "200 response"
}
}
}

View File

@@ -0,0 +1,48 @@
{
"operationId": "disableUser2fa",
"summary": "Disable 2fa for user",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
},
{
"in": "query",
"name": "code",
"schema": {
"type": "string",
"minLength": 6,
"maxLength": 6,
"example": "012345"
},
"required": true,
"description": "2fa Code",
"example": "012345"
}
],
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": true
}
},
"schema": {
"type": "boolean"
}
}
},
"description": "200 response"
}
}
}

View File

@@ -0,0 +1,92 @@
{
"operationId": "enableUser2fa",
"summary": "Verify code and enable 2FA",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"requestBody": {
"description": "Verififcation Payload",
"required": true,
"content": {
"application/json": {
"schema": {
"additionalProperties": false,
"properties": {
"code": {
"minLength": 6,
"maxLength": 6,
"type": "string",
"example": "123456"
}
},
"required": ["code"],
"type": "object"
},
"example": {
"code": "123456"
}
}
}
},
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"backup_codes": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
]
}
}
},
"schema": {
"type": "object",
"required": ["backup_codes"],
"additionalProperties": false,
"properties": {
"backup_codes": {
"description": "Backup codes",
"example": [
"6CD7CB06",
"495302F3",
"D8037852",
"A6FFC956",
"BC1A1851",
"A05E644F",
"A406D2E8",
"0AE3C522"
],
"type": "array",
"items": {
"type": "string",
"example": "6CD7CB06"
}
}
}
}
}
},
"description": "200 response"
}
}
}

View File

@@ -0,0 +1,57 @@
{
"operationId": "getUser2faStatus",
"summary": "Get user 2fa Status",
"tags": ["users"],
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"responses": {
"200": {
"description": "200 response",
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"enabled": false,
"backup_codes_remaining": 0
}
}
},
"schema": {
"type": "object",
"additionalProperties": false,
"required": ["enabled", "backup_codes_remaining"],
"properties": {
"enabled": {
"type": "boolean",
"description": "Is 2FA enabled for this user",
"example": true
},
"backup_codes_remaining": {
"type": "integer",
"description": "Number of remaining backup codes for this user",
"example": 5
}
}
}
}
}
}
}
}

View File

@@ -0,0 +1,52 @@
{
"operationId": "setupUser2fa",
"summary": "Start 2FA setup, returns QR code URL",
"tags": ["users"],
"parameters": [
{
"in": "path",
"name": "userID",
"schema": {
"type": "integer",
"minimum": 1
},
"required": true,
"description": "User ID",
"example": 2
}
],
"responses": {
"200": {
"content": {
"application/json": {
"examples": {
"default": {
"value": {
"secret": "JZYCEBIEEJYUGPQM",
"otpauth_url": "otpauth://totp/Nginx%20Proxy%20Manager:jc%40jc21.com?secret=JZYCEBIEEJYUGPQM&period=30&digits=6&algorithm=SHA1&issuer=Nginx%20Proxy%20Manager"
}
}
},
"schema": {
"type": "object",
"required": ["secret", "otpauth_url"],
"additionalProperties": false,
"properties": {
"secret": {
"description": "TOTP Secret",
"example": "JZYCEBIEEJYUGPQM",
"type": "string"
},
"otpauth_url": {
"description": "OTP Auth URL for QR Code generation",
"example": "otpauth://totp/Nginx%20Proxy%20Manager:jc%40jc21.com?secret=JZYCEBIEEJYUGPQM&period=30&digits=6&algorithm=SHA1&issuer=Nginx%20Proxy%20Manager",
"type": "string"
}
}
}
}
},
"description": "200 response"
}
}
}

View File

@@ -293,6 +293,11 @@
"$ref": "./paths/tokens/post.json"
}
},
"/tokens/2fa": {
"post": {
"$ref": "./paths/tokens/2fa/post.json"
}
},
"/version/check": {
"get": {
"$ref": "./paths/version/check/get.json"
@@ -317,6 +322,27 @@
"$ref": "./paths/users/userID/delete.json"
}
},
"/users/{userID}/2fa": {
"post": {
"$ref": "./paths/users/userID/2fa/post.json"
},
"get": {
"$ref": "./paths/users/userID/2fa/get.json"
},
"delete": {
"$ref": "./paths/users/userID/2fa/delete.json"
}
},
"/users/{userID}/2fa/enable": {
"post": {
"$ref": "./paths/users/userID/2fa/enable/post.json"
}
},
"/users/{userID}/2fa/backup-codes": {
"post": {
"$ref": "./paths/users/userID/2fa/backup-codes/post.json"
}
},
"/users/{userID}/auth": {
"put": {
"$ref": "./paths/users/userID/auth/put.json"

View File

@@ -1,176 +0,0 @@
# Two-Factor Authentication Implementation
> **Note:** This document should be deleted after PR approval. It serves as a reference for reviewers to understand the scope of the contribution.
---
**Acknowledgments**
Thanks to all contributors and authors from the Inte.Team for the great work on Nginx Proxy Manager. It saves us time and effort, and we're happy to contribute back to the project.
---
## Overview
Add TOTP-based two-factor authentication to the login flow. Users can enable 2FA from their profile settings, scan a QR code with any authenticator app (Google Authenticator, Authy, etc.), and will be required to enter a 6-digit code on login.
## Current Authentication Flow
```
POST /tokens {identity, secret}
-> Validate user exists and is not disabled
-> Verify password against auth.secret
-> Return JWT token
```
## Proposed 2FA Flow
```
POST /tokens {identity, secret}
-> Validate user exists and is not disabled
-> Verify password against auth.secret
-> If 2FA enabled:
Return {requires_2fa: true, challenge_token: <short-lived JWT>}
-> Else:
Return {token: <JWT>, expires: <timestamp>}
POST /tokens/2fa {challenge_token, code}
-> Validate challenge_token
-> Verify TOTP code against user's secret
-> Return {token: <JWT>, expires: <timestamp>}
```
## Database Changes
Extend the existing `auth.meta` JSON column to store 2FA data:
```json
{
"totp_secret": "<encrypted-secret>",
"totp_enabled": true,
"totp_enabled_at": "<timestamp>",
"backup_codes": ["<hashed-code-1>", "<hashed-code-2>", ...]
}
```
No new tables required. The `auth.meta` column is already designed for this purpose.
## Backend Changes
### New Files
1. `backend/internal/2fa.js` - Core 2FA logic
- `generateSecret()` - Generate TOTP secret
- `generateQRCodeURL(user, secret)` - Generate otpauth URL
- `verifyCode(secret, code)` - Verify TOTP code
- `generateBackupCodes()` - Generate 8 backup codes
- `verifyBackupCode(user, code)` - Verify and consume backup code
2. `backend/routes/2fa.js` - 2FA management endpoints
- `GET /users/:id/2fa` - Get 2FA status
- `POST /users/:id/2fa/setup` - Start 2FA setup, return QR code
- `PUT /users/:id/2fa/enable` - Verify code and enable 2FA
- `DELETE /users/:id/2fa` - Disable 2FA (requires code)
- `GET /users/:id/2fa/backup-codes` - View remaining backup codes count
- `POST /users/:id/2fa/backup-codes` - Regenerate backup codes
### Modified Files
1. `backend/internal/token.js`
- Update `getTokenFromEmail()` to check for 2FA
- Add `verifyTwoFactorChallenge()` function
- Add `createChallengeToken()` for short-lived 2FA tokens
2. `backend/routes/tokens.js`
- Add `POST /tokens/2fa` endpoint
3. `backend/index.js`
- Register new 2FA routes
### Dependencies
Add to `package.json`:
```json
"otplib": "^12.0.1"
```
## Frontend Changes
### New Files
1. `frontend/src/pages/Login2FA/index.tsx` - 2FA code entry page
2. `frontend/src/modals/TwoFactorSetupModal.tsx` - Setup wizard modal
3. `frontend/src/api/backend/twoFactor.ts` - 2FA API functions
4. `frontend/src/api/backend/verify2FA.ts` - Token verification
### Modified Files
1. `frontend/src/api/backend/responseTypes.ts`
- Add `TwoFactorChallengeResponse` type
- Add `TwoFactorStatusResponse` type
2. `frontend/src/context/AuthContext.tsx`
- Add `twoFactorRequired` state
- Add `challengeToken` state
- Update `login()` to handle 2FA response
- Add `verify2FA()` function
3. `frontend/src/pages/Login/index.tsx`
- Handle 2FA challenge response
- Redirect to 2FA entry when required
4. `frontend/src/pages/Settings/` (or user profile)
- Add 2FA enable/disable section
### Dependencies
Add to `package.json`:
```json
"qrcode.react": "^3.1.0"
```
## API Endpoints Summary
| Method | Endpoint | Auth | Description |
|--------|----------|------|-------------|
| POST | /tokens | No | Login (returns challenge if 2FA) |
| POST | /tokens/2fa | Challenge | Complete 2FA login |
| GET | /users/:id/2fa | JWT | Get 2FA status |
| POST | /users/:id/2fa/setup | JWT | Start setup, get QR code |
| PUT | /users/:id/2fa/enable | JWT | Verify and enable |
| DELETE | /users/:id/2fa | JWT | Disable (requires code) |
| GET | /users/:id/2fa/backup-codes | JWT | Get backup codes count |
| POST | /users/:id/2fa/backup-codes | JWT | Regenerate codes |
## Security Considerations
1. Challenge tokens expire in 5 minutes
2. TOTP secrets encrypted at rest
3. Backup codes hashed with bcrypt
4. Rate limit on 2FA attempts (5 attempts, 15 min lockout)
5. Backup codes single-use only
6. 2FA disable requires valid TOTP code
## Implementation Order
1. Backend: Add `otplib` dependency
2. Backend: Create `internal/2fa.js` module
3. Backend: Update `internal/token.js` for challenge flow
4. Backend: Add `POST /tokens/2fa` route
5. Backend: Create `routes/2fa.js` for management
6. Frontend: Add `qrcode.react` dependency
7. Frontend: Update API types and functions
8. Frontend: Update AuthContext for 2FA state
9. Frontend: Create Login2FA page
10. Frontend: Update Login to handle 2FA
11. Frontend: Add 2FA settings UI
## Testing
1. Enable 2FA for user
2. Login with password only - should get challenge
3. Submit correct TOTP - should get token
4. Submit wrong TOTP - should fail
5. Use backup code - should work once
6. Disable 2FA - should require valid code
7. Login after disable - should work without 2FA

View File

@@ -156,7 +156,6 @@ export async function del({ url, params }: DeleteArgs, abortController?: AbortCo
const method = "DELETE";
const headers = {
...buildAuthHeader(),
[contentTypeHeader]: "application/json",
};
const signal = abortController?.signal;
const response = await fetch(apiUrl, { method, headers, signal });

View File

@@ -1,11 +1,5 @@
import { camelizeKeys, decamelizeKeys } from "humps";
import AuthStore from "src/modules/AuthStore";
import type {
TwoFactorEnableResponse,
TwoFactorSetupResponse,
TwoFactorStatusResponse,
} from "./responseTypes";
import * as api from "./base";
import type { TwoFactorEnableResponse, TwoFactorSetupResponse, TwoFactorStatusResponse } from "./responseTypes";
export async function get2FAStatus(userId: number | "me"): Promise<TwoFactorStatusResponse> {
return await api.get({
@@ -15,42 +9,27 @@ export async function get2FAStatus(userId: number | "me"): Promise<TwoFactorStat
export async function start2FASetup(userId: number | "me"): Promise<TwoFactorSetupResponse> {
return await api.post({
url: `/users/${userId}/2fa/setup`,
url: `/users/${userId}/2fa`,
});
}
export async function enable2FA(userId: number | "me", code: string): Promise<TwoFactorEnableResponse> {
return await api.put({
return await api.post({
url: `/users/${userId}/2fa/enable`,
data: { code },
});
}
export async function disable2FA(userId: number | "me", code: string): Promise<{ success: boolean }> {
const headers: Record<string, string> = {
"Content-Type": "application/json",
};
if (AuthStore.token) {
headers.Authorization = `Bearer ${AuthStore.token.token}`;
}
const response = await fetch(`/api/users/${userId}/2fa`, {
method: "DELETE",
headers,
body: JSON.stringify(decamelizeKeys({ code })),
export async function disable2FA(userId: number | "me", code: string): Promise<boolean> {
return await api.del({
url: `/users/${userId}/2fa`,
params: {
code,
},
});
const payload = await response.json();
if (!response.ok) {
throw new Error(payload.error?.messageI18n || payload.error?.message || "Failed to disable 2FA");
}
return camelizeKeys(payload) as { success: boolean };
}
export async function regenerateBackupCodes(
userId: number | "me",
code: string,
): Promise<TwoFactorEnableResponse> {
export async function regenerateBackupCodes(userId: number | "me", code: string): Promise<TwoFactorEnableResponse> {
return await api.post({
url: `/users/${userId}/2fa/backup-codes`,
data: { code },

View File

@@ -2,6 +2,7 @@ import { createIntl, createIntlCache } from "react-intl";
import langDe from "./lang/de.json";
import langEn from "./lang/en.json";
import langEs from "./lang/es.json";
import langGa from "./lang/ga.json";
import langIt from "./lang/it.json";
import langJa from "./lang/ja.json";
import langList from "./lang/lang-list.json";
@@ -22,6 +23,7 @@ const localeOptions = [
["en", "en-US", langEn],
["de", "de-DE", langDe],
["es", "es-ES", langEs],
["ga", "ga-IE", langGa],
["ja", "ja-JP", langJa],
["it", "it-IT", langIt],
["nl", "nl-NL", langNl],

View File

@@ -0,0 +1,7 @@
## Cad is Liosta Rochtana ann?
Soláthraíonn Liostaí Rochtana liosta dubh nó liosta bán de sheoltaí IP cliant ar leith mar aon le fíordheimhniú do na hÓstaigh Seachfhreastalaí trí Fhíordheimhniú Bunúsach HTTP.
Is féidir leat rialacha cliant, ainmneacha úsáideora agus pasfhocail iolracha a chumrú le haghaidh Liosta Rochtana aonair agus ansin iad sin a chur i bhfeidhm ar _Óstach Seachfhreastalaí_ amháin nó níos mó.
Tá sé seo an-úsáideach i gcás seirbhísí gréasáin atreoraithe nach bhfuil meicníochtaí fíordheimhnithe ionsuite iontu nó nuair is mian leat cosaint a dhéanamh ar chliaint anaithnide.

View File

@@ -0,0 +1,21 @@
## Cabhair le Deimhnithe
### Teastas HTTP
Ciallaíonn deimhniú bailíochtaithe HTTP go ndéanfaidh freastalaithe Let's Encrypt iarracht teacht ar do fhearainn thar HTTP (ní HTTPS!) agus má éiríonn leo, eiseoidh siad do theastas.
Chun an modh seo a dhéanamh, beidh ort _Óstach Proxy_ a chruthú do do fhearainn(eanna) atá inrochtana le HTTP agus ag pointeáil chuig an suiteáil Nginx seo. Tar éis deimhniú a thabhairt, is féidir leat an _Óstach Proxy_ a mhodhnú chun an deimhniú seo a úsáid le haghaidh naisc HTTPS freisin. Mar sin féin, beidh ort an _Óstach Proxy_ a chumrú fós le haghaidh rochtain HTTP chun go ndéanfar an deimhniú a athnuachan.
_Ní thacaíonn_ an próiseas seo le fearainn fiáine.
### Teastas DNS
Éilíonn deimhniú bailíochtaithe DNS ort breiseán Soláthraí DNS a úsáid. Úsáidfear an Soláthraí DNS seo chun taifid shealadacha a chruthú ar do fhearann agus ansin déanfaidh Let's Encrypt fiosrúchán ar na taifid sin lena chinntiú gurb tusa an t-úinéir agus má éiríonn leo, eiseoidh siad do theastas.
Ní gá duit _Óstach Proxy_ a chruthú sula n-iarrann tú an cineál seo teastais. Ní gá duit do _Óstach Proxy_ a chumrú le haghaidh rochtana HTTP ach an oiread.
_Tacaíonn_ an próiseas seo le fearainn fiáine.
### Teastas Saincheaptha
Úsáid an rogha seo chun do Theastas SSL féin a uaslódáil, mar a sholáthraíonn d'Údarás Deimhnithe féin é.

View File

@@ -0,0 +1,7 @@
## Cad is Óstach 404 ann?
Is socrú óstach a thaispeánann leathanach 404 é Óstach 404.
Is féidir leis seo a bheith úsáideach nuair a bhíonn do fhearann liostaithe in innill chuardaigh agus más mian leat leathanach earráide níos deise a sholáthar nó a chur in iúl do na hinnéacsóirí cuardaigh go sonrach nach bhfuil na leathanaigh fearainn ann a thuilleadh.
Buntáiste eile a bhaineann leis an óstach seo a bheith agat ná go bhfeictear na logaí le haghaidh amas agus go bhfeictear na tagairtí.

View File

@@ -0,0 +1,7 @@
## Cad is Óstach Seachfhreastalaí ann?
Is é Óstach Seachfhreastalaí an críochphointe isteach do sheirbhís ghréasáin ar mhaith leat a atreorú.
Soláthraíonn sé foirceannadh SSL roghnach do do sheirbhís nach bhfuil tacaíocht SSL ionsuite inti b'fhéidir.
Is iad Óstaigh Seachfhreastalaí an úsáid is coitianta a bhaintear as Bainisteoir Seachfhreastalaí Nginx.

View File

@@ -0,0 +1,5 @@
## Cad is Óstach Athsheolta ann?
Déanfaidh Óstach Athsheolta iarratais a atreorú ón bhfearann ag teacht isteach agus an breathnóir a bhrú chuig fearann eile.
Is é an chúis is coitianta le húsáid a bhaint as an gcineál seo óstála ná nuair a athraíonn do shuíomh Gréasáin fearainn ach go bhfuil naisc innill chuardaigh nó atreoraithe agat fós ag tagairt don seanfhearann.

View File

@@ -0,0 +1,5 @@
## Cad is Sruth ann?
Gné réasúnta nua do Nginx is ea Sruth a sheolfaidh trácht TCP/UDP go díreach chuig ríomhaire eile ar an líonra.
Más freastalaithe cluichí, freastalaithe FTP nó SSH atá á rith agat, dfhéadfadh sé seo a bheith úsáideach.

View File

@@ -0,0 +1,6 @@
export * as AccessLists from "./AccessLists.md";
export * as Certificates from "./Certificates.md";
export * as DeadHosts from "./DeadHosts.md";
export * as ProxyHosts from "./ProxyHosts.md";
export * as RedirectionHosts from "./RedirectionHosts.md";
export * as Streams from "./Streams.md";

View File

@@ -1,5 +1,7 @@
import * as de from "./de/index";
import * as en from "./en/index";
import * as ga from './ga/index'
import * as id from "./id/index";
import * as it from "./it/index";
import * as ja from "./ja/index";
import * as nl from "./nl/index";
@@ -10,9 +12,8 @@ import * as vi from "./vi/index";
import * as zh from "./zh/index";
import * as ko from "./ko/index";
import * as bg from "./bg/index";
import * as id from "./id/index";
const items: any = { en, de, ja, sk, zh, pl, ru, it, vi, nl, bg, ko, id };
const items: any = { en, de, ja, sk, zh, pl, ru, it, vi, nl, bg, ko, ga, id }
const fallbackLang = "en";

View File

@@ -0,0 +1,683 @@
{
"access-list": {
"defaultMessage": "Liosta Rochtana"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Rial} other {Rialacha}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {Úsáideoir} other {Úsáideoirí}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Nuair a bhíonn riail amháin ar a laghad ann, cuirfear an riail seo chun gach rud a dhiúltú leis an gceann deireanach."
},
"access-list.help.rules-order": {
"defaultMessage": "Tabhair faoi deara go gcuirfear na treoracha ceadaigh agus diúltaigh i bhfeidhm san ord a shainmhínítear iad."
},
"access-list.pass-auth": {
"defaultMessage": "Tabhair Údarú chuig an Sruth Uachtarach"
},
"access-list.public": {
"defaultMessage": "Inrochtana don Phobal"
},
"access-list.public.subtitle": {
"defaultMessage": "Níl aon údarú bunúsach ag teastáil"
},
"access-list.rule-source.placeholder": {
"defaultMessage": "192.168.1.100 nó 192.168.1.0/24 nó 2001:0db8::/32"
},
"access-list.satisfy-any": {
"defaultMessage": "Sásaigh Aon"
},
"access-list.subtitle": {
"defaultMessage": "{users} {users, plural, one {Úsáideoir} other {Úsáideoirí}}, {rules} {rules, plural, one {Riail} other {Rialacha}} - Cruthaithe: {date}"
},
"access-lists": {
"defaultMessage": "Liostaí Rochtana"
},
"action.add": {
"defaultMessage": "Cuir leis"
},
"action.add-location": {
"defaultMessage": "Cuir Suíomh leis"
},
"action.allow": {
"defaultMessage": "Ceadaigh"
},
"action.close": {
"defaultMessage": "Dún"
},
"action.delete": {
"defaultMessage": "Scrios"
},
"action.deny": {
"defaultMessage": "Diúltaigh"
},
"action.disable": {
"defaultMessage": "Díchumasaigh"
},
"action.download": {
"defaultMessage": "Íoslódáil"
},
"action.edit": {
"defaultMessage": "Cuir in Eagar"
},
"action.enable": {
"defaultMessage": "Cumasaigh"
},
"action.permissions": {
"defaultMessage": "Ceadanna"
},
"action.renew": {
"defaultMessage": "Athnuachan"
},
"action.view-details": {
"defaultMessage": "Féach Sonraí"
},
"auditlogs": {
"defaultMessage": "Logaí Iniúchta"
},
"auto": {
"defaultMessage": "Uath"
},
"cancel": {
"defaultMessage": "Cealaigh"
},
"certificate": {
"defaultMessage": "Teastas"
},
"certificate.custom-certificate": {
"defaultMessage": "Teastas"
},
"certificate.custom-certificate-key": {
"defaultMessage": "Eochair Teastais"
},
"certificate.custom-intermediate": {
"defaultMessage": "Teastas Idirmheánach"
},
"certificate.in-use": {
"defaultMessage": "In Úsáid"
},
"certificate.none.subtitle": {
"defaultMessage": "Níor sannadh aon deimhniú"
},
"certificate.none.subtitle.for-http": {
"defaultMessage": "Ní úsáidfidh an t-óstach seo HTTPS"
},
"certificate.none.title": {
"defaultMessage": "Dada"
},
"certificate.not-in-use": {
"defaultMessage": "Níor Úsáideadh"
},
"certificate.renew": {
"defaultMessage": "Athnuachan an Teastais"
},
"certificates": {
"defaultMessage": "Teastais"
},
"certificates.custom": {
"defaultMessage": "Teastas Saincheaptha"
},
"certificates.custom.warning": {
"defaultMessage": "Ní thacaítear le comhaid eochair atá cosanta le frása faire."
},
"certificates.dns.credentials": {
"defaultMessage": "Ábhar Comhaid Dintiúir"
},
"certificates.dns.credentials-note": {
"defaultMessage": "Éilíonn an breiseán seo comhad cumraíochta ina bhfuil comhartha API nó dintiúir eile do do sholáthraí."
},
"certificates.dns.credentials-warning": {
"defaultMessage": "Stórálfar an fhaisnéis seo mar théacs simplí sa bhunachar sonraí agus i gcomhad!"
},
"certificates.dns.propagation-seconds": {
"defaultMessage": "Soicindí Iolraithe"
},
"certificates.dns.propagation-seconds-note": {
"defaultMessage": "Fág folamh chun luach réamhshocraithe na mbreiseán a úsáid. Líon na soicindí le fanacht le haghaidh iomadú DNS."
},
"certificates.dns.provider": {
"defaultMessage": "Soláthraí DNS"
},
"certificates.dns.provider.placeholder": {
"defaultMessage": "Roghnaigh Soláthraí..."
},
"certificates.dns.warning": {
"defaultMessage": "Éilíonn an chuid seo roinnt eolais faoi Certbot agus a bhreiseáin DNS. Féach ar dhoiciméadacht na mbreiseán faoi seach, le do thoil."
},
"certificates.http.reachability-404": {
"defaultMessage": "Tá freastalaí aimsithe ag an bhfearann seo ach ní cosúil gur Bainisteoir Proxy Nginx atá ann. Déan cinnte go bhfuil do fhearann ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith."
},
"certificates.http.reachability-failed-to-check": {
"defaultMessage": "Theip ar sheiceáil an inrochtaineachta mar gheall ar earráid chumarsáide le site24x7.com."
},
"certificates.http.reachability-not-resolved": {
"defaultMessage": "Níl aon fhreastalaí ar fáil ag an bhfearann seo. Cinntigh le do thoil go bhfuil do fhearann ann agus go bhfuil sé ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith agus más gá, go bhfuil port 80 curtha ar aghaidh i do ródaire."
},
"certificates.http.reachability-ok": {
"defaultMessage": "Tá rochtain ar do fhreastalaí agus ba cheart go mbeadh sé indéanta deimhnithe a chruthú."
},
"certificates.http.reachability-other": {
"defaultMessage": "Tá freastalaí aimsithe ag an bhfearann seo ach thug sé cód stádais gan choinne {code} ar ais. An é an freastalaí NPM atá ann? Déan cinnte go bhfuil do fhearann ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith."
},
"certificates.http.reachability-wrong-data": {
"defaultMessage": "Tá freastalaí aimsithe ag an bhfearann seo ach thug sé sonraí gan choinne ar ais. An é an freastalaí NPM atá ann? Déan cinnte go bhfuil do fhearann ag pointeáil chuig an seoladh IP ina bhfuil d'eispéireas NPM ag rith."
},
"certificates.http.test-results": {
"defaultMessage": "Torthaí Tástála"
},
"certificates.http.warning": {
"defaultMessage": "Ní mór na fearainn seo a bheith cumraithe cheana féin chun pointeáil chuig an suiteáil seo."
},
"certificates.request.subtitle": {
"defaultMessage": "le Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Iarr Teastas nua"
},
"column.access": {
"defaultMessage": "Rochtain"
},
"column.authorization": {
"defaultMessage": "Údarú"
},
"column.authorizations": {
"defaultMessage": "Údaruithe"
},
"column.custom-locations": {
"defaultMessage": "Suíomhanna Saincheaptha"
},
"column.destination": {
"defaultMessage": "Ceann Scríbe"
},
"column.details": {
"defaultMessage": "Sonraí"
},
"column.email": {
"defaultMessage": "Ríomhphost"
},
"column.event": {
"defaultMessage": "Imeacht"
},
"column.expires": {
"defaultMessage": "Éagaíonn"
},
"column.http-code": {
"defaultMessage": "Cód HTTP"
},
"column.incoming-port": {
"defaultMessage": "Port Isteach"
},
"column.name": {
"defaultMessage": "Ainm"
},
"column.protocol": {
"defaultMessage": "Prótacal"
},
"column.provider": {
"defaultMessage": "Soláthraí"
},
"column.roles": {
"defaultMessage": "Róil"
},
"column.rules": {
"defaultMessage": "Rialacha"
},
"column.satisfy": {
"defaultMessage": "Sásamh"
},
"column.satisfy-all": {
"defaultMessage": "Gach"
},
"column.satisfy-any": {
"defaultMessage": "Aon"
},
"column.scheme": {
"defaultMessage": "Scéim"
},
"column.source": {
"defaultMessage": "Foinse"
},
"column.ssl": {
"defaultMessage": "SSL"
},
"column.status": {
"defaultMessage": "Stádas"
},
"created-on": {
"defaultMessage": "Cruthaithe: {date}"
},
"dashboard": {
"defaultMessage": "Painéal Rialaithe"
},
"dead-host": {
"defaultMessage": "Óstach 404"
},
"dead-hosts": {
"defaultMessage": "404 Óstaigh"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Óstach 404} other {Óstaigh 404}}"
},
"disabled": {
"defaultMessage": "Míchumasaithe"
},
"domain-names": {
"defaultMessage": "Ainmneacha Fearainn"
},
"domain-names.max": {
"defaultMessage": "Uasmhéid d'ainmneacha fearainn: {count}"
},
"domain-names.placeholder": {
"defaultMessage": "Tosaigh ag clóscríobh chun fearann a chur leis..."
},
"domain-names.wildcards-not-permitted": {
"defaultMessage": "Ní cheadaítear cártaí fiáine don chineál seo"
},
"domain-names.wildcards-not-supported": {
"defaultMessage": "Ní thacaítear le cártaí fiáine don ÚD seo"
},
"domains.force-ssl": {
"defaultMessage": "Fórsáil SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "Cumasaithe HSTS"
},
"domains.hsts-subdomains": {
"defaultMessage": "Fo-fhearainn HSTS"
},
"domains.http2-support": {
"defaultMessage": "Tacaíocht HTTP/2"
},
"domains.use-dns": {
"defaultMessage": "Úsáid Dúshlán DNS"
},
"email-address": {
"defaultMessage": "Seoladh ríomhphoist"
},
"empty-search": {
"defaultMessage": "Níor aimsíodh aon torthaí"
},
"empty-subtitle": {
"defaultMessage": "Cén fáth nach gcruthaíonn tú ceann?"
},
"enabled": {
"defaultMessage": "Cumasaithe"
},
"error.access.at-least-one": {
"defaultMessage": "Tá Údarú amháin nó Riail Rochtana amháin ag teastáil"
},
"error.access.duplicate-usernames": {
"defaultMessage": "Ní mór dainmneacha úsáideora údaraithe a bheith uathúil"
},
"error.invalid-auth": {
"defaultMessage": "Ríomhphost nó pasfhocal neamhbhailí"
},
"error.invalid-domain": {
"defaultMessage": "Fearann neamhbhailí: {domain}"
},
"error.invalid-email": {
"defaultMessage": "Seoladh ríomhphoist neamhbhailí"
},
"error.max-character-length": {
"defaultMessage": "Is é an fad uasta ná {max} carachtar{max, plural, one {} other {anna}}"
},
"error.max-domains": {
"defaultMessage": "An iomarca fearainn, is é {max} an t-uasmhéid"
},
"error.maximum": {
"defaultMessage": "Is é {max} an t-uasmhéid"
},
"error.min-character-length": {
"defaultMessage": "Is é an fad íosta ná {min} carachtar{min, plural, one {} other {anna}}"
},
"error.minimum": {
"defaultMessage": "Is é {min} an t-íosmhéid"
},
"error.passwords-must-match": {
"defaultMessage": "Ní mór pasfhocail a bheith mar a chéile"
},
"error.required": {
"defaultMessage": "Tá sé seo riachtanach"
},
"expires.on": {
"defaultMessage": "Éagaíonn: {date}"
},
"footer.github-fork": {
"defaultMessage": "Forc mé ar Github"
},
"host.flags.block-exploits": {
"defaultMessage": "Blocáil Easnaimh Choitianta"
},
"host.flags.cache-assets": {
"defaultMessage": "Sócmhainní Taisce"
},
"host.flags.preserve-path": {
"defaultMessage": "Cosán a Chaomhnú"
},
"host.flags.protocols": {
"defaultMessage": "Prótacail"
},
"host.flags.websockets-upgrade": {
"defaultMessage": "Tacaíocht Websockets"
},
"host.forward-port": {
"defaultMessage": "Port Ar Aghaidh"
},
"host.forward-scheme": {
"defaultMessage": "Scéim"
},
"hosts": {
"defaultMessage": "Óstaigh"
},
"http-only": {
"defaultMessage": "HTTP Amháin"
},
"lets-encrypt": {
"defaultMessage": "Let's Encrypt"
},
"lets-encrypt-via-dns": {
"defaultMessage": "Let's Encrypt trí DNS"
},
"lets-encrypt-via-http": {
"defaultMessage": "Let's Encrypt trí HTTP"
},
"loading": {
"defaultMessage": "Ag lódáil…"
},
"login.title": {
"defaultMessage": "Logáil isteach i do chuntas"
},
"nginx-config.label": {
"defaultMessage": "Cumraíocht Nginx Saincheaptha"
},
"nginx-config.placeholder": {
"defaultMessage": "# Cuir isteach do chumraíocht saincheaptha Nginx anseo ar do phriacal féin!"
},
"no-permission-error": {
"defaultMessage": "Níl rochtain agat chun seo a fheiceáil."
},
"notfound.action": {
"defaultMessage": "Tabhair abhaile mé"
},
"notfound.content": {
"defaultMessage": "Tá brón orainn ach níor aimsíodh an leathanach atá á lorg agat"
},
"notfound.title": {
"defaultMessage": "Úps… Fuair tú leathanach earráide díreach anois."
},
"notification.error": {
"defaultMessage": "Earráid"
},
"notification.object-deleted": {
"defaultMessage": "Scriosadh {object}"
},
"notification.object-disabled": {
"defaultMessage": "Tá {object} díchumasaithe"
},
"notification.object-enabled": {
"defaultMessage": "Tá {object} cumasaithe"
},
"notification.object-renewed": {
"defaultMessage": "Tá {object} athnuaite"
},
"notification.object-saved": {
"defaultMessage": "Tá {object} sábháilte"
},
"notification.success": {
"defaultMessage": "Rath"
},
"object.actions-title": {
"defaultMessage": "{object} #{id}"
},
"object.add": {
"defaultMessage": "Cuir {object} leis"
},
"object.delete": {
"defaultMessage": "Scrios {object}"
},
"object.delete.content": {
"defaultMessage": "An bhfuil tú cinnte gur mian leat an {object} seo a scriosadh?"
},
"object.edit": {
"defaultMessage": "Cuir in eagar {object}"
},
"object.empty": {
"defaultMessage": "Níl aon {objects} ann"
},
"object.event.created": {
"defaultMessage": "Cruthaithe {object}"
},
"object.event.deleted": {
"defaultMessage": "Scriosadh {object}"
},
"object.event.disabled": {
"defaultMessage": "Díchumasaithe {object}"
},
"object.event.enabled": {
"defaultMessage": "Cumasaithe {object}"
},
"object.event.renewed": {
"defaultMessage": "Athnuaite {object}"
},
"object.event.updated": {
"defaultMessage": "Nuashonraithe {object}"
},
"offline": {
"defaultMessage": "As líne"
},
"online": {
"defaultMessage": "Ar líne"
},
"options": {
"defaultMessage": "Roghanna"
},
"password": {
"defaultMessage": "Pasfhocal"
},
"password.generate": {
"defaultMessage": "Gin pasfhocal randamach"
},
"password.hide": {
"defaultMessage": "Folaigh Pasfhocal"
},
"password.show": {
"defaultMessage": "Taispeáin Pasfhocal"
},
"permissions.hidden": {
"defaultMessage": "I bhfolach"
},
"permissions.manage": {
"defaultMessage": "Bainistigh"
},
"permissions.view": {
"defaultMessage": "Amharc Amháin"
},
"permissions.visibility.all": {
"defaultMessage": "Gach Míreanna"
},
"permissions.visibility.title": {
"defaultMessage": "Infheictheacht Míre"
},
"permissions.visibility.user": {
"defaultMessage": "Míreanna Cruthaithe Amháin"
},
"proxy-host": {
"defaultMessage": "Óstach Seachfhreastalaí"
},
"proxy-host.forward-host": {
"defaultMessage": "Ainm Óstach / IP Ar Aghaidh"
},
"proxy-hosts": {
"defaultMessage": "Óstaigh Seachfhreastalaí"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Óstach Seachfhreastalaí} other {Óstaigh Seachfhreastalaí}}"
},
"public": {
"defaultMessage": "Poiblí"
},
"redirection-host": {
"defaultMessage": "Óstach Athsheolta"
},
"redirection-host.forward-domain": {
"defaultMessage": "Fearann Ar Aghaidh"
},
"redirection-host.forward-http-code": {
"defaultMessage": "Cód HTTP"
},
"redirection-hosts": {
"defaultMessage": "Óstaigh Athsheolta"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Athsheoladh Óstach} other {Athsheoladh Óstaigh}}"
},
"redirection-hosts.http-code.300": {
"defaultMessage": "300 Rogha Ilghnéitheach"
},
"redirection-hosts.http-code.301": {
"defaultMessage": "301 Bogtha go buan"
},
"redirection-hosts.http-code.302": {
"defaultMessage": "302 Bogtha go sealadach"
},
"redirection-hosts.http-code.303": {
"defaultMessage": "303 Féach eile"
},
"redirection-hosts.http-code.307": {
"defaultMessage": "307 Atreorú sealadach"
},
"redirection-hosts.http-code.308": {
"defaultMessage": "308 Athsheoladh buan"
},
"role.admin": {
"defaultMessage": "Riarthóir"
},
"role.standard-user": {
"defaultMessage": "Úsáideoir Caighdeánach"
},
"save": {
"defaultMessage": "Sábháil"
},
"setting": {
"defaultMessage": "Socrú"
},
"settings": {
"defaultMessage": "Socruithe"
},
"settings.default-site": {
"defaultMessage": "Suíomh Réamhshocraithe"
},
"settings.default-site.404": {
"defaultMessage": "Leathanach 404"
},
"settings.default-site.444": {
"defaultMessage": "Gan Freagra (444)"
},
"settings.default-site.congratulations": {
"defaultMessage": "Leathanach Comhghairdeas"
},
"settings.default-site.description": {
"defaultMessage": "Cad atá le taispeáint nuair a bhuaileann óstach anaithnid Nginx"
},
"settings.default-site.html": {
"defaultMessage": "HTML saincheaptha"
},
"settings.default-site.html.placeholder": {
"defaultMessage": "<!-- Cuir isteach dábhar HTML saincheaptha anseo -->"
},
"settings.default-site.redirect": {
"defaultMessage": "Atreorú"
},
"setup.preamble": {
"defaultMessage": "Tosaigh trí do chuntas riarthóra a chruthú."
},
"setup.title": {
"defaultMessage": "Fáilte!"
},
"sign-in": {
"defaultMessage": "Sínigh isteach"
},
"ssl-certificate": {
"defaultMessage": "Teastas SSL"
},
"stream": {
"defaultMessage": "Sruth"
},
"stream.forward-host": {
"defaultMessage": "Óstach Ar Aghaidh"
},
"stream.forward-host.placeholder": {
"defaultMessage": "example.com nó 10.0.0.1 nó 2001:db8:3333:4444:5555:6666:7777:8888"
},
"stream.incoming-port": {
"defaultMessage": "Port Isteach"
},
"streams": {
"defaultMessage": "Sruthanna"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Sruth} other {Sruthanna}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
},
"streams.udp": {
"defaultMessage": "UDP"
},
"test": {
"defaultMessage": "Tástáil"
},
"update-available": {
"defaultMessage": "Nuashonrú ar Fáil: {latestVersion}"
},
"user": {
"defaultMessage": "Úsáideoir"
},
"user.change-password": {
"defaultMessage": "Athraigh Pasfhocal"
},
"user.confirm-password": {
"defaultMessage": "Deimhnigh Pasfhocal"
},
"user.current-password": {
"defaultMessage": "Pasfhocal Reatha"
},
"user.edit-profile": {
"defaultMessage": "Cuir Próifíl in Eagar"
},
"user.full-name": {
"defaultMessage": "Ainm Iomlán"
},
"user.login-as": {
"defaultMessage": "Sínigh isteach mar {name}"
},
"user.logout": {
"defaultMessage": "Logáil Amach"
},
"user.new-password": {
"defaultMessage": "Pasfhocal Nua"
},
"user.nickname": {
"defaultMessage": "Leasainm"
},
"user.set-password": {
"defaultMessage": "Socraigh Pasfhocal"
},
"user.set-permissions": {
"defaultMessage": "Socraigh Ceadanna do {name}"
},
"user.switch-dark": {
"defaultMessage": "Athraigh go Mód Dorcha"
},
"user.switch-light": {
"defaultMessage": "Athraigh go mód Solais"
},
"username": {
"defaultMessage": "Ainm úsáideora"
},
"users": {
"defaultMessage": "Úsáideoirí"
}
}

View File

@@ -441,7 +441,7 @@
"defaultMessage": "Modifica {object}"
},
"object.empty": {
"defaultMessage": "Nessun {objects} presente"
"defaultMessage": "Non ci sono {objects} presenti"
},
"object.event.created": {
"defaultMessage": "{object} creato"

View File

@@ -1,44 +1,47 @@
{
"locale-en-US": {
"defaultMessage": "English"
},
"locale-es-ES": {
"defaultMessage": "Español"
},
"locale-de-DE": {
"defaultMessage": "Deutsch"
},
"locale-ja-JP": {
"defaultMessage": "日本語"
},
"locale-ru-RU": {
"defaultMessage": "Русский"
},
"locale-sk-SK": {
"defaultMessage": "Slovenčina"
},
"locale-zh-CN": {
"defaultMessage": "中文"
},
"locale-pl-PL": {
"defaultMessage": "Polski"
},
"locale-it-IT": {
"defaultMessage": "Italiano"
},
"locale-vi-VN": {
"defaultMessage": "Tiếng Việt"
},
"locale-nl-NL": {
"defaultMessage": "Nederlands"
},
"locale-ko-KR": {
"defaultMessage": "한국어"
},
"locale-bg-BG": {
"defaultMessage": "Български"
},
"locale-id-ID": {
"defaultMessage": "Bahasa Indonesia"
}
"locale-en-US": {
"defaultMessage": "English"
},
"locale-es-ES": {
"defaultMessage": "Español"
},
"locale-ie-GA": {
"defaultMessage": "Gaeilge"
},
"locale-de-DE": {
"defaultMessage": "German"
},
"locale-id-ID": {
"defaultMessage": "Bahasa Indonesia"
},
"locale-ja-JP": {
"defaultMessage": "日本語"
},
"locale-ru-RU": {
"defaultMessage": "Русский"
},
"locale-sk-SK": {
"defaultMessage": "Slovenčina"
},
"locale-zh-CN": {
"defaultMessage": "中文"
},
"locale-pl-PL": {
"defaultMessage": "Polski"
},
"locale-it-IT": {
"defaultMessage": "Italiano"
},
"locale-vi-VN": {
"defaultMessage": "Tiếng Việt"
},
"locale-nl-NL": {
"defaultMessage": "Nederlands"
},
"locale-ko-KR": {
"defaultMessage": "한국어"
},
"locale-bg-BG": {
"defaultMessage": "Български"
}
}

View File

@@ -1,12 +1,12 @@
{
"access-list": {
"defaultMessage": "Lista dostępu"
"defaultMessage": "wpis listy dostępu"
},
"access-list.access-count": {
"defaultMessage": "{count} {count, plural, one {Reguła} few {Reguły} other {Reguł}}"
},
"access-list.auth-count": {
"defaultMessage": "{count} {count, plural, one {Użytkownik} few {Użytkowników} other {Użytkowników}}"
"defaultMessage": "{count} {count, plural, one {Użytkownik} few {Użytkownicy} other {Użytkowników}}"
},
"access-list.help-rules-last": {
"defaultMessage": "Gdy istnieje co najmniej 1 reguła, ta reguła blokująca wszystko zostanie dodana na końcu"
@@ -38,12 +38,18 @@
"action.add-location": {
"defaultMessage": "Dodaj lokalizację"
},
"action.allow": {
"defaultMessage": "Zezwól"
},
"action.close": {
"defaultMessage": "Zamknij"
},
"action.delete": {
"defaultMessage": "Usuń"
},
"action.deny": {
"defaultMessage": "Odrzuć"
},
"action.disable": {
"defaultMessage": "Wyłącz"
},
@@ -66,13 +72,13 @@
"defaultMessage": "Pokaż szczegóły"
},
"auditlogs": {
"defaultMessage": "Logi audytu"
"defaultMessage": "Logi"
},
"cancel": {
"defaultMessage": "Anuluj"
},
"certificate": {
"defaultMessage": "Certyfikat"
"defaultMessage": "certyfikat"
},
"certificate.custom-certificate": {
"defaultMessage": "Certyfikat"
@@ -105,7 +111,7 @@
"defaultMessage": "Certyfikaty"
},
"certificates.custom": {
"defaultMessage": "Certyfikat własny"
"defaultMessage": "Własny certyfikat"
},
"certificates.custom.warning": {
"defaultMessage": "Pliki kluczy chronione hasłem nie są obsługiwane."
@@ -153,7 +159,7 @@
"defaultMessage": "Wyniki testu"
},
"certificates.http.warning": {
"defaultMessage": "Te domeny muszą być już skonfigurowane tak, aby wskazywały na ten serwer www"
"defaultMessage": "Te domeny muszą być już skonfigurowane tak, aby wskazywały na ten serwer"
},
"certificates.key-type": {
"defaultMessage": "Typ klucza"
@@ -171,7 +177,7 @@
"defaultMessage": "z Let's Encrypt"
},
"certificates.request.title": {
"defaultMessage": "Zamów nowy certyfikat"
"defaultMessage": "Wygeneruj nowy certyfikat"
},
"column.access": {
"defaultMessage": "Dostęp"
@@ -183,7 +189,7 @@
"defaultMessage": "Autoryzacje"
},
"column.custom-locations": {
"defaultMessage": "Własne lokalizacje"
"defaultMessage": "Własne ustawienia lokalizacji"
},
"column.destination": {
"defaultMessage": "Cel"
@@ -249,13 +255,13 @@
"defaultMessage": "Panel"
},
"dead-host": {
"defaultMessage": "Host 404"
"defaultMessage": "host 404"
},
"dead-hosts": {
"defaultMessage": "Hosty 404"
"defaultMessage": "404"
},
"dead-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host 404} few {Hosty 404} other {Hostów 404}}"
"defaultMessage": "{count} {count, plural, one {host 404} few {hosty 404} other {hostów 404}}"
},
"disabled": {
"defaultMessage": "Wyłączone"
@@ -279,7 +285,7 @@
"defaultMessage": "Wymuś SSL"
},
"domains.hsts-enabled": {
"defaultMessage": "HSTS włączone"
"defaultMessage": "Włącz HSTS "
},
"domains.hsts-subdomains": {
"defaultMessage": "HSTS dla subdomen"
@@ -348,7 +354,7 @@
"defaultMessage": "Blokuj typowe exploity"
},
"host.flags.cache-assets": {
"defaultMessage": "Buforuj zasoby"
"defaultMessage": "Buforuj zasoby statyczne (ang. cache)"
},
"host.flags.preserve-path": {
"defaultMessage": "Zachowaj ścieżkę"
@@ -360,7 +366,7 @@
"defaultMessage": "Obsługa WebSockets"
},
"host.forward-port": {
"defaultMessage": "Port przekierowania"
"defaultMessage": "Port docelowy"
},
"host.forward-scheme": {
"defaultMessage": "Schemat"
@@ -501,22 +507,22 @@
"defaultMessage": "Tylko utworzone elementy"
},
"proxy-host": {
"defaultMessage": "Host proxy"
"defaultMessage": "host proxy"
},
"proxy-host.forward-host": {
"defaultMessage": "Przekieruj na hostname / IP"
},
"proxy-hosts": {
"defaultMessage": "Hosty proxy"
"defaultMessage": "Proxy"
},
"proxy-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host proxy} few {Hosty proxy} other {Hostów proxy}}"
"defaultMessage": "{count} {count, plural, one {host proxy} few {hosty proxy} many {hostów proxy} other {hostów proxy}}"
},
"public": {
"defaultMessage": "Publiczne"
},
"redirection-host": {
"defaultMessage": "Host przekierowania"
"defaultMessage": "adres przekierowania"
},
"redirection-host.forward-domain": {
"defaultMessage": "Domena docelowa"
@@ -525,10 +531,10 @@
"defaultMessage": "Kod HTTP"
},
"redirection-hosts": {
"defaultMessage": "Hosty przekierowań"
"defaultMessage": "Przekierowania"
},
"redirection-hosts.count": {
"defaultMessage": "{count} {count, plural, one {Host przekierowania} few {Hosty przekierowań} other {Hostów przekierowań}}"
"defaultMessage": "{count} {count, plural, one {przekierowanie} few {przekierowania} many {przekierowań} other {przekierowań}}"
},
"role.admin": {
"defaultMessage": "Administrator"
@@ -582,7 +588,7 @@
"defaultMessage": "Certyfikat SSL"
},
"stream": {
"defaultMessage": "Strumień"
"defaultMessage": "strumień"
},
"stream.forward-host": {
"defaultMessage": "Host docelowy"
@@ -594,7 +600,7 @@
"defaultMessage": "Strumienie"
},
"streams.count": {
"defaultMessage": "{count} {count, plural, one {Strumień} few {Strumienie} other {Strumieni}}"
"defaultMessage": "{count} {count, plural, one {strumień} few {strumienie} many {strumieni} other {strumieni}}"
},
"streams.tcp": {
"defaultMessage": "TCP"
@@ -606,13 +612,13 @@
"defaultMessage": "Test"
},
"user": {
"defaultMessage": "Użytkownik"
"defaultMessage": "użytkownik"
},
"user.change-password": {
"defaultMessage": "Zmień hasło"
},
"user.confirm-password": {
"defaultMessage": "Potwierdź hasło"
"defaultMessage": "Potwierdź nowe hasło"
},
"user.current-password": {
"defaultMessage": "Aktualne hasło"
@@ -621,7 +627,7 @@
"defaultMessage": "Edytuj profil"
},
"user.full-name": {
"defaultMessage": "Pełne imię i nazwisko"
"defaultMessage": "Imię / Nazwisko"
},
"user.login-as": {
"defaultMessage": "Zaloguj jako {name}"