Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0a963bfaf1 | ||
|
|
472c93d548 | ||
|
|
93aacca9b4 | ||
|
|
3505669135 | ||
|
|
fe735f9704 | ||
|
|
3563715f57 | ||
|
|
14c2185e6f | ||
|
|
27ee9fb6cb |
13
CHANGELOG.md
13
CHANGELOG.md
@@ -1,3 +1,16 @@
|
||||
## [0.29.0](https://github.com/stonith404/pingvin-share/compare/v0.28.0...v0.29.0) (2024-07-30)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* add more options to reverse shares ([#495](https://github.com/stonith404/pingvin-share/issues/495)) ([fe735f9](https://github.com/stonith404/pingvin-share/commit/fe735f9704c9d96398f3127a559e17848b08d140)), closes [#155](https://github.com/stonith404/pingvin-share/issues/155)
|
||||
* sort share files by name by default ([27ee9fb](https://github.com/stonith404/pingvin-share/commit/27ee9fb6cb98177661bed20a0baa399b27e70b7e))
|
||||
|
||||
|
||||
### Reverts
|
||||
|
||||
* Revert "fix: set max age of access token cookie to 15 minutes" ([14c2185](https://github.com/stonith404/pingvin-share/commit/14c2185e6f1a81d63e25fbeec3e30a54cf6a44c5))
|
||||
|
||||
## [0.28.0](https://github.com/stonith404/pingvin-share/compare/v0.27.0...v0.28.0) (2024-07-22)
|
||||
|
||||
|
||||
|
||||
@@ -57,4 +57,7 @@ EXPOSE 3000
|
||||
HEALTHCHECK --interval=10s --timeout=3s CMD curl -f http://localhost:3000/api/health || exit 1
|
||||
|
||||
# Application startup updated for Caddy
|
||||
CMD cp -rn /tmp/img/* /opt/app/frontend/public/img && caddy run --config /etc/caddy/Caddyfile & PORT=3333 HOSTNAME=0.0.0.0 node frontend/server.js & cd backend && npm run prod
|
||||
CMD cp -rn /tmp/img/* /opt/app/frontend/public/img && \
|
||||
caddy run --config /etc/caddy/Caddyfile 2> caddy.log & \
|
||||
PORT=3333 HOSTNAME=0.0.0.0 node frontend/server.js & \
|
||||
cd backend && npm run prod
|
||||
4
backend/package-lock.json
generated
4
backend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pingvin-share-backend",
|
||||
"version": "0.28.0",
|
||||
"version": "0.29.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pingvin-share-backend",
|
||||
"version": "0.28.0",
|
||||
"version": "0.29.0",
|
||||
"dependencies": {
|
||||
"@nestjs/cache-manager": "^2.2.2",
|
||||
"@nestjs/common": "^10.3.9",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pingvin-share-backend",
|
||||
"version": "0.28.0",
|
||||
"version": "0.29.0",
|
||||
"scripts": {
|
||||
"build": "nest build",
|
||||
"dev": "cross-env NODE_ENV=development nest start --watch",
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
-- RedefineTables
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_ReverseShare" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"token" TEXT NOT NULL,
|
||||
"shareExpiration" DATETIME NOT NULL,
|
||||
"maxShareSize" TEXT NOT NULL,
|
||||
"sendEmailNotification" BOOLEAN NOT NULL,
|
||||
"remainingUses" INTEGER NOT NULL,
|
||||
"simplified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"creatorId" TEXT NOT NULL,
|
||||
CONSTRAINT "ReverseShare_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_ReverseShare" ("createdAt", "creatorId", "id", "maxShareSize", "remainingUses", "sendEmailNotification", "shareExpiration", "token") SELECT "createdAt", "creatorId", "id", "maxShareSize", "remainingUses", "sendEmailNotification", "shareExpiration", "token" FROM "ReverseShare";
|
||||
DROP TABLE "ReverseShare";
|
||||
ALTER TABLE "new_ReverseShare" RENAME TO "ReverseShare";
|
||||
CREATE UNIQUE INDEX "ReverseShare_token_key" ON "ReverseShare"("token");
|
||||
PRAGMA foreign_key_check;
|
||||
PRAGMA foreign_keys=ON;
|
||||
@@ -0,0 +1,22 @@
|
||||
-- RedefineTables
|
||||
PRAGMA defer_foreign_keys=ON;
|
||||
PRAGMA foreign_keys=OFF;
|
||||
CREATE TABLE "new_ReverseShare" (
|
||||
"id" TEXT NOT NULL PRIMARY KEY,
|
||||
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"token" TEXT NOT NULL,
|
||||
"shareExpiration" DATETIME NOT NULL,
|
||||
"maxShareSize" TEXT NOT NULL,
|
||||
"sendEmailNotification" BOOLEAN NOT NULL,
|
||||
"remainingUses" INTEGER NOT NULL,
|
||||
"simplified" BOOLEAN NOT NULL DEFAULT false,
|
||||
"publicAccess" BOOLEAN NOT NULL DEFAULT true,
|
||||
"creatorId" TEXT NOT NULL,
|
||||
CONSTRAINT "ReverseShare_creatorId_fkey" FOREIGN KEY ("creatorId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
INSERT INTO "new_ReverseShare" ("createdAt", "creatorId", "id", "maxShareSize", "remainingUses", "sendEmailNotification", "shareExpiration", "simplified", "token") SELECT "createdAt", "creatorId", "id", "maxShareSize", "remainingUses", "sendEmailNotification", "shareExpiration", "simplified", "token" FROM "ReverseShare";
|
||||
DROP TABLE "ReverseShare";
|
||||
ALTER TABLE "new_ReverseShare" RENAME TO "ReverseShare";
|
||||
CREATE UNIQUE INDEX "ReverseShare_token_key" ON "ReverseShare"("token");
|
||||
PRAGMA foreign_keys=ON;
|
||||
PRAGMA defer_foreign_keys=OFF;
|
||||
@@ -103,6 +103,8 @@ model ReverseShare {
|
||||
maxShareSize String
|
||||
sendEmailNotification Boolean
|
||||
remainingUses Int
|
||||
simplified Boolean @default(false)
|
||||
publicAccess Boolean @default(true)
|
||||
|
||||
creatorId String
|
||||
creator User @relation(fields: [creatorId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@ -251,7 +251,7 @@ export class AuthService {
|
||||
if (accessToken)
|
||||
response.cookie("access_token", accessToken, {
|
||||
sameSite: "lax",
|
||||
maxAge: 1000 * 60 * 15, // 15 minutes
|
||||
maxAge: 1000 * 60 * 60 * 24 * 30 * 3, // 3 months
|
||||
});
|
||||
if (refreshToken)
|
||||
response.cookie("refresh_token", refreshToken, {
|
||||
|
||||
@@ -9,14 +9,16 @@ import * as moment from "moment";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { ShareSecurityGuard } from "src/share/guard/shareSecurity.guard";
|
||||
import { ShareService } from "src/share/share.service";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
|
||||
@Injectable()
|
||||
export class FileSecurityGuard extends ShareSecurityGuard {
|
||||
constructor(
|
||||
private _shareService: ShareService,
|
||||
private _prisma: PrismaService,
|
||||
_config: ConfigService,
|
||||
) {
|
||||
super(_shareService, _prisma);
|
||||
super(_shareService, _prisma, _config);
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
|
||||
@@ -133,9 +133,9 @@ export abstract class GenericOidcProvider implements OAuthProvider<OidcToken> {
|
||||
: idTokenData.preferred_username ||
|
||||
idTokenData.name ||
|
||||
idTokenData.nickname;
|
||||
|
||||
|
||||
let isAdmin: boolean;
|
||||
|
||||
|
||||
if (roleConfig?.path) {
|
||||
// A path to read roles from the token is configured
|
||||
let roles: string[] | null;
|
||||
@@ -146,9 +146,14 @@ export abstract class GenericOidcProvider implements OAuthProvider<OidcToken> {
|
||||
}
|
||||
if (Array.isArray(roles)) {
|
||||
// Roles are found in the token
|
||||
if (roleConfig.generalAccess && !roles.includes(roleConfig.generalAccess)) {
|
||||
if (
|
||||
roleConfig.generalAccess &&
|
||||
!roles.includes(roleConfig.generalAccess)
|
||||
) {
|
||||
// Role for general access is configured and the user does not have it
|
||||
this.logger.error(`User roles ${roles} do not include ${roleConfig.generalAccess}`);
|
||||
this.logger.error(
|
||||
`User roles ${roles} do not include ${roleConfig.generalAccess}`,
|
||||
);
|
||||
throw new ErrorPageException("user_not_allowed");
|
||||
}
|
||||
if (roleConfig.adminAccess) {
|
||||
|
||||
@@ -35,8 +35,10 @@ export class OidcProvider extends GenericOidcProvider {
|
||||
): Promise<OAuthSignInDto> {
|
||||
const claim = this.config.get("oauth.oidc-usernameClaim") || undefined;
|
||||
const rolePath = this.config.get("oauth.oidc-rolePath") || undefined;
|
||||
const roleGeneralAccess = this.config.get("oauth.oidc-roleGeneralAccess") || undefined;
|
||||
const roleAdminAccess = this.config.get("oauth.oidc-roleAdminAccess") || undefined;
|
||||
const roleGeneralAccess =
|
||||
this.config.get("oauth.oidc-roleGeneralAccess") || undefined;
|
||||
const roleAdminAccess =
|
||||
this.config.get("oauth.oidc-roleAdminAccess") || undefined;
|
||||
return super.getUserInfo(token, query, claim, {
|
||||
path: rolePath,
|
||||
generalAccess: roleGeneralAccess,
|
||||
|
||||
@@ -13,4 +13,10 @@ export class CreateReverseShareDTO {
|
||||
@Min(1)
|
||||
@Max(1000)
|
||||
maxUseCount: number;
|
||||
|
||||
@IsBoolean()
|
||||
simplified: boolean;
|
||||
|
||||
@IsBoolean()
|
||||
publicAccess: boolean;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,9 @@ export class ReverseShareDTO {
|
||||
@Expose()
|
||||
token: string;
|
||||
|
||||
@Expose()
|
||||
simplified: boolean;
|
||||
|
||||
from(partial: Partial<ReverseShareDTO>) {
|
||||
return plainToClass(ReverseShareDTO, partial, {
|
||||
excludeExtraneousValues: true,
|
||||
|
||||
@@ -49,6 +49,8 @@ export class ReverseShareService {
|
||||
remainingUses: data.maxUseCount,
|
||||
maxShareSize: data.maxShareSize,
|
||||
sendEmailNotification: data.sendEmailNotification,
|
||||
simplified: data.simplified,
|
||||
publicAccess: data.publicAccess,
|
||||
creatorId,
|
||||
},
|
||||
});
|
||||
|
||||
19
backend/src/share/dto/shareComplete.dto.ts
Normal file
19
backend/src/share/dto/shareComplete.dto.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Expose, plainToClass } from "class-transformer";
|
||||
import { ShareDTO } from "./share.dto";
|
||||
|
||||
export class CompletedShareDTO extends ShareDTO {
|
||||
@Expose()
|
||||
notifyReverseShareCreator?: boolean;
|
||||
|
||||
from(partial: Partial<CompletedShareDTO>) {
|
||||
return plainToClass(CompletedShareDTO, partial, {
|
||||
excludeExtraneousValues: true,
|
||||
});
|
||||
}
|
||||
|
||||
fromList(partial: Partial<CompletedShareDTO>[]) {
|
||||
return partial.map((part) =>
|
||||
plainToClass(CompletedShareDTO, part, { excludeExtraneousValues: true }),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
@@ -9,13 +8,19 @@ import { Request } from "express";
|
||||
import * as moment from "moment";
|
||||
import { PrismaService } from "src/prisma/prisma.service";
|
||||
import { ShareService } from "src/share/share.service";
|
||||
import { ConfigService } from "src/config/config.service";
|
||||
import { JwtGuard } from "src/auth/guard/jwt.guard";
|
||||
import { User } from "@prisma/client";
|
||||
|
||||
@Injectable()
|
||||
export class ShareSecurityGuard implements CanActivate {
|
||||
export class ShareSecurityGuard extends JwtGuard {
|
||||
constructor(
|
||||
private shareService: ShareService,
|
||||
private prisma: PrismaService,
|
||||
) {}
|
||||
configService: ConfigService,
|
||||
) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
async canActivate(context: ExecutionContext) {
|
||||
const request: Request = context.switchToHttp().getRequest();
|
||||
@@ -31,7 +36,7 @@ export class ShareSecurityGuard implements CanActivate {
|
||||
|
||||
const share = await this.prisma.share.findUnique({
|
||||
where: { id: shareId },
|
||||
include: { security: true },
|
||||
include: { security: true, reverseShare: true },
|
||||
});
|
||||
|
||||
if (
|
||||
@@ -53,6 +58,22 @@ export class ShareSecurityGuard implements CanActivate {
|
||||
"share_token_required",
|
||||
);
|
||||
|
||||
// Run the JWTGuard to set the user
|
||||
await super.canActivate(context);
|
||||
const user = request.user as User;
|
||||
|
||||
// Only the creator and reverse share creator can access the reverse share if it's not public
|
||||
if (
|
||||
share.reverseShare &&
|
||||
!share.reverseShare.publicAccess &&
|
||||
share.creatorId !== user?.id &&
|
||||
share.reverseShare.creatorId !== user?.id
|
||||
)
|
||||
throw new ForbiddenException(
|
||||
"Only reverse share creator can access this share",
|
||||
"private_share",
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ import { ShareOwnerGuard } from "./guard/shareOwner.guard";
|
||||
import { ShareSecurityGuard } from "./guard/shareSecurity.guard";
|
||||
import { ShareTokenSecurity } from "./guard/shareTokenSecurity.guard";
|
||||
import { ShareService } from "./share.service";
|
||||
import { CompletedShareDTO } from "./dto/shareComplete.dto";
|
||||
@Controller("shares")
|
||||
export class ShareController {
|
||||
constructor(
|
||||
@@ -86,7 +87,7 @@ export class ShareController {
|
||||
@UseGuards(CreateShareGuard, ShareOwnerGuard)
|
||||
async complete(@Param("id") id: string, @Req() request: Request) {
|
||||
const { reverse_share_token } = request.cookies;
|
||||
return new ShareDTO().from(
|
||||
return new CompletedShareDTO().from(
|
||||
await this.shareService.complete(id, reverse_share_token),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -159,11 +159,12 @@ export class ShareService {
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
share.reverseShare &&
|
||||
this.config.get("smtp.enabled") &&
|
||||
share.reverseShare.sendEmailNotification
|
||||
) {
|
||||
const notifyReverseShareCreator = share.reverseShare
|
||||
? this.config.get("smtp.enabled") &&
|
||||
share.reverseShare.sendEmailNotification
|
||||
: undefined;
|
||||
|
||||
if (notifyReverseShareCreator) {
|
||||
await this.emailService.sendMailToReverseShareCreator(
|
||||
share.reverseShare.creator.email,
|
||||
share.id,
|
||||
@@ -180,10 +181,15 @@ export class ShareService {
|
||||
});
|
||||
}
|
||||
|
||||
return this.prisma.share.update({
|
||||
const updatedShare = await this.prisma.share.update({
|
||||
where: { id },
|
||||
data: { uploadLocked: true },
|
||||
});
|
||||
|
||||
return {
|
||||
...updatedShare,
|
||||
notifyReverseShareCreator,
|
||||
};
|
||||
}
|
||||
|
||||
async revertComplete(id: string) {
|
||||
@@ -239,7 +245,11 @@ export class ShareService {
|
||||
const share = await this.prisma.share.findUnique({
|
||||
where: { id },
|
||||
include: {
|
||||
files: true,
|
||||
files: {
|
||||
orderBy: {
|
||||
name: "asc",
|
||||
},
|
||||
},
|
||||
creator: true,
|
||||
security: true,
|
||||
},
|
||||
|
||||
5
frontend/package-lock.json
generated
5
frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pingvin-share-frontend",
|
||||
"version": "0.28.0",
|
||||
"version": "0.29.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pingvin-share-frontend",
|
||||
"version": "0.28.0",
|
||||
"version": "0.29.0",
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/server": "^11.11.0",
|
||||
@@ -26,7 +26,6 @@
|
||||
"mime-types": "^2.1.35",
|
||||
"moment": "^2.30.1",
|
||||
"next": "^14.2.3",
|
||||
"next-cookies": "^2.0.3",
|
||||
"next-http-proxy-middleware": "^1.2.6",
|
||||
"next-pwa": "^5.6.0",
|
||||
"p-limit": "^4.0.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pingvin-share-frontend",
|
||||
"version": "0.28.0",
|
||||
"version": "0.29.0",
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
@@ -27,7 +27,6 @@
|
||||
"mime-types": "^2.1.35",
|
||||
"moment": "^2.30.1",
|
||||
"next": "^14.2.3",
|
||||
"next-cookies": "^2.0.3",
|
||||
"next-http-proxy-middleware": "^1.2.6",
|
||||
"next-pwa": "^5.6.0",
|
||||
"p-limit": "^4.0.0",
|
||||
|
||||
@@ -39,7 +39,7 @@ const FileList = ({
|
||||
const t = useTranslate();
|
||||
|
||||
const [sort, setSort] = useState<TableSort>({
|
||||
property: undefined,
|
||||
property: "name",
|
||||
direction: "desc",
|
||||
});
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { getExpirationPreview } from "../../../utils/date.util";
|
||||
import toast from "../../../utils/toast.util";
|
||||
import FileSizeInput from "../FileSizeInput";
|
||||
import showCompletedReverseShareModal from "./showCompletedReverseShareModal";
|
||||
import { getCookie, setCookie } from "cookies-next";
|
||||
|
||||
const showCreateReverseShareModal = (
|
||||
modals: ModalsContextProps,
|
||||
@@ -61,10 +62,16 @@ const Body = ({
|
||||
sendEmailNotification: false,
|
||||
expiration_num: 1,
|
||||
expiration_unit: "-days",
|
||||
simplified: !!(getCookie("reverse-share.simplified") ?? false),
|
||||
publicAccess: !!(getCookie("reverse-share.public-access") ?? true),
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = form.onSubmit(async (values) => {
|
||||
// remember simplified and publicAccess in cookies
|
||||
setCookie("reverse-share.simplified", values.simplified);
|
||||
setCookie("reverse-share.public-access", values.publicAccess);
|
||||
|
||||
const expirationDate = moment().add(
|
||||
form.values.expiration_num,
|
||||
form.values.expiration_unit.replace(
|
||||
@@ -91,6 +98,8 @@ const Body = ({
|
||||
values.maxShareSize,
|
||||
values.maxUseCount,
|
||||
values.sendEmailNotification,
|
||||
values.simplified,
|
||||
values.publicAccess,
|
||||
)
|
||||
.then(({ link }) => {
|
||||
modals.closeAll();
|
||||
@@ -210,7 +219,28 @@ const Body = ({
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
<Switch
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
label={t("account.reverseShares.modal.simplified")}
|
||||
description={t(
|
||||
"account.reverseShares.modal.simplified.description",
|
||||
)}
|
||||
{...form.getInputProps("simplified", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Switch
|
||||
mt="xs"
|
||||
labelPosition="left"
|
||||
label={t("account.reverseShares.modal.public-access")}
|
||||
description={t(
|
||||
"account.reverseShares.modal.public-access.description",
|
||||
)}
|
||||
{...form.getInputProps("publicAccess", {
|
||||
type: "checkbox",
|
||||
})}
|
||||
/>
|
||||
<Button mt="md" type="submit">
|
||||
<FormattedMessage id="common.button.create" />
|
||||
</Button>
|
||||
|
||||
@@ -7,12 +7,12 @@ import { FormattedMessage } from "react-intl";
|
||||
import useTranslate, {
|
||||
translateOutsideContext,
|
||||
} from "../../../hooks/useTranslate.hook";
|
||||
import { Share } from "../../../types/share.type";
|
||||
import { CompletedShare } from "../../../types/share.type";
|
||||
import CopyTextField from "../CopyTextField";
|
||||
|
||||
const showCompletedUploadModal = (
|
||||
modals: ModalsContextProps,
|
||||
share: Share,
|
||||
share: CompletedShare,
|
||||
appUrl: string,
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
@@ -25,7 +25,7 @@ const showCompletedUploadModal = (
|
||||
});
|
||||
};
|
||||
|
||||
const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||
const Body = ({ share, appUrl }: { share: CompletedShare; appUrl: string }) => {
|
||||
const modals = useModals();
|
||||
const router = useRouter();
|
||||
const t = useTranslate();
|
||||
@@ -35,6 +35,19 @@ const Body = ({ share, appUrl }: { share: Share; appUrl: string }) => {
|
||||
return (
|
||||
<Stack align="stretch">
|
||||
<CopyTextField link={link} />
|
||||
{share.notifyReverseShareCreator === true && (
|
||||
<Text
|
||||
size="sm"
|
||||
sx={(theme) => ({
|
||||
color:
|
||||
theme.colorScheme === "dark"
|
||||
? theme.colors.gray[3]
|
||||
: theme.colors.dark[4],
|
||||
})}
|
||||
>
|
||||
{t("upload.modal.completed.notified-reverse-share-creator")}
|
||||
</Text>
|
||||
)}
|
||||
<Text
|
||||
size="xs"
|
||||
sx={(theme) => ({
|
||||
|
||||
@@ -31,6 +31,7 @@ import { FileUpload } from "../../../types/File.type";
|
||||
import { CreateShare } from "../../../types/share.type";
|
||||
import { getExpirationPreview } from "../../../utils/date.util";
|
||||
import React from "react";
|
||||
import toast from "../../../utils/toast.util";
|
||||
|
||||
const showCreateUploadModal = (
|
||||
modals: ModalsContextProps,
|
||||
@@ -41,12 +42,26 @@ const showCreateUploadModal = (
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
maxExpirationInHours: number;
|
||||
simplified: boolean;
|
||||
},
|
||||
files: FileUpload[],
|
||||
uploadCallback: (createShare: CreateShare, files: FileUpload[]) => void,
|
||||
) => {
|
||||
const t = translateOutsideContext();
|
||||
|
||||
if (options.simplified) {
|
||||
return modals.openModal({
|
||||
title: t("upload.modal.title"),
|
||||
children: (
|
||||
<SimplifiedCreateUploadModalModal
|
||||
options={options}
|
||||
files={files}
|
||||
uploadCallback={uploadCallback}
|
||||
/>
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return modals.openModal({
|
||||
title: t("upload.modal.title"),
|
||||
children: (
|
||||
@@ -59,6 +74,23 @@ const showCreateUploadModal = (
|
||||
});
|
||||
};
|
||||
|
||||
const generateLink = () =>
|
||||
Buffer.from(Math.random().toString(), "utf8")
|
||||
.toString("base64")
|
||||
.substring(10, 17);
|
||||
|
||||
const generateAvailableLink = async (times = 10): Promise<string> => {
|
||||
if (times <= 0) {
|
||||
throw new Error("Could not generate available link");
|
||||
}
|
||||
const _link = generateLink();
|
||||
if (!(await shareService.isShareIdAvailable(_link))) {
|
||||
return await generateAvailableLink(times - 1);
|
||||
} else {
|
||||
return _link;
|
||||
}
|
||||
};
|
||||
|
||||
const CreateUploadModalBody = ({
|
||||
uploadCallback,
|
||||
files,
|
||||
@@ -78,9 +110,7 @@ const CreateUploadModalBody = ({
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const generatedLink = Buffer.from(Math.random().toString(), "utf8")
|
||||
.toString("base64")
|
||||
.substr(10, 7);
|
||||
const generatedLink = generateLink();
|
||||
|
||||
const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(true);
|
||||
|
||||
@@ -202,14 +232,7 @@ const CreateUploadModalBody = ({
|
||||
<Button
|
||||
style={{ flex: "0 0 auto" }}
|
||||
variant="outline"
|
||||
onClick={() =>
|
||||
form.setFieldValue(
|
||||
"link",
|
||||
Buffer.from(Math.random().toString(), "utf8")
|
||||
.toString("base64")
|
||||
.substr(10, 7),
|
||||
)
|
||||
}
|
||||
onClick={() => form.setFieldValue("link", generateLink())}
|
||||
>
|
||||
<FormattedMessage id="common.button.generate" />
|
||||
</Button>
|
||||
@@ -429,4 +452,108 @@ const CreateUploadModalBody = ({
|
||||
);
|
||||
};
|
||||
|
||||
const SimplifiedCreateUploadModalModal = ({
|
||||
uploadCallback,
|
||||
files,
|
||||
options,
|
||||
}: {
|
||||
files: FileUpload[];
|
||||
uploadCallback: (createShare: CreateShare, files: FileUpload[]) => void;
|
||||
options: {
|
||||
isUserSignedIn: boolean;
|
||||
isReverseShare: boolean;
|
||||
appUrl: string;
|
||||
allowUnauthenticatedShares: boolean;
|
||||
enableEmailRecepients: boolean;
|
||||
maxExpirationInHours: number;
|
||||
};
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
|
||||
const [showNotSignedInAlert, setShowNotSignedInAlert] = useState(true);
|
||||
|
||||
const validationSchema = yup.object().shape({
|
||||
name: yup
|
||||
.string()
|
||||
.transform((value) => value || undefined)
|
||||
.min(3, t("common.error.too-short", { length: 3 }))
|
||||
.max(30, t("common.error.too-long", { length: 30 })),
|
||||
});
|
||||
|
||||
const form = useForm({
|
||||
initialValues: {
|
||||
name: undefined,
|
||||
description: undefined,
|
||||
},
|
||||
validate: yupResolver(validationSchema),
|
||||
});
|
||||
|
||||
const onSubmit = form.onSubmit(async (values) => {
|
||||
const link = await generateAvailableLink().catch(() => {
|
||||
toast.error(t("upload.modal.link.error.taken"));
|
||||
return undefined;
|
||||
});
|
||||
|
||||
if (!link) {
|
||||
return;
|
||||
}
|
||||
|
||||
uploadCallback(
|
||||
{
|
||||
id: link,
|
||||
name: values.name,
|
||||
expiration: "never",
|
||||
recipients: [],
|
||||
description: values.description,
|
||||
security: {
|
||||
password: undefined,
|
||||
maxViews: undefined,
|
||||
},
|
||||
},
|
||||
files,
|
||||
);
|
||||
modals.closeAll();
|
||||
});
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{showNotSignedInAlert && !options.isUserSignedIn && (
|
||||
<Alert
|
||||
withCloseButton
|
||||
onClose={() => setShowNotSignedInAlert(false)}
|
||||
icon={<TbAlertCircle size={16} />}
|
||||
title={t("upload.modal.not-signed-in")}
|
||||
color="yellow"
|
||||
>
|
||||
<FormattedMessage id="upload.modal.not-signed-in-description" />
|
||||
</Alert>
|
||||
)}
|
||||
<form onSubmit={onSubmit}>
|
||||
<Stack align="stretch">
|
||||
<Stack align="stretch">
|
||||
<TextInput
|
||||
variant="filled"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.name-and-description.name.placeholder",
|
||||
)}
|
||||
{...form.getInputProps("name")}
|
||||
/>
|
||||
<Textarea
|
||||
variant="filled"
|
||||
placeholder={t(
|
||||
"upload.modal.accordion.name-and-description.description.placeholder",
|
||||
)}
|
||||
{...form.getInputProps("description")}
|
||||
/>
|
||||
</Stack>
|
||||
<Button type="submit" data-autofocus>
|
||||
<FormattedMessage id="common.button.share" />
|
||||
</Button>
|
||||
</Stack>
|
||||
</form>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default showCreateUploadModal;
|
||||
|
||||
@@ -199,6 +199,14 @@ export default {
|
||||
"account.reverseShares.modal.send-email.description":
|
||||
"Send an email notification when a share is created with this reverse share link.",
|
||||
|
||||
"account.reverseShares.modal.simplified": "Simple mode",
|
||||
"account.reverseShares.modal.simplified.description":
|
||||
"Make it easy for the person uploading the file to share it with you. They will be able to customize only the name and description of the share.",
|
||||
|
||||
"account.reverseShares.modal.public-access": "Public access",
|
||||
"account.reverseShares.modal.public-access.description":
|
||||
"Make the created shares with this reverse share public. If disabled, only you and the creator of the share can view it.",
|
||||
|
||||
"account.reverseShares.modal.max-use.label": "Max uses",
|
||||
"account.reverseShares.modal.max-use.description":
|
||||
"The maximum amount of times this URL can be used to create a share.",
|
||||
@@ -342,6 +350,7 @@ export default {
|
||||
"upload.modal.completed.expires-on":
|
||||
"This share will expire on {expiration}.",
|
||||
"upload.modal.completed.share-ready": "Share ready",
|
||||
"upload.modal.completed.notified-reverse-share-creator": "We have notified the creator of the reverse share. You can also manually share this link with them through other means.",
|
||||
|
||||
// END /upload
|
||||
|
||||
@@ -355,6 +364,8 @@ export default {
|
||||
"share.error.not-found.title": "Share not found",
|
||||
"share.error.not-found.description":
|
||||
"The share you're looking for doesn't exist.",
|
||||
"share.error.access-denied.title": "Private share",
|
||||
"share.error.access-denied.description": "The current account does not have permission to access this share",
|
||||
|
||||
"share.modal.password.title": "Password required",
|
||||
"share.modal.password.description":
|
||||
|
||||
@@ -345,8 +345,8 @@ export default {
|
||||
"admin.config.smtp.password": "Mot de passe",
|
||||
"admin.config.smtp.password.description": "Mot de passe du serveur SMTP",
|
||||
"admin.config.smtp.button.test": "Envoyer un courriel de test",
|
||||
"admin.config.smtp.allow-unauthorized-certificates": "Trust unauthorized SMTP server certificates",
|
||||
"admin.config.smtp.allow-unauthorized-certificates.description": "Only set this to true if you need to trust self signed certificates.",
|
||||
"admin.config.smtp.allow-unauthorized-certificates": "Faire confiance aux certificats de serveurs SMTP non autorisés",
|
||||
"admin.config.smtp.allow-unauthorized-certificates.description": "Ne permettez ceci que si vous avez besoin de faire confiance aux certificats autosignés.",
|
||||
"admin.config.oauth.allow-registration": "Autoriser l’inscription",
|
||||
"admin.config.oauth.allow-registration.description": "Permettre aux utilisateurs de s’inscrire via leur identifiant social",
|
||||
"admin.config.oauth.ignore-totp": "Ignorer TOTP",
|
||||
@@ -387,12 +387,12 @@ export default {
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "L’URI de découverte de la connexion à l'application OpenID OAuth",
|
||||
"admin.config.oauth.oidc-username-claim": "Revendication du nom d’utilisateur OpenID",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Le champ contenant la revendication du nom d’utilisateur dans le jeton OpenID Connect. Laissez vide si vous ne savez pas quoi indiquer.",
|
||||
"admin.config.oauth.oidc-role-path": "Path to roles in OpenID Connect token",
|
||||
"admin.config.oauth.oidc-role-path.description": "Must be a valid JMES path referencing an array of roles. " + "Managing access rights using OpenID Connect roles is only recommended if no other identity provider is configured and password login is disabled. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-general-access": "OpenID Connect role for general access",
|
||||
"admin.config.oauth.oidc-role-general-access.description": "Role required for general access. Must be present in a user’s roles for them to log in. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-admin-access": "OpenID Connect role for admin access",
|
||||
"admin.config.oauth.oidc-role-admin-access.description": "Role required for administrative access. Must be present in a user’s roles for them to access the admin panel. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-path": "Chemin vers les rôles dans le jeton OpenID Connect",
|
||||
"admin.config.oauth.oidc-role-path.description": "Doit être un chemin JMES valide référençant un tableau de rôles. " + "La gestion des droits d'accès en utilisant les rôles OpenID Connect n'est recommandée que si aucun autre fournisseur d'identité n'est configuré et que la connexion par mot de passe est désactivée. " + "Laissez vide si vous ne savez pas ce qu'est cette configuration.",
|
||||
"admin.config.oauth.oidc-role-general-access": "Rôle OpenID Connect pour un accès général",
|
||||
"admin.config.oauth.oidc-role-general-access.description": "Rôle requis pour un accès général. Doit être présent dans les rôles d'un utilisateur pour qu'il se connecte. " + "Laissez vide si vous ne savez pas ce qu'est cette configuration.",
|
||||
"admin.config.oauth.oidc-role-admin-access": "Rôle OpenID Connect pour l'accès admin",
|
||||
"admin.config.oauth.oidc-role-admin-access.description": "Rôle requis pour l'accès administratif. Doit être présent dans les rôles d'un utilisateur pour accéder au panneau d'administration. " + "Laissez vide si vous ne savez pas ce qu'est cette configuration.",
|
||||
"admin.config.oauth.oidc-client-id": "ID du client OpenID",
|
||||
"admin.config.oauth.oidc-client-id.description": "L’ID du client de l’application OAuth OpenID Connect",
|
||||
"admin.config.oauth.oidc-client-secret": "Secret du client OpenID",
|
||||
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
"signIn.notify.totp-required.title": "Kétfaktoros hitelesítésre van szükség",
|
||||
"signIn.notify.totp-required.description": "Adja meg a másik úton kapott kódját",
|
||||
"signIn.oauth.or": "VAGY",
|
||||
"signIn.oauth.signInWith": "Sign in with",
|
||||
"signIn.oauth.signInWith": "Bejelentkezés a következővel",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
@@ -345,14 +345,14 @@ export default {
|
||||
"admin.config.smtp.password": "Jelszó",
|
||||
"admin.config.smtp.password.description": "Jelszó az SMTP kiszolgálón",
|
||||
"admin.config.smtp.button.test": "Teszt email küldése",
|
||||
"admin.config.smtp.allow-unauthorized-certificates": "Trust unauthorized SMTP server certificates",
|
||||
"admin.config.smtp.allow-unauthorized-certificates.description": "Only set this to true if you need to trust self signed certificates.",
|
||||
"admin.config.smtp.allow-unauthorized-certificates": "A jogosulatlan SMTP kiszolgáló tanúsítványok is megbízhatók",
|
||||
"admin.config.smtp.allow-unauthorized-certificates.description": "Csak akkor engedélyezze ha saját aláírású tanúsítványok elfogadása is szükséges.",
|
||||
"admin.config.oauth.allow-registration": "Regisztráció engedélyezése",
|
||||
"admin.config.oauth.allow-registration.description": "A felhasználók közösségi bejelentkezésen át is regisztrálhatnak",
|
||||
"admin.config.oauth.ignore-totp": "TOTP mellőzése",
|
||||
"admin.config.oauth.ignore-totp.description": "TOTP mellőzése a közösségi bejelentkezést használó felhasználónál",
|
||||
"admin.config.oauth.disable-password": "Disable password login",
|
||||
"admin.config.oauth.disable-password.description": "Whether to disable password login\nMake sure that an OAuth provider is properly configured before activating this configuration to avoid being locked out.",
|
||||
"admin.config.oauth.disable-password": "Jelszavas bejelentkezés letiltása",
|
||||
"admin.config.oauth.disable-password.description": "A jelszavas bejelentkezés be- és kikapcsolása\nA letiltás előtt a kizáródás elkerülésére mindenképpen ellenőrizendő az OAuth szolgáltató megfelelő konfigurációja.",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "GitHub bejelentkezés engedélyezése",
|
||||
"admin.config.oauth.github-client-id": "GitHub ügyfél ID",
|
||||
@@ -387,12 +387,12 @@ export default {
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Az OpenID Connect OAuth applikáció Discovery URI azonosítója",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect felhasználónév igény",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Az OpenID Connect ID token felhasználónév igénye. Hagyja üresen ha nincs információja a beállításról.",
|
||||
"admin.config.oauth.oidc-role-path": "Path to roles in OpenID Connect token",
|
||||
"admin.config.oauth.oidc-role-path.description": "Must be a valid JMES path referencing an array of roles. " + "Managing access rights using OpenID Connect roles is only recommended if no other identity provider is configured and password login is disabled. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-general-access": "OpenID Connect role for general access",
|
||||
"admin.config.oauth.oidc-role-general-access.description": "Role required for general access. Must be present in a user’s roles for them to log in. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-admin-access": "OpenID Connect role for admin access",
|
||||
"admin.config.oauth.oidc-role-admin-access.description": "Role required for administrative access. Must be present in a user’s roles for them to access the admin panel. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-path": "Az OpenID Connect token szerepeinek elérési útvonala",
|
||||
"admin.config.oauth.oidc-role-path.description": "Szerepkörökből álló tömbre hivatkozó érvényes JMES-útvonalnak kell lennie. " + "A belépési jogosultságok kezelésére az OpenID Connect szerepkörök csak más azonosító szolgáltatások hiányában és a jelszavas bejelentkezés letiltottsága mellett javasolt. " + "Hagyja üresen, ha nem tudja, mi ez a konfiguráció.",
|
||||
"admin.config.oauth.oidc-role-general-access": "OpenID Connect szerepkör általános hozzáféréshez",
|
||||
"admin.config.oauth.oidc-role-general-access.description": "Role required for general access. Must be present in a user’s roles for them to log in. " + "Hagyja üresen, ha nem tudja, mi ez a konfiguráció.",
|
||||
"admin.config.oauth.oidc-role-admin-access": "OpenID Connect szerepkör admin hozzáféréshez",
|
||||
"admin.config.oauth.oidc-role-admin-access.description": "A rendszergazdai hozzáféréshez szükséges szerepkör. Meg kell lennie a felhasználó szerepkörében ahhoz, hogy hozzáférhessen az adminisztrációs panelhez. " + "Hagyja üresen, ha nem tudja, mi ez a konfiguráció.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect ügyfél ID azonosító",
|
||||
"admin.config.oauth.oidc-client-id.description": "Az OpenID Connect OAuth applikáció ügyfél ID azonosítója",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect ügyfél titok",
|
||||
|
||||
@@ -34,7 +34,7 @@ export default {
|
||||
"signIn.notify.totp-required.title": "二段階認証が必要です",
|
||||
"signIn.notify.totp-required.description": "二段階認証コードを入力してください",
|
||||
"signIn.oauth.or": "または",
|
||||
"signIn.oauth.signInWith": "Sign in with",
|
||||
"signIn.oauth.signInWith": "サインインの方法",
|
||||
"signIn.oauth.github": "GitHub",
|
||||
"signIn.oauth.google": "Google",
|
||||
"signIn.oauth.microsoft": "Microsoft",
|
||||
@@ -58,7 +58,7 @@ export default {
|
||||
// /auth/reset-password
|
||||
"resetPassword.title": "パスワードを忘れてしまいましたか?",
|
||||
"resetPassword.description": "登録しているメールアドレスを入力してください。",
|
||||
"resetPassword.notify.success": "A message with a link to reset your password has been sent if the email exists.",
|
||||
"resetPassword.notify.success": "電子メールが存在する場合、パスワードをリセットするためのリンクを含むメッセージが送信されました。",
|
||||
"resetPassword.button.back": "サインインページに戻る",
|
||||
"resetPassword.text.resetPassword": "パスワードをリセット",
|
||||
"resetPassword.text.enterNewPassword": "新規パスワードを入力",
|
||||
@@ -170,7 +170,7 @@ export default {
|
||||
// /admin
|
||||
"admin.title": "管理画面",
|
||||
"admin.button.users": "ユーザー管理",
|
||||
"admin.button.shares": "Share management",
|
||||
"admin.button.shares": "共有管理",
|
||||
"admin.button.config": "設定",
|
||||
"admin.version": "バージョン",
|
||||
// END /admin
|
||||
@@ -198,13 +198,13 @@ export default {
|
||||
"admin.users.modal.create.admin.description": "チェックされている場合、ユーザーは管理画面にアクセスできるようになります。",
|
||||
// END /admin/users
|
||||
// /admin/shares
|
||||
"admin.shares.title": "Share management",
|
||||
"admin.shares.table.id": "Share ID",
|
||||
"admin.shares.table.username": "Creator",
|
||||
"admin.shares.table.visitors": "Visitors",
|
||||
"admin.shares.table.expires": "Expires At",
|
||||
"admin.shares.edit.delete.title": "Delete share {id}",
|
||||
"admin.shares.edit.delete.description": "Do you really want to delete this share?",
|
||||
"admin.shares.title": "共有管理",
|
||||
"admin.shares.table.id": "共有ID",
|
||||
"admin.shares.table.username": "作成者",
|
||||
"admin.shares.table.visitors": "訪問者",
|
||||
"admin.shares.table.expires": "有効期限",
|
||||
"admin.shares.edit.delete.title": "共有 {id} を削除",
|
||||
"admin.shares.edit.delete.description": "この共有を削除してもよろしいですか?",
|
||||
// END /admin/shares
|
||||
// /upload
|
||||
"upload.title": "アップロード",
|
||||
@@ -240,9 +240,9 @@ export default {
|
||||
"upload.modal.expires.month-plural": "ヶ月間",
|
||||
"upload.modal.expires.year-singular": "年間",
|
||||
"upload.modal.expires.year-plural": "年間",
|
||||
"upload.modal.accordion.name-and-description.title": "Name and description",
|
||||
"upload.modal.accordion.name-and-description.name.placeholder": "Name",
|
||||
"upload.modal.accordion.name-and-description.description.placeholder": "Note for the recipients of this share",
|
||||
"upload.modal.accordion.name-and-description.title": "名前と説明",
|
||||
"upload.modal.accordion.name-and-description.name.placeholder": "名前",
|
||||
"upload.modal.accordion.name-and-description.description.placeholder": "この共有に関する受信者へのメモ",
|
||||
"upload.modal.accordion.email.title": "メールで受け取る相手",
|
||||
"upload.modal.accordion.email.placeholder": "メールの宛先を入力",
|
||||
"upload.modal.accordion.email.invalid-email": "無効なメールアドレスです",
|
||||
@@ -274,7 +274,7 @@ export default {
|
||||
"share.table.name": "ファイル名",
|
||||
"share.table.size": "サイズ",
|
||||
"share.modal.file-preview.error.not-supported.title": "プレビューに対応していません",
|
||||
"share.modal.file-preview.error.not-supported.description": "A preview for this file type is unsupported. Please download the file to view it.",
|
||||
"share.modal.file-preview.error.not-supported.description": "このファイルタイプのプレビューはサポートされていません。ファイルをダウンロードして表示してください。",
|
||||
// END /share/[id]
|
||||
// /share/[id]/edit
|
||||
"share.edit.title": "編集 {shareId}",
|
||||
@@ -295,8 +295,8 @@ export default {
|
||||
"admin.config.general.app-url.description": "Pingvin Shareで利用できるURL",
|
||||
"admin.config.general.show-home-page": "ホームページを表示する",
|
||||
"admin.config.general.show-home-page.description": "ホームページを表示するかどうか選択",
|
||||
"admin.config.general.session-duration": "Session Duration",
|
||||
"admin.config.general.session-duration.description": "Time in hours after which a user must log in again (default: 3 months).",
|
||||
"admin.config.general.session-duration": "セッション期間",
|
||||
"admin.config.general.session-duration.description": "ユーザーが再度ログインする必要がある時間(時間単位)(デフォルト: 3 か月)。",
|
||||
"admin.config.general.logo": "ロゴ",
|
||||
"admin.config.general.logo.description": "新しい画像をアップロードしてロゴを変更できます。画像は、PNG形式でアスペクト比が1:1である必要があります。",
|
||||
"admin.config.general.logo.placeholder": "画像を選択",
|
||||
@@ -328,10 +328,10 @@ export default {
|
||||
"admin.config.share.max-size.description": "最大ファイルサイズ(byte単位)",
|
||||
"admin.config.share.zip-compression-level": "Zip圧縮レベル",
|
||||
"admin.config.share.zip-compression-level.description": "ファイルサイズと圧縮速度のバランスを取るように、レベルを調整できます。有効な値は0~9の間で、0が無圧縮、9で最大限の圧縮となります。 ",
|
||||
"admin.config.share.chunk-size": "Chunk size",
|
||||
"admin.config.share.chunk-size.description": "Adjust the chunk size (in bytes) for your uploads to balance efficiency and reliability according to your internet connection. Smaller chunks can enhance success rates for unstable connections, while larger chunks speed up uploads for stable connections.",
|
||||
"admin.config.share.auto-open-share-modal": "Auto open create share modal",
|
||||
"admin.config.share.auto-open-share-modal.description": "The share creation modal automatically appears when a user selects files, eliminating the need to manually click the button.",
|
||||
"admin.config.share.chunk-size": "チャンクサイズ",
|
||||
"admin.config.share.chunk-size.description": "インターネット接続に応じてアップロードのチャンクサイズ(バイト単位)を調整し、効率と信頼性のバランスを取ってください。不安定な接続の場合、チャンクサイズを小さくすることでアップロード成功率を高めることができ、安定した接続の場合、チャンクサイズを大きくすることでアップロード速度を速めることができます。",
|
||||
"admin.config.share.auto-open-share-modal": "共有モーダルを自動的に開く",
|
||||
"admin.config.share.auto-open-share-modal.description": "ユーザーがファイルを選択すると、共有作成モーダルが自動的に表示されるため、手動でボタンをクリックする必要がありません。",
|
||||
"admin.config.smtp.enabled": "有効",
|
||||
"admin.config.smtp.enabled.description": "SMTPを有効にするかどうかを選択してください。SMTPサーバーのホスト名、ポート番号、電子メールアドレス、ユーザー名、パスワードが入力されている場合にのみ、有効にしてください。",
|
||||
"admin.config.smtp.host": "ホスト名",
|
||||
@@ -345,14 +345,14 @@ export default {
|
||||
"admin.config.smtp.password": "パスワード",
|
||||
"admin.config.smtp.password.description": "SMTPサーバーのパスワード",
|
||||
"admin.config.smtp.button.test": "テストメールを送信",
|
||||
"admin.config.smtp.allow-unauthorized-certificates": "Trust unauthorized SMTP server certificates",
|
||||
"admin.config.smtp.allow-unauthorized-certificates.description": "Only set this to true if you need to trust self signed certificates.",
|
||||
"admin.config.smtp.allow-unauthorized-certificates": "許可されていない SMTP サーバー証明書を信頼します",
|
||||
"admin.config.smtp.allow-unauthorized-certificates.description": "自己署名証明書を信頼する必要がある場合にのみ、これをtrueに設定してください。",
|
||||
"admin.config.oauth.allow-registration": "登録を許可する",
|
||||
"admin.config.oauth.allow-registration.description": "ユーザーにソーシャルアカウント経由での登録を許可します",
|
||||
"admin.config.oauth.ignore-totp": "二段階認証を無視する",
|
||||
"admin.config.oauth.ignore-totp.description": "ソーシャルログイン時に二段階認証を無視するかどうかを設定します",
|
||||
"admin.config.oauth.disable-password": "Disable password login",
|
||||
"admin.config.oauth.disable-password.description": "Whether to disable password login\nMake sure that an OAuth provider is properly configured before activating this configuration to avoid being locked out.",
|
||||
"admin.config.oauth.disable-password": "パスワードログインを無効にする",
|
||||
"admin.config.oauth.disable-password.description": "パスワードログインを無効にするかどうか\nロックアウトされないように、この設定を有効にする前にOAuthプロバイダーが適切に設定されていることを確認してください。",
|
||||
"admin.config.oauth.github-enabled": "GitHub",
|
||||
"admin.config.oauth.github-enabled.description": "GitHubアカウントを使用したログインを許可するかどうかを設定します",
|
||||
"admin.config.oauth.github-client-id": "GitHub クライアントID",
|
||||
@@ -375,28 +375,28 @@ export default {
|
||||
"admin.config.oauth.microsoft-client-secret.description": "Microsoft OAuthアプリのクライアントシークレット",
|
||||
"admin.config.oauth.discord-enabled": "Discord",
|
||||
"admin.config.oauth.discord-enabled.description": "Discordアカウントを使用したログインを許可するかどうかを設定します",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord limited server ID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "Limit signing in to users in a specific server. Leave it blank to disable.",
|
||||
"admin.config.oauth.discord-limited-guild": "Discord限定サーバーID",
|
||||
"admin.config.oauth.discord-limited-guild.description": "特定のサーバーのユーザーにサインインを制限します。無効にするには空白のままにしてください。",
|
||||
"admin.config.oauth.discord-client-id": "Discord クライアントID",
|
||||
"admin.config.oauth.discord-client-id.description": "Discord OAuthアプリのクライアントID",
|
||||
"admin.config.oauth.discord-client-secret": "Discord クライアントシークレット",
|
||||
"admin.config.oauth.discord-client-secret.description": "Discord OAuthアプリのクライアントシークレット",
|
||||
"admin.config.oauth.oidc-enabled": "OpenID Connect",
|
||||
"admin.config.oauth.oidc-enabled.description": "Whether OpenID Connect login is enabled",
|
||||
"admin.config.oauth.oidc-enabled.description": "OpenID Connect のログインが有効かを設定します",
|
||||
"admin.config.oauth.oidc-discovery-uri": "OpenID Connect Discovery URI",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "Discovery URI of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect username claim",
|
||||
"admin.config.oauth.oidc-username-claim.description": "Username claim in OpenID Connect ID token. Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-path": "Path to roles in OpenID Connect token",
|
||||
"admin.config.oauth.oidc-role-path.description": "Must be a valid JMES path referencing an array of roles. " + "Managing access rights using OpenID Connect roles is only recommended if no other identity provider is configured and password login is disabled. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-general-access": "OpenID Connect role for general access",
|
||||
"admin.config.oauth.oidc-role-general-access.description": "Role required for general access. Must be present in a user’s roles for them to log in. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-role-admin-access": "OpenID Connect role for admin access",
|
||||
"admin.config.oauth.oidc-role-admin-access.description": "Role required for administrative access. Must be present in a user’s roles for them to access the admin panel. " + "Leave it blank if you don't know what this config is.",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect Client ID",
|
||||
"admin.config.oauth.oidc-client-id.description": "Client ID of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-discovery-uri.description": "OpenID OAuthアプリのDiscovery URI",
|
||||
"admin.config.oauth.oidc-username-claim": "OpenID Connect ユーザー名の要求",
|
||||
"admin.config.oauth.oidc-username-claim.description": "OpenID Connect ID トークンのユーザー名要求。この設定が何かわからない場合は空白のままにしてください。",
|
||||
"admin.config.oauth.oidc-role-path": "OpenID Connectトークンのロールへのパス",
|
||||
"admin.config.oauth.oidc-role-path.description": "ロールの配列を参照する有効なJMESパスでなければなりません。" + "OpenID Connectのロールを使用してアクセス権を管理することは、他のIDプロバイダが設定されておらず、パスワードログインが無効になっている場合にのみ推奨されます。この構成がわからない場合は空白のままにしてください。" + "この設定が何であるか分からない場合は空白のままにしてください。",
|
||||
"admin.config.oauth.oidc-role-general-access": "一般的なアクセスのためのOpenID Connectのロール",
|
||||
"admin.config.oauth.oidc-role-general-access.description": "一般的なアクセスに必要なロール。ログインするユーザーのロールに存在する必要があります。 " + "この設定が何であるか分からない場合は空白のままにしてください。",
|
||||
"admin.config.oauth.oidc-role-admin-access": "管理者アクセスのための OpenID Connectのロール",
|
||||
"admin.config.oauth.oidc-role-admin-access.description": "管理者アクセスに必要なロール。管理パネルにアクセスするためには、ユーザのロールに存在する必要があります。" + "この設定が何であるか分からない場合は空白のままにしてください。",
|
||||
"admin.config.oauth.oidc-client-id": "OpenID Connect クライアントID",
|
||||
"admin.config.oauth.oidc-client-id.description": "OpenID Connect OAuth アプリのクライアント ID",
|
||||
"admin.config.oauth.oidc-client-secret": "OpenID Connect Client secret",
|
||||
"admin.config.oauth.oidc-client-secret.description": "Client secret of the OpenID Connect OAuth app",
|
||||
"admin.config.oauth.oidc-client-secret.description": "OpenID Connect OAuthアプリのクライアントシークレット",
|
||||
// 404
|
||||
"404.description": "ページが見つかりません。",
|
||||
"404.button.home": "ホームに戻る",
|
||||
@@ -407,14 +407,14 @@ export default {
|
||||
"error.msg.default": "問題が発生しました。",
|
||||
"error.msg.access_denied": "認証処理を中止しました、後で再度お試しください。",
|
||||
"error.msg.expired_token": "認証処理に時間がかかりすぎています、後で再度お試しください。",
|
||||
"error.msg.invalid_token": "Internal Error",
|
||||
"error.msg.invalid_token": "内部エラー",
|
||||
"error.msg.no_user": "この{0} アカウントにリンクしたユーザーが存在しません。",
|
||||
"error.msg.no_email": "この{0} アカウントからメールアドレスを取得出来ません。",
|
||||
"error.msg.already_linked": "この{0} アカウントは、既に別のアカウントにリンクされています。",
|
||||
"error.msg.not_linked": "この{0} アカウントはどのアカウントにもリンクされていません。",
|
||||
"error.msg.unverified_account": "This {0} account is unverified, please try again after verification.",
|
||||
"error.msg.user_not_allowed": "You are not allowed to sign in.",
|
||||
"error.msg.cannot_get_user_info": "Can not get your user info from this {0} account.",
|
||||
"error.msg.unverified_account": "この {0} アカウントは認証されていません。認証後にもう一度お試しください。",
|
||||
"error.msg.user_not_allowed": "サインインできません。",
|
||||
"error.msg.cannot_get_user_info": "この {0} アカウントからユーザー情報を取得できません。",
|
||||
"error.param.provider_github": "GitHub",
|
||||
"error.param.provider_google": "Google",
|
||||
"error.param.provider_microsoft": "Microsoft",
|
||||
@@ -432,10 +432,10 @@ export default {
|
||||
"common.button.generate": "生成",
|
||||
"common.button.done": "完了",
|
||||
"common.text.link": "リンク",
|
||||
"common.text.navigate-to-link": "Go to the link",
|
||||
"common.text.navigate-to-link": "リンクへ移動",
|
||||
"common.text.or": "または",
|
||||
"common.button.go-back": "戻る",
|
||||
"common.button.go-home": "Go home",
|
||||
"common.button.go-home": "ホームに戻る",
|
||||
"common.notify.copied": "リンクをクリップボードにコピーしました",
|
||||
"common.success": "成功",
|
||||
"common.error": "エラー",
|
||||
|
||||
@@ -152,6 +152,12 @@ export default {
|
||||
"account.reverseShares.modal.max-size.label": "共享文件上限",
|
||||
"account.reverseShares.modal.send-email": "发送邮件提醒",
|
||||
"account.reverseShares.modal.send-email.description": "当这个预留共享链接被用于共享时,发送邮件提醒",
|
||||
"account.reverseShares.modal.simplified": "简单模式",
|
||||
"account.reverseShares.modal.simplified.description":
|
||||
"让上传者更轻松地与你共享文件,他们仅能自定义共享的名称和描述。",
|
||||
"account.reverseShares.modal.public-access": "公开访问",
|
||||
"account.reverseShares.modal.public-access.description":
|
||||
"让通过这个预留共享创建共享能被公开访问。如果禁用,将只有您和创建者能够访问。",
|
||||
"account.reverseShares.modal.max-use.label": "最大使用次数",
|
||||
"account.reverseShares.modal.max-use.description": "这个预留共享链接可被用于创建共享的最大使用次数",
|
||||
"account.reverseShare.never-expires": "这个预留共享永不过期",
|
||||
@@ -255,6 +261,7 @@ export default {
|
||||
"upload.modal.completed.never-expires": "这个共享永不过期",
|
||||
"upload.modal.completed.expires-on": "这个共享将过期于 {expiration}.",
|
||||
"upload.modal.completed.share-ready": "共享创建完毕",
|
||||
"upload.modal.completed.notified-reverse-share-creator": "我们已经通知预留共享的创建者。您也可以通过其他方式将该链接手动分享给他们。",
|
||||
// END /upload
|
||||
// /share/[id]
|
||||
"share.title": "共享 {shareId}",
|
||||
@@ -264,6 +271,8 @@ export default {
|
||||
"share.error.removed.title": "共享已删除",
|
||||
"share.error.not-found.title": "共享未找到",
|
||||
"share.error.not-found.description": "共享文件走丢了",
|
||||
"share.error.access-denied.title": "私有共享",
|
||||
"share.error.access-denied.description": "当前账户没有权限访问此共享",
|
||||
"share.modal.password.title": "需要密码",
|
||||
"share.modal.password.description": "请输入密码来访问此共享",
|
||||
"share.modal.password": "密码",
|
||||
|
||||
@@ -43,6 +43,12 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
t("share.error.not-found.description"),
|
||||
);
|
||||
}
|
||||
} else if (e.response.status == 403 && error == "share_removed") {
|
||||
showErrorModal(
|
||||
modals,
|
||||
t("share.error.access-denied.title"),
|
||||
t("share.error.access-denied.description"),
|
||||
);
|
||||
} else {
|
||||
showErrorModal(modals, t("common.error"), t("common.error.unknown"));
|
||||
}
|
||||
|
||||
@@ -69,6 +69,12 @@ const Share = ({ shareId }: { shareId: string }) => {
|
||||
"go-home",
|
||||
);
|
||||
}
|
||||
} else if (e.response.status == 403 && error == "private_share") {
|
||||
showErrorModal(
|
||||
modals,
|
||||
t("share.error.access-denied.title"),
|
||||
t("share.error.access-denied.description"),
|
||||
);
|
||||
} else if (error == "share_password_required") {
|
||||
showEnterPasswordModal(modals, getShareToken);
|
||||
} else if (error == "share_token_required") {
|
||||
|
||||
@@ -17,12 +17,14 @@ const Share = ({ reverseShareToken }: { reverseShareToken: string }) => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
const [maxShareSize, setMaxShareSize] = useState(0);
|
||||
const [simplified, setSimplified] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
shareService
|
||||
.setReverseShare(reverseShareToken)
|
||||
.then((reverseShareTokenData) => {
|
||||
setMaxShareSize(parseInt(reverseShareTokenData.maxShareSize));
|
||||
setSimplified(reverseShareTokenData.simplified);
|
||||
setIsLoading(false);
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -38,7 +40,13 @@ const Share = ({ reverseShareToken }: { reverseShareToken: string }) => {
|
||||
|
||||
if (isLoading) return <LoadingOverlay visible />;
|
||||
|
||||
return <Upload isReverseShare maxShareSize={maxShareSize} />;
|
||||
return (
|
||||
<Upload
|
||||
isReverseShare
|
||||
maxShareSize={maxShareSize}
|
||||
simplified={simplified}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Share;
|
||||
|
||||
@@ -25,9 +25,11 @@ let createdShare: Share;
|
||||
const Upload = ({
|
||||
maxShareSize,
|
||||
isReverseShare = false,
|
||||
simplified,
|
||||
}: {
|
||||
maxShareSize?: number;
|
||||
isReverseShare: boolean;
|
||||
simplified: boolean;
|
||||
}) => {
|
||||
const modals = useModals();
|
||||
const t = useTranslate();
|
||||
@@ -133,6 +135,7 @@ const Upload = ({
|
||||
),
|
||||
enableEmailRecepients: config.get("email.enableShareEmailRecipients"),
|
||||
maxExpirationInHours: config.get("share.maxExpiration"),
|
||||
simplified,
|
||||
},
|
||||
files,
|
||||
uploadFiles,
|
||||
|
||||
@@ -109,6 +109,8 @@ const createReverseShare = async (
|
||||
maxShareSize: number,
|
||||
maxUseCount: number,
|
||||
sendEmailNotification: boolean,
|
||||
simplified: boolean,
|
||||
publicAccess: boolean,
|
||||
) => {
|
||||
return (
|
||||
await api.post("reverseShares", {
|
||||
@@ -116,6 +118,8 @@ const createReverseShare = async (
|
||||
maxShareSize: maxShareSize.toString(),
|
||||
maxUseCount,
|
||||
sendEmailNotification,
|
||||
simplified,
|
||||
publicAccess,
|
||||
})
|
||||
).data;
|
||||
};
|
||||
|
||||
@@ -11,6 +11,15 @@ export type Share = {
|
||||
hasPassword: boolean;
|
||||
};
|
||||
|
||||
export type CompletedShare = Share & {
|
||||
/**
|
||||
* undefined means is not reverse share
|
||||
* true means server was send email to reverse share creator
|
||||
* false means server was not send email to reverse share creator
|
||||
* */
|
||||
notifyReverseShareCreator: boolean | undefined;
|
||||
};
|
||||
|
||||
export type CreateShare = {
|
||||
id: string;
|
||||
name?: string;
|
||||
|
||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "pingvin-share",
|
||||
"version": "0.28.0",
|
||||
"version": "0.29.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "pingvin-share",
|
||||
"version": "0.28.0",
|
||||
"version": "0.29.0",
|
||||
"devDependencies": {
|
||||
"conventional-changelog-cli": "^3.0.0"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "pingvin-share",
|
||||
"version": "0.28.0",
|
||||
"version": "0.29.0",
|
||||
"scripts": {
|
||||
"format": "cd frontend && npm run format && cd ../backend && npm run format",
|
||||
"lint": "cd frontend && npm run lint && cd ../backend && npm run lint",
|
||||
|
||||
Reference in New Issue
Block a user