Compare commits

..

4 Commits

Author SHA1 Message Date
mirarifhasan
54a7e73981 chore: shortcode seed added and revamp the script 2024-05-08 21:17:43 +06:00
mirarifhasan
2a3dce6621 fix: generate fake email in lower case 2024-05-08 20:14:20 +06:00
mirarifhasan
260f526edc fix: dist folder being unsupported 2024-05-07 13:02:44 +06:00
mirarifhasan
20b9b94b53 build: seed file added
run nestjs app in dev mode, prod mode has an error
2024-05-07 11:30:32 +06:00
44 changed files with 1227 additions and 1328 deletions

View File

@@ -35,20 +35,9 @@ MICROSOFT_SCOPE="user.read"
MICROSOFT_TENANT="common"
# Mailer config
MAILER_SMTP_ENABLE="true"
MAILER_USE_ADVANCE_CONFIGS="false"
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com"
MAILER_ADDRESS_FROM='"From Name Here" <from@example.com>'
MAILER_SMTP_URL="smtps://user@domain.com:pass@smtp.domain.com" # used if custom mailer configs is false
# The following are used if custom mailer configs is true
MAILER_SMTP_HOST="smtp.domain.com"
MAILER_SMTP_PORT="587"
MAILER_SMTP_SECURE="true"
MAILER_SMTP_USER="user@domain.com"
MAILER_SMTP_PASSWORD="pass"
MAILER_TLS_REJECT_UNAUTHORIZED="true"
# Rate Limit Config
RATE_LIMIT_TTL=60 # In seconds
RATE_LIMIT_MAX=100 # Max requests per IP

View File

@@ -1,4 +1,4 @@
FROM node:20.12.2 AS builder
FROM node:18.8.0 AS builder
WORKDIR /usr/src/app

View File

@@ -5,6 +5,9 @@
"author": "",
"private": true,
"license": "UNLICENSED",
"prisma": {
"seed": "ts-node prisma/seed.ts"
},
"scripts": {
"prebuild": "rimraf dist",
"build": "nest build",
@@ -30,13 +33,11 @@
"@nestjs/common": "10.2.7",
"@nestjs/config": "3.1.1",
"@nestjs/core": "10.2.7",
"@nestjs/event-emitter": "2.0.4",
"@nestjs/graphql": "12.0.9",
"@nestjs/jwt": "10.1.1",
"@nestjs/passport": "10.0.2",
"@nestjs/platform-express": "10.2.7",
"@nestjs/schedule": "4.0.1",
"@nestjs/terminus": "10.2.3",
"@nestjs/throttler": "5.0.1",
"@prisma/client": "5.8.1",
"argon2": "0.30.3",
@@ -68,6 +69,7 @@
"rxjs": "7.6.0"
},
"devDependencies": {
"@faker-js/faker": "8.4.1",
"@nestjs/cli": "10.2.1",
"@nestjs/schematics": "10.0.3",
"@nestjs/testing": "10.2.7",

View File

@@ -0,0 +1,444 @@
import { faker } from '@faker-js/faker';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const createUsersAndAccounts = async (
entries = 10,
withDedicateUsers: { email: string; isAdmin: boolean }[] = [],
) => {
const createAUser = async (email?, isAdmin?) => {
try {
const newUser = await prisma.user.create({
data: {
displayName: faker.person.fullName(),
email: email ?? faker.internet.email().toLowerCase(),
photoURL: faker.image.avatar(),
isAdmin: isAdmin ?? false,
},
});
await prisma.account.create({
data: {
userId: newUser.uid,
provider: 'magic',
providerAccountId: newUser.email,
},
});
} catch (e) {
console.log('Error creating user/account:', e.message);
}
};
for (let i = 0; i < withDedicateUsers.length; i++) {
const user = withDedicateUsers[i];
await createAUser(user.email.toLowerCase(), user.isAdmin);
}
for (let i = 0; i < entries - withDedicateUsers.length; i++) {
await createAUser();
}
console.log('Users created');
};
const createInvitedUsers = async (entries = 10) => {
try {
const admin = await prisma.user.findFirst({ where: { isAdmin: true } });
for (let i = 0; i < entries; i++) {
await prisma.invitedUsers.create({
data: {
adminUid: admin.uid,
adminEmail: admin.email,
inviteeEmail: faker.internet.email().toLowerCase(),
},
});
}
console.log('Invited users created');
} catch (e) {
console.log('Error creating invited users:', e.message);
}
};
const createUserCollections = async (
parentCollEntries = 10,
childCollEntriesOnEachParent = 10,
) => {
const createACollection = async (userUid, parentID, orderIndex) => {
return prisma.userCollection.create({
data: {
parentID,
title: faker.vehicle.vehicle(),
orderIndex,
type: 'REST',
userUid,
},
});
};
try {
const users = await prisma.user.findMany();
for (let u = 0; u < users.length; u++) {
const user = users[u];
for (let i = 0; i < parentCollEntries; i++) {
const parentCollection = await createACollection(user.uid, null, i + 1);
for (let j = 0; j < childCollEntriesOnEachParent; j++) {
await createACollection(user.uid, parentCollection.id, j + 1);
}
}
}
console.log('User collections created');
} catch (e) {
console.log('Error creating user collections:', e.message);
}
};
const createUserRequests = async (entriesPerCollection = 10) => {
try {
const collections = await prisma.userCollection.findMany();
for (let i = 0; i < collections.length; i++) {
const collection = collections[i];
for (let j = 0; j < entriesPerCollection; j++) {
const requestTitle = faker.git.branch();
await prisma.userRequest.create({
data: {
collectionID: collection.id,
userUid: collection.userUid,
title: requestTitle,
request: {
v: '4',
auth: { authType: 'inherit', authActive: true },
body: { body: null, contentType: null },
name: requestTitle,
method: 'GET',
params: [],
headers: [],
endpoint: 'https://echo.hoppscotch.io',
testScript: '',
preRequestScript: '',
requestVariables: [],
},
type: collection.type,
orderIndex: j + 1,
},
});
}
}
console.log('User requests created');
} catch (e) {
console.log('Error creating user requests:', e.message);
}
};
const createUserEnvironments = async () => {
try {
const users = await prisma.user.findMany();
for (let i = 0; i < users.length; i++) {
const user = users[i];
const environments = await prisma.userEnvironment.findMany({
where: { userUid: user.uid },
});
if (environments.length > 1) continue; // skip if already created. (assuming default GLOBAL environments are created by APP itself)
await prisma.userEnvironment.createMany({
data: [
{
userUid: user.uid,
name: 'production',
variables: [
{
key: 'product_id',
value: faker.number.int({ max: 1000 }),
secret: false,
},
],
isGlobal: false,
},
{
userUid: user.uid,
name: 'development',
variables: [
{
key: 'product_id',
value: faker.number.int({ max: 1000 }),
secret: false,
},
],
isGlobal: false,
},
],
});
}
console.log('User environments created');
} catch (e) {
console.log('Error creating user environment:', e.message);
}
};
const createTeamsAndTeamMembers = async (
entries = 10,
memberCountInATeam = 4,
) => {
const createATeam = async (ownerUid) => {
return prisma.team.create({
data: {
name: faker.airline.airplane().name,
members: { create: { userUid: ownerUid, role: 'OWNER' } },
},
});
};
const createATeamMember = async (teamID, userUid) => {
try {
return prisma.teamMember.create({
data: {
teamID,
userUid,
role: +faker.number.binary() === 1 ? 'EDITOR' : 'VIEWER',
},
});
} catch (e) {
console.log(e.message);
}
};
try {
const users = await prisma.user.findMany();
for (let i = 0; i < entries; i++) {
const ownerIndex = faker.number.int({ min: 0, max: users.length - 1 });
const team = await createATeam(users[ownerIndex].uid); // create a team with owner
for (let j = 0; j < Math.min(memberCountInATeam, users.length) - 1; ) {
const memberIndex = faker.number.int({ min: 0, max: users.length - 1 });
// check if user already added
const existingTeamMember = await prisma.teamMember.findFirst({
where: {
teamID: team.id,
userUid: users[memberIndex].uid,
},
});
if (existingTeamMember) continue;
await createATeamMember(team.id, users[memberIndex].uid);
j++;
}
}
console.log('Teams and TeamMembers created');
} catch (e) {
console.log('Error creating teams and team members:', e.message);
}
};
const createTeamEnvironments = async () => {
try {
const teams = await prisma.team.findMany();
for (let i = 0; i < teams.length; i++) {
const team = teams[i];
const environments = await prisma.teamEnvironment.findMany({
where: { teamID: team.id },
});
if (environments.length > 1) continue; // skip if already created. (assuming default GLOBAL environments are created by APP itself)
await prisma.teamEnvironment.createMany({
data: [
{
teamID: team.id,
name: 'team_env_production',
variables: [
{
key: 'category_id',
value: faker.number.int({ max: 1000 }).toString(),
secret: false,
},
],
},
{
teamID: team.id,
name: 'team_env_development',
variables: [
{
key: 'category_id',
value: faker.number.int({ max: 1000 }).toString(),
secret: false,
},
],
},
],
});
}
console.log('Team environments created');
} catch (e) {
console.log('Error creating team environments: ', e.message);
}
};
const createTeamCollections = async (
parentCollEntries = 10,
childCollEntriesOnEachParent = 10,
) => {
const createACollection = async (teamID, parentID, orderIndex) => {
return prisma.teamCollection.create({
data: { parentID, title: faker.vehicle.vehicle(), orderIndex, teamID },
});
};
try {
const teams = await prisma.team.findMany();
for (let t = 0; t < teams.length; t++) {
const team = teams[t];
for (let i = 0; i < parentCollEntries; i++) {
const parentCollection = await createACollection(team.id, null, i + 1);
for (let j = 0; j < childCollEntriesOnEachParent; j++) {
await createACollection(team.id, parentCollection.id, j + 1);
}
}
}
console.log('Team collections created');
} catch (e) {
console.log('Error creating team collection:', e.message);
}
};
const createTeamRequests = async (entriesPerCollection = 10) => {
try {
const collections = await prisma.teamCollection.findMany();
for (let i = 0; i < collections.length; i++) {
const collection = collections[i];
for (let j = 0; j < entriesPerCollection; j++) {
const requestTitle = faker.git.branch();
await prisma.teamRequest.create({
data: {
collectionID: collection.id,
teamID: collection.teamID,
title: requestTitle,
request: {
v: '4',
auth: { authType: 'inherit', authActive: true },
body: { body: null, contentType: null },
name: requestTitle,
method: 'GET',
params: [],
headers: [],
endpoint: 'https://echo.hoppscotch.io',
testScript: '',
preRequestScript: '',
requestVariables: [],
},
orderIndex: j + 1,
},
});
}
}
console.log('Team requests created');
} catch (e) {
console.log('Error creating team requests:', e.message);
}
};
async function createShortcodes(entriesPerUser = 2) {
try {
const users = await prisma.user.findMany();
for (let i = 0; i < users.length; i++) {
const user = users[i];
for (let j = 0; j < entriesPerUser; j++) {
const userRequests = await prisma.userRequest.findMany({
where: { userUid: user.uid },
take: entriesPerUser,
});
let shortCodeRequestObj = {
v: '4',
id: userRequests[j].id,
auth: { authType: 'inherit', authActive: true },
body: { body: null, contentType: null },
name: 'driver-hack',
method: 'GET',
params: [],
headers: [],
endpoint: 'https://echo.hoppscotch.io',
testScript: '',
preRequestScript: '',
requestVariables: [],
};
await prisma.shortcode.create({
data: {
id: faker.string.alphanumeric(12),
creatorUid: user.uid,
request: shortCodeRequestObj,
embedProperties: null,
},
});
}
}
console.log('Shortcodes created');
} catch (e) {
console.log('Error creating shortcodes:', e.message);
}
}
async function clearAllData() {
try {
// Get all model names
const modelNames = Object.keys(prisma).filter(
(str) => !str.startsWith('_') && !str.startsWith('$'),
);
// Iterate through each model and delete all data
for (let i = 0; i < modelNames.length; i++) {
await prisma[modelNames[i]].deleteMany({});
}
console.log('All data cleared');
} catch (e) {
console.log('Error in clearing data:', e.message);
}
}
(async () => {
await clearAllData();
await createUsersAndAccounts(2, [
{ email: 'admin@gmail.com', isAdmin: true },
{ email: 'user@gmail.com', isAdmin: false },
]);
await createInvitedUsers(10);
// `userSettings` can be created by APP itself
await createUserCollections(10, 10);
await createUserRequests(10);
// `userHistory` can be created by APP itself
await createUserEnvironments();
await createShortcodes(3);
await createTeamsAndTeamMembers(10, 4);
// `teamInvitation` can be created by APP itself
await createTeamEnvironments();
await createTeamCollections(5, 5);
await createTeamRequests(3);
})();

View File

@@ -22,7 +22,6 @@ import { ShortcodeService } from 'src/shortcode/shortcode.service';
import { ConfigService } from '@nestjs/config';
import { OffsetPaginationArgs } from 'src/types/input-types.args';
import * as E from 'fp-ts/Either';
import { EventEmitter2 } from '@nestjs/event-emitter';
const mockPrisma = mockDeep<PrismaService>();
const mockPubSub = mockDeep<PubSubService>();
@@ -35,7 +34,6 @@ const mockTeamCollectionService = mockDeep<TeamCollectionService>();
const mockMailerService = mockDeep<MailerService>();
const mockShortcodeService = mockDeep<ShortcodeService>();
const mockConfigService = mockDeep<ConfigService>();
const mockEventEmitter = mockDeep<EventEmitter2>();
const adminService = new AdminService(
mockUserService,
@@ -46,9 +44,9 @@ const adminService = new AdminService(
mockTeamInvitationService,
mockPubSub as any,
mockPrisma as any,
mockMailerService,
mockShortcodeService,
mockConfigService,
mockEventEmitter,
);
const invitedUsers: InvitedUsers[] = [

View File

@@ -19,6 +19,7 @@ import {
USER_IS_ADMIN,
USER_NOT_FOUND,
} from '../errors';
import { MailerService } from '../mailer/mailer.service';
import { InvitedUser } from './invited-user.model';
import { TeamService } from '../team/team.service';
import { TeamCollectionService } from '../team-collection/team-collection.service';
@@ -30,8 +31,6 @@ import { ShortcodeService } from 'src/shortcode/shortcode.service';
import { ConfigService } from '@nestjs/config';
import { OffsetPaginationArgs } from 'src/types/input-types.args';
import { UserDeletionResult } from 'src/user/user.model';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Events } from 'src/types/EventEmitter';
@Injectable()
export class AdminService {
@@ -44,9 +43,9 @@ export class AdminService {
private readonly teamInvitationService: TeamInvitationService,
private readonly pubsub: PubSubService,
private readonly prisma: PrismaService,
private readonly mailerService: MailerService,
private readonly shortcodeService: ShortcodeService,
private readonly configService: ConfigService,
private readonly eventEmitter: EventEmitter2,
) {}
/**
@@ -100,16 +99,17 @@ export class AdminService {
});
if (alreadyInvitedUser != null) return E.left(USER_ALREADY_INVITED);
this.eventEmitter.emit(Events.MAILER_SEND_USER_INVITATION_EMAIL, {
to: inviteeEmail,
mailDesc: {
try {
await this.mailerService.sendUserInvitationEmail(inviteeEmail, {
template: 'user-invitation',
variables: {
inviteeEmail: inviteeEmail,
magicLink: `${this.configService.get('VITE_BASE_URL')}`,
},
},
});
});
} catch (e) {
return E.left(EMAIL_FAILED);
}
// Add invitee email to the list of invited users by admin
const dbInvitedUser = await this.prisma.invitedUsers.create({

View File

@@ -292,14 +292,6 @@ export class InfraResolver {
return this.infraConfigService.getAllowedAuthProviders();
}
@Query(() => Boolean, {
description: 'Check if the SMTP is enabled or not',
})
@UseGuards(GqlAuthGuard)
isSMTPEnabled() {
return this.infraConfigService.isSMTPEnabled();
}
/* Mutations */
@Mutation(() => [InfraConfig], {
@@ -367,23 +359,4 @@ export class InfraResolver {
return true;
}
@Mutation(() => Boolean, {
description: 'Enable or Disable SMTP for sending emails',
})
@UseGuards(GqlAuthGuard, GqlAdminGuard)
async toggleSMTP(
@Args({
name: 'status',
type: () => ServiceStatus,
description: 'Toggle SMTP',
})
status: ServiceStatus,
) {
const isUpdated = await this.infraConfigService.enableAndDisableSMTP(
status,
);
if (E.isLeft(isUpdated)) throwErr(isUpdated.left);
return true;
}
}

View File

@@ -26,12 +26,9 @@ import { loadInfraConfiguration } from './infra-config/helper';
import { MailerModule } from './mailer/mailer.module';
import { PosthogModule } from './posthog/posthog.module';
import { ScheduleModule } from '@nestjs/schedule';
import { HealthModule } from './health/health.module';
import { EventEmitterModule } from '@nestjs/event-emitter';
@Module({
imports: [
EventEmitterModule.forRoot(),
ConfigModule.forRoot({
isGlobal: true,
load: [async () => loadInfraConfiguration()],
@@ -103,7 +100,6 @@ import { EventEmitterModule } from '@nestjs/event-emitter';
InfraConfigModule,
PosthogModule,
ScheduleModule.forRoot(),
HealthModule,
],
providers: [GQLComplexityPlugin],
controllers: [AppController],

View File

@@ -23,7 +23,6 @@ import * as argon2 from 'argon2';
import * as E from 'fp-ts/Either';
import { ConfigService } from '@nestjs/config';
import { InfraConfigService } from 'src/infra-config/infra-config.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
const mockPrisma = mockDeep<PrismaService>();
const mockUser = mockDeep<UserService>();
@@ -31,7 +30,6 @@ const mockJWT = mockDeep<JwtService>();
const mockMailer = mockDeep<MailerService>();
const mockConfigService = mockDeep<ConfigService>();
const mockInfraConfigService = mockDeep<InfraConfigService>();
const mockEventEmitter = mockDeep<EventEmitter2>();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
@@ -39,9 +37,9 @@ const authService = new AuthService(
mockUser,
mockPrisma,
mockJWT,
mockMailer,
mockConfigService,
mockInfraConfigService,
mockEventEmitter,
);
const currentTime = new Date();

View File

@@ -1,4 +1,5 @@
import { HttpStatus, Injectable } from '@nestjs/common';
import { MailerService } from 'src/mailer/mailer.service';
import { PrismaService } from 'src/prisma/prisma.service';
import { UserService } from 'src/user/user.service';
import { VerifyMagicDto } from './dto/verify-magic.dto';
@@ -29,8 +30,6 @@ import { VerificationToken } from '@prisma/client';
import { Origin } from './helper';
import { ConfigService } from '@nestjs/config';
import { InfraConfigService } from 'src/infra-config/infra-config.service';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Events } from 'src/types/EventEmitter';
@Injectable()
export class AuthService {
@@ -38,9 +37,9 @@ export class AuthService {
private usersService: UserService,
private prismaService: PrismaService,
private jwtService: JwtService,
private readonly mailerService: MailerService,
private readonly configService: ConfigService,
private infraConfigService: InfraConfigService,
private eventEmitter: EventEmitter2,
) {}
/**
@@ -235,14 +234,11 @@ export class AuthService {
url = this.configService.get('VITE_BASE_URL');
}
this.eventEmitter.emit(Events.MAILER_SEND_EMAIL, {
to: email,
mailDesc: {
template: 'user-invitation',
variables: {
inviteeEmail: email,
magicLink: `${url}/enter?token=${generatedTokens.token}`,
},
await this.mailerService.sendEmail(email, {
template: 'user-invitation',
variables: {
inviteeEmail: email,
magicLink: `${url}/enter?token=${generatedTokens.token}`,
},
});

View File

@@ -678,19 +678,6 @@ export const MAILER_SMTP_URL_UNDEFINED = 'mailer/smtp_url_undefined' as const;
export const MAILER_FROM_ADDRESS_UNDEFINED =
'mailer/from_address_undefined' as const;
/**
* MAILER_SMTP_USER environment variable is not defined
* (MailerModule)
*/
export const MAILER_SMTP_USER_UNDEFINED = 'mailer/smtp_user_undefined' as const;
/**
* MAILER_SMTP_PASSWORD environment variable is not defined
* (MailerModule)
*/
export const MAILER_SMTP_PASSWORD_UNDEFINED =
'mailer/smtp_password_undefined' as const;
/**
* SharedRequest invalid request JSON format
* (ShortcodeService)

View File

@@ -1,24 +0,0 @@
import { Controller, Get } from '@nestjs/common';
import {
HealthCheck,
HealthCheckService,
PrismaHealthIndicator,
} from '@nestjs/terminus';
import { PrismaService } from 'src/prisma/prisma.service';
@Controller('health')
export class HealthController {
constructor(
private health: HealthCheckService,
private prismaHealth: PrismaHealthIndicator,
private prisma: PrismaService,
) {}
@Get()
@HealthCheck()
check() {
return this.health.check([
async () => this.prismaHealth.pingCheck('database', this.prisma),
]);
}
}

View File

@@ -1,10 +0,0 @@
import { Module } from '@nestjs/common';
import { HealthController } from './health.controller';
import { PrismaModule } from 'src/prisma/prisma.module';
import { TerminusModule } from '@nestjs/terminus';
@Module({
imports: [PrismaModule, TerminusModule],
controllers: [HealthController],
})
export class HealthModule {}

View File

@@ -33,17 +33,10 @@ const AuthProviderConfigurations = {
InfraConfigEnum.MICROSOFT_SCOPE,
InfraConfigEnum.MICROSOFT_TENANT,
],
[AuthProvider.EMAIL]: !!process.env.MAILER_USE_CUSTOM_CONFIGS
? [
InfraConfigEnum.MAILER_SMTP_HOST,
InfraConfigEnum.MAILER_SMTP_PORT,
InfraConfigEnum.MAILER_SMTP_SECURE,
InfraConfigEnum.MAILER_SMTP_USER,
InfraConfigEnum.MAILER_SMTP_PASSWORD,
InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED,
InfraConfigEnum.MAILER_ADDRESS_FROM,
]
: [InfraConfigEnum.MAILER_SMTP_URL, InfraConfigEnum.MAILER_ADDRESS_FROM],
[AuthProvider.EMAIL]: [
InfraConfigEnum.MAILER_SMTP_URL,
InfraConfigEnum.MAILER_ADDRESS_FROM,
],
};
/**
@@ -82,14 +75,6 @@ export async function getDefaultInfraConfigs(): Promise<
// Prepare rows for 'infra_config' table with default values (from .env) for each 'name'
const infraConfigDefaultObjs: { name: InfraConfigEnum; value: string }[] = [
{
name: InfraConfigEnum.MAILER_SMTP_ENABLE,
value: process.env.MAILER_SMTP_ENABLE ?? 'true',
},
{
name: InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS,
value: process.env.MAILER_USE_CUSTOM_CONFIGS ?? 'false',
},
{
name: InfraConfigEnum.MAILER_SMTP_URL,
value: process.env.MAILER_SMTP_URL,
@@ -98,30 +83,6 @@ export async function getDefaultInfraConfigs(): Promise<
name: InfraConfigEnum.MAILER_ADDRESS_FROM,
value: process.env.MAILER_ADDRESS_FROM,
},
{
name: InfraConfigEnum.MAILER_SMTP_HOST,
value: process.env.MAILER_SMTP_HOST,
},
{
name: InfraConfigEnum.MAILER_SMTP_PORT,
value: process.env.MAILER_SMTP_PORT,
},
{
name: InfraConfigEnum.MAILER_SMTP_SECURE,
value: process.env.MAILER_SMTP_SECURE,
},
{
name: InfraConfigEnum.MAILER_SMTP_USER,
value: process.env.MAILER_SMTP_USER,
},
{
name: InfraConfigEnum.MAILER_SMTP_PASSWORD,
value: process.env.MAILER_SMTP_PASSWORD,
},
{
name: InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED,
value: process.env.MAILER_TLS_REJECT_UNAUTHORIZED,
},
{
name: InfraConfigEnum.GOOGLE_CLIENT_ID,
value: process.env.GOOGLE_CLIENT_ID,

View File

@@ -43,7 +43,6 @@ export class InfraConfigService implements OnModuleInit {
InfraConfigEnum.ALLOW_ANALYTICS_COLLECTION,
InfraConfigEnum.ANALYTICS_USER_ID,
InfraConfigEnum.IS_FIRST_TIME_INFRA_SETUP,
InfraConfigEnum.MAILER_SMTP_ENABLE,
];
// Following fields can not be fetched by `infraConfigs` Query. Use dedicated queries for these fields instead.
EXCLUDE_FROM_FETCH_CONFIGS = [
@@ -219,47 +218,6 @@ export class InfraConfigService implements OnModuleInit {
return E.right(isUpdated.right.value === 'true');
}
/**
* Enable or Disable SMTP
* @param status Status to enable or disable
* @returns Either true or an error
*/
async enableAndDisableSMTP(status: ServiceStatus) {
const isUpdated = await this.toggleServiceStatus(
InfraConfigEnum.MAILER_SMTP_ENABLE,
status,
true,
);
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
if (status === ServiceStatus.DISABLE) {
this.enableAndDisableSSO([{ provider: AuthProvider.EMAIL, status }]);
}
return E.right(true);
}
/**
* Enable or Disable Service (i.e. ALLOW_AUDIT_LOGS, ALLOW_ANALYTICS_COLLECTION, ALLOW_DOMAIN_WHITELISTING, SITE_PROTECTION)
* @param configName Name of the InfraConfigEnum
* @param status Status to enable or disable
* @param restartEnabled If true, restart the app after updating the InfraConfig
* @returns Either true or an error
*/
async toggleServiceStatus(
configName: InfraConfigEnum,
status: ServiceStatus,
restartEnabled = false,
) {
const isUpdated = await this.update(
configName,
status === ServiceStatus.ENABLE ? 'true' : 'false',
restartEnabled,
);
if (E.isLeft(isUpdated)) return E.left(isUpdated.left);
return E.right(true);
}
/**
* Enable or Disable SSO for login/signup
* @param provider Auth Provider to enable or disable
@@ -358,16 +316,6 @@ export class InfraConfigService implements OnModuleInit {
.split(',');
}
/**
* Check if SMTP is enabled or not
* @returns boolean
*/
isSMTPEnabled() {
return (
this.configService.get<string>('INFRA.MAILER_SMTP_ENABLE') === 'true'
);
}
/**
* Reset all the InfraConfigs to their default values (from .env)
*/
@@ -415,20 +363,6 @@ export class InfraConfigService implements OnModuleInit {
) {
for (let i = 0; i < infraConfigs.length; i++) {
switch (infraConfigs[i].name) {
case InfraConfigEnum.MAILER_SMTP_ENABLE:
if (
infraConfigs[i].value !== 'true' &&
infraConfigs[i].value !== 'false'
)
return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_USE_CUSTOM_CONFIGS:
if (
infraConfigs[i].value !== 'true' &&
infraConfigs[i].value !== 'false'
)
return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_SMTP_URL:
const isValidUrl = validateSMTPUrl(infraConfigs[i].value);
if (!isValidUrl) return E.left(INFRA_CONFIG_INVALID_INPUT);
@@ -437,32 +371,6 @@ export class InfraConfigService implements OnModuleInit {
const isValidEmail = validateSMTPEmail(infraConfigs[i].value);
if (!isValidEmail) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_SMTP_HOST:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_SMTP_PORT:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_SMTP_SECURE:
if (
infraConfigs[i].value !== 'true' &&
infraConfigs[i].value !== 'false'
)
return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_SMTP_USER:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_SMTP_PASSWORD:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.MAILER_TLS_REJECT_UNAUTHORIZED:
if (
infraConfigs[i].value !== 'true' &&
infraConfigs[i].value !== 'false'
)
return E.left(INFRA_CONFIG_INVALID_INPUT);
break;
case InfraConfigEnum.GOOGLE_CLIENT_ID:
if (!infraConfigs[i].value) return E.left(INFRA_CONFIG_INVALID_INPUT);
break;

View File

@@ -1,30 +0,0 @@
import { Injectable } from '@nestjs/common';
import { OnEvent } from '@nestjs/event-emitter';
import { Events } from 'src/types/EventEmitter';
import {
AdminUserInvitationMailDescription,
MailDescription,
UserMagicLinkMailDescription,
} from './MailDescriptions';
import { MailerService } from './mailer.service';
@Injectable()
export class MailerEventListener {
constructor(private mailerService: MailerService) {}
@OnEvent(Events.MAILER_SEND_EMAIL, { async: true })
async handleSendEmailEvent(data: {
to: string;
mailDesc: MailDescription | UserMagicLinkMailDescription;
}) {
await this.mailerService.sendEmail(data.to, data.mailDesc);
}
@OnEvent(Events.MAILER_SEND_USER_INVITATION_EMAIL, { async: true })
async handleSendUserInvitationEmailEvent(data: {
to: string;
mailDesc: AdminUserInvitationMailDescription;
}) {
await this.mailerService.sendUserInvitationEmail(data.to, data.mailDesc);
}
}

View File

@@ -4,43 +4,38 @@ import { HandlebarsAdapter } from '@nestjs-modules/mailer/dist/adapters/handleba
import { MailerService } from './mailer.service';
import { throwErr } from 'src/utils';
import {
MAILER_SMTP_PASSWORD_UNDEFINED,
MAILER_FROM_ADDRESS_UNDEFINED,
MAILER_SMTP_URL_UNDEFINED,
MAILER_SMTP_USER_UNDEFINED,
} from 'src/errors';
import { ConfigService } from '@nestjs/config';
import { loadInfraConfiguration } from 'src/infra-config/helper';
import { MailerEventListener } from './mailer.listener';
import { TransportType } from '@nestjs-modules/mailer/dist/interfaces/mailer-options.interface';
@Global()
@Module({})
@Module({
imports: [],
providers: [MailerService],
exports: [MailerService],
})
export class MailerModule {
static async register() {
const config = new ConfigService();
const env = await loadInfraConfiguration();
// If mailer SMTP is DISABLED, return the module without any configuration (service, listener, etc.)
if (env.INFRA.MAILER_SMTP_ENABLE !== 'true') {
console.log('Mailer SMTP is disabled');
return { module: MailerModule };
let mailerSmtpUrl = env.INFRA.MAILER_SMTP_URL;
let mailerAddressFrom = env.INFRA.MAILER_ADDRESS_FROM;
if (!env.INFRA.MAILER_SMTP_URL || !env.INFRA.MAILER_ADDRESS_FROM) {
const config = new ConfigService();
mailerSmtpUrl = config.get('MAILER_SMTP_URL');
mailerAddressFrom = config.get('MAILER_ADDRESS_FROM');
}
// If mailer is ENABLED, return the module with configuration (service, listener, etc.)
// Determine transport configuration based on custom config flag
let transportOption = getTransportOption(env, config);
// Get mailer address from environment or config
const mailerAddressFrom = getMailerAddressFrom(env, config);
return {
module: MailerModule,
providers: [MailerService, MailerEventListener],
imports: [
NestMailerModule.forRoot({
transport: transportOption,
transport: mailerSmtpUrl ?? throwErr(MAILER_SMTP_URL_UNDEFINED),
defaults: {
from: mailerAddressFrom,
from: mailerAddressFrom ?? throwErr(MAILER_FROM_ADDRESS_UNDEFINED),
},
template: {
dir: __dirname + '/templates',
@@ -51,52 +46,3 @@ export class MailerModule {
};
}
}
function isEnabled(value) {
return value === 'true';
}
function getMailerAddressFrom(env, config): string {
return (
env.INFRA.MAILER_ADDRESS_FROM ??
config.get('MAILER_ADDRESS_FROM') ??
throwErr(MAILER_SMTP_URL_UNDEFINED)
);
}
function getTransportOption(env, config): TransportType {
const useCustomConfigs = isEnabled(
env.INFRA.MAILER_USE_CUSTOM_CONFIGS ??
config.get('MAILER_USE_CUSTOM_CONFIGS'),
);
if (!useCustomConfigs) {
console.log('Using simple mailer configuration');
return (
env.INFRA.MAILER_SMTP_URL ??
config.get('MAILER_SMTP_URL') ??
throwErr(MAILER_SMTP_URL_UNDEFINED)
);
} else {
console.log('Using advanced mailer configuration');
return {
host: env.INFRA.MAILER_SMTP_HOST ?? config.get('MAILER_SMTP_HOST'),
port: +env.INFRA.MAILER_SMTP_PORT ?? +config.get('MAILER_SMTP_PORT'),
secure:
!!env.INFRA.MAILER_SMTP_SECURE ?? !!config.get('MAILER_SMTP_SECURE'),
auth: {
user:
env.INFRA.MAILER_SMTP_USER ??
config.get('MAILER_SMTP_USER') ??
throwErr(MAILER_SMTP_USER_UNDEFINED),
pass:
env.INFRA.MAILER_SMTP_PASSWORD ??
config.get('MAILER_SMTP_PASSWORD') ??
throwErr(MAILER_SMTP_PASSWORD_UNDEFINED),
},
tls: {
rejectUnauthorized:
!!env.INFRA.MAILER_TLS_REJECT_UNAUTHORIZED ??
!!config.get('MAILER_TLS_REJECT_UNAUTHORIZED'),
},
};
}
}

View File

@@ -7,14 +7,10 @@ import {
import { throwErr } from 'src/utils';
import { EMAIL_FAILED } from 'src/errors';
import { MailerService as NestMailerService } from '@nestjs-modules/mailer';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class MailerService {
constructor(
private readonly nestMailerService: NestMailerService,
private readonly configService: ConfigService,
) {}
constructor(private readonly nestMailerService: NestMailerService) {}
/**
* Takes an input mail description and spits out the Email subject required for it
@@ -46,8 +42,6 @@ export class MailerService {
to: string,
mailDesc: MailDescription | UserMagicLinkMailDescription,
) {
if (this.configService.get('INFRA.MAILER_SMTP_ENABLE') !== 'true') return;
try {
await this.nestMailerService.sendMail({
to,
@@ -70,8 +64,6 @@ export class MailerService {
to: string,
mailDesc: AdminUserInvitationMailDescription,
) {
if (this.configService.get('INFRA.MAILER_SMTP_ENABLE') !== 'true') return;
try {
const res = await this.nestMailerService.sendMail({
to,

View File

@@ -15,13 +15,12 @@ import {
TEAM_MEMBER_NOT_FOUND,
} from 'src/errors';
import { TeamInvitation } from './team-invitation.model';
import { MailerService } from 'src/mailer/mailer.service';
import { UserService } from 'src/user/user.service';
import { PubSubService } from 'src/pubsub/pubsub.service';
import { validateEmail } from '../utils';
import { AuthUser } from 'src/types/AuthUser';
import { ConfigService } from '@nestjs/config';
import { EventEmitter2 } from '@nestjs/event-emitter';
import { Events } from 'src/types/EventEmitter';
@Injectable()
export class TeamInvitationService {
@@ -29,9 +28,9 @@ export class TeamInvitationService {
private readonly prisma: PrismaService,
private readonly userService: UserService,
private readonly teamService: TeamService,
private readonly mailerService: MailerService,
private readonly pubsub: PubSubService,
private readonly configService: ConfigService,
private eventEmitter: EventEmitter2,
) {}
/**
@@ -148,17 +147,14 @@ export class TeamInvitationService {
},
});
this.eventEmitter.emit(Events.MAILER_SEND_EMAIL, {
to: inviteeEmail,
mailDesc: {
template: 'team-invitation',
variables: {
invitee: creator.displayName ?? 'A Hoppscotch User',
action_url: `${this.configService.get(
'VITE_BASE_URL',
)}/join-team?id=${dbInvitation.id}`,
invite_team_name: team.name,
},
await this.mailerService.sendEmail(inviteeEmail, {
template: 'team-invitation',
variables: {
invitee: creator.displayName ?? 'A Hoppscotch User',
action_url: `${this.configService.get('VITE_BASE_URL')}/join-team?id=${
dbInvitation.id
}`,
invite_team_name: team.name,
},
});

View File

@@ -1,4 +0,0 @@
export enum Events {
MAILER_SEND_EMAIL = 'mailer.sendEmail',
MAILER_SEND_USER_INVITATION_EMAIL = 'mailer.sendUserInvitationEmail',
}

View File

@@ -1,16 +1,7 @@
export enum InfraConfigEnum {
MAILER_SMTP_ENABLE = 'MAILER_SMTP_ENABLE',
MAILER_USE_CUSTOM_CONFIGS = 'MAILER_USE_CUSTOM_CONFIGS',
MAILER_SMTP_URL = 'MAILER_SMTP_URL',
MAILER_ADDRESS_FROM = 'MAILER_ADDRESS_FROM',
MAILER_SMTP_HOST = 'MAILER_SMTP_HOST',
MAILER_SMTP_PORT = 'MAILER_SMTP_PORT',
MAILER_SMTP_SECURE = 'MAILER_SMTP_SECURE',
MAILER_SMTP_USER = 'MAILER_SMTP_USER',
MAILER_SMTP_PASSWORD = 'MAILER_SMTP_PASSWORD',
MAILER_TLS_REJECT_UNAUTHORIZED = 'MAILER_TLS_REJECT_UNAUTHORIZED',
GOOGLE_CLIENT_ID = 'GOOGLE_CLIENT_ID',
GOOGLE_CLIENT_SECRET = 'GOOGLE_CLIENT_SECRET',
GOOGLE_CALLBACK_URL = 'GOOGLE_CALLBACK_URL',

View File

@@ -1,4 +1,4 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
"exclude": ["node_modules", "test", "dist", "**/*spec.ts", "prisma/seed.ts"]
}

File diff suppressed because it is too large Load Diff

View File

@@ -11,13 +11,12 @@
"connect": "Подключиться",
"connecting": "Соединение...",
"copy": "Скопировать",
"create": "Создать",
"create": "Create",
"delete": "Удалить",
"disconnect": "Отключиться",
"dismiss": "Скрыть",
"dont_save": "Не сохранять",
"download_file": "Скачать файл",
"download_here": "Download here",
"drag_to_reorder": "Перетягивайте для сортировки",
"duplicate": "Дублировать",
"edit": "Редактировать",
@@ -25,7 +24,6 @@
"go_back": "Вернуться",
"go_forward": "Вперёд",
"group_by": "Сгруппировать по",
"hide_secret": "Hide secret",
"label": "Название",
"learn_more": "Узнать больше",
"less": "Меньше",
@@ -35,7 +33,7 @@
"open_workspace": "Открыть пространство",
"paste": "Вставить",
"prettify": "Форматировать",
"properties": "Параметры",
"properties": "Properties",
"remove": "Удалить",
"rename": "Переименовать",
"restore": "Восстановить",
@@ -44,14 +42,13 @@
"scroll_to_top": "Вверх",
"search": "Поиск",
"send": "Отправить",
"share": "Поделиться",
"show_secret": "Show secret",
"share": "Share",
"start": "Начать",
"starting": "Запускаю",
"stop": "Стоп",
"to_close": "закрыть",
"to_close": "что бы закрыть",
"to_navigate": "для навигации",
"to_select": "выбрать",
"to_select": "выборать",
"turn_off": "Выключить",
"turn_on": "Включить",
"undo": "Отменить",
@@ -69,12 +66,12 @@
"copy_interface_type": "Copy interface type",
"copy_user_id": "Копировать токен пользователя",
"developer_option": "Настройки разработчика",
"developer_option_description": "Инструмент разработчика помогает обслуживать и развивать Hoppscotch",
"developer_option_description": "Инструмент разработчика помогает обслуживить и развивить Hoppscotch",
"discord": "Discord",
"documentation": "Документация",
"github": "GitHub",
"help": "Справка, отзывы и документация",
"home": "На главную",
"home": "Дом",
"invite": "Пригласить",
"invite_description": "В Hoppscotch мы разработали простой и интуитивно понятный интерфейс для создания и управления вашими API. Hoppscotch - это инструмент, который помогает создавать, тестировать, документировать и делиться своими API.",
"invite_your_friends": "Пригласить своих друзей",
@@ -88,7 +85,7 @@
"reload": "Перезагрузить",
"search": "Поиск",
"share": "Поделиться",
"shortcuts": "Горячие клавиши",
"shortcuts": "Ярлыки",
"social_description": "Подписывайся на наши соц. сети и оставайся всегда в курсе последних новостей, обновлений и релизов.",
"social_links": "Социальные сети",
"spotlight": "Прожектор",
@@ -99,19 +96,17 @@
"type_a_command_search": "Введите команду или выполните поиск…",
"we_use_cookies": "Мы используем куки",
"whats_new": "Что нового?",
"wiki": "Узнать больше"
"wiki": "Вики"
},
"auth": {
"account_exists": "Учетная запись существует с разными учетными данными - войдите, чтобы связать обе учетные записи",
"all_sign_in_options": "Все варианты входа",
"continue_with_auth_provider": "Continue with {provider}",
"continue_with_email": "Продолжить с электронной почтой",
"continue_with_github": "Продолжить с GitHub",
"continue_with_github_enterprise": "Continue with GitHub Enterprise",
"continue_with_google": "Продолжить с Google",
"continue_with_microsoft": "Продолжить с Microsoft",
"email": "Электронное письмо",
"logged_out": "Успешно вышли. Будем скучать!",
"logged_out": "Вышли из",
"login": "Авторизоваться",
"login_success": "Успешный вход в систему",
"login_to_hoppscotch": "Войти в Hoppscotch",
@@ -126,7 +121,7 @@
"generate_token": "Сгенерировать токен",
"graphql_headers": "Authorization Headers are sent as part of the payload to connection_init",
"include_in_url": "Добавить в URL",
"inherited_from": "Унаследован тип аутентификации {auth} из родительской коллекции {collection}",
"inherited_from": "Inherited {auth} from parent collection {collection} ",
"learn": "Узнать больше",
"oauth": {
"redirect_auth_server_returned_error": "Auth Server returned an error state",
@@ -140,32 +135,11 @@
"redirect_no_token_endpoint": "No Token Endpoint Defined",
"something_went_wrong_on_oauth_redirect": "Something went wrong during OAuth Redirect",
"something_went_wrong_on_token_generation": "Something went wrong on token generation",
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed",
"grant_type": "Grant Type",
"grant_type_auth_code": "Authorization Code",
"token_fetched_successfully": "Token fetched successfully",
"token_fetch_failed": "Failed to fetch token",
"validation_failed": "Validation Failed, please check the form fields",
"label_authorization_endpoint": "Authorization Endpoint",
"label_client_id": "Client ID",
"label_client_secret": "Client Secret",
"label_code_challenge": "Code Challenge",
"label_code_challenge_method": "Code Challenge Method",
"label_code_verifier": "Code Verifier",
"label_scopes": "Scopes",
"label_token_endpoint": "Token Endpoint",
"label_use_pkce": "Use PKCE",
"label_implicit": "Implicit",
"label_password": "Password",
"label_username": "Username",
"label_auth_code": "Authorization Code",
"label_client_credentials": "Client Credentials"
"token_generation_oidc_discovery_failed": "Failure on token generation: OpenID Connect Discovery Failed"
},
"pass_by_headers_label": "Headers",
"pass_by_query_params_label": "Query Parameters",
"pass_key_by": "Pass by",
"password": "Пароль",
"save_to_inherit": "Чтобы унаследовать аутентификации, нужно сохранить запрос в коллекции",
"save_to_inherit": "Please save this request in any collection to inherit the authorization",
"token": "Токен",
"type": "Метод авторизации",
"username": "Имя пользователя"
@@ -175,7 +149,6 @@
"different_parent": "Нельзя сортировать коллекцию с разной родительской коллекцией",
"edit": "Редактировать коллекцию",
"import_or_create": "Вы можете импортировать существующую или создать новую коллекцию",
"import_collection": "Импортировать коллекцию",
"invalid_name": "Укажите допустимое название коллекции",
"invalid_root_move": "Коллекция уже в корне",
"moved": "Перемещено успешно",
@@ -184,36 +157,38 @@
"name_length_insufficient": "Имя коллекции должно иметь 3 или более символов",
"new": "Создать коллекцию",
"order_changed": "Порядок коллекции обновлён",
"properties": "Параметры коллекции",
"properties_updated": "Параметры коллекции обновлены",
"properties": "Collection Properties",
"properties_updated": "Collection Properties Updated",
"renamed": "Коллекция переименована",
"request_in_use": "Запрос обрабатывается",
"save_as": "Сохранить как",
"save_to_collection": "Сохранить в коллекцию",
"select": "Выбрать коллекцию",
"select_location": "Выберите местоположение"
"select_location": "Выберите местоположение",
"select_team": "Выберите команду",
"team_collections": "Коллекции команд"
},
"confirm": {
"close_unsaved_tab": "Вы уверены, что хотите закрыть эту вкладку?",
"close_unsaved_tabs": "Вы уверены, что хотите закрыть все эти вкладки? Несохранённые данные {count} вкладок будут утеряны.",
"close_unsaved_tab": "Вы уверены что хотите закрыть эту вкладку?",
"close_unsaved_tabs": "Вы уверены что хотите закрыть все эти вкладки? Несохранённые данные {count} вкладок будут утеряны.",
"exit_team": "Вы точно хотите покинуть эту команду?",
"logout": "Вы действительно хотите выйти?",
"remove_collection": "Вы уверены, что хотите навсегда удалить эту коллекцию?",
"remove_environment": "Вы действительно хотите удалить это окружение без возможности восстановления?",
"remove_environment": "Вы действительно хотите удалить эту среду без возможности восстановления?",
"remove_folder": "Вы уверены, что хотите навсегда удалить эту папку?",
"remove_history": "Вы уверены, что хотите навсегда удалить всю историю?",
"remove_request": "Вы уверены, что хотите навсегда удалить этот запрос?",
"remove_shared_request": "Вы уверены, что хотите навсегда удалить этот запрос?",
"remove_shared_request": "Are you sure you want to permanently delete this shared request?",
"remove_team": "Вы уверены, что хотите удалить эту команду?",
"remove_telemetry": "Вы действительно хотите отказаться от телеметрии?",
"request_change": "Вы уверены, что хотите сбросить текущий запрос, все не сохранённые данные будт утеряны?",
"request_change": "Вы уверены что хотите сбросить текущий запрос, все не сохранённые данные будт утеряны?",
"save_unsaved_tab": "Вы хотите сохранить изменения в этой вкладке?",
"sync": "Вы уверены, что хотите синхронизировать это рабочее пространство?"
},
"context_menu": {
"add_parameters": "Добавить в список параметров",
"open_request_in_new_tab": "Открыть запрос в новом окне",
"set_environment_variable": "Добавить значение в переменную"
"add_parameters": "Add to parameters",
"open_request_in_new_tab": "Open request in new tab",
"set_environment_variable": "Set as variable"
},
"cookies": {
"modal": {
@@ -252,25 +227,24 @@
"collections": "Коллекции пустые",
"documentation": "Подключите GraphQL endpoint, чтобы увидеть документацию.",
"endpoint": "Endpoint не может быть пустым",
"environments": "Переменных окружения нет",
"environments": "Окружения пусты",
"folder": "Папка пуста",
"headers": "У этого запроса нет заголовков",
"history": "История пуста",
"invites": "Вы еще никого не приглашали",
"members": "В этой команде еще нет участников",
"parameters": "Этот запрос не содержит параметров",
"parameters": "Этот запрос не имеет параметров",
"pending_invites": "Пока что нет ожидающих заявок на вступление в команду",
"profile": "Войдите, чтобы просмотреть свой профиль",
"protocols": "Протоколы пустые",
"request_variables": "Этот запрос не содержит никаких переменных",
"secret_environments": "Секреты хранятся только на этом устройстве и не синхронизируются с сервером",
"schema": "Подключиться к конечной точке GraphQL",
"shared_requests": "Вы еще не делились запросами с другими",
"shared_requests_logout": "Нужно войти, чтобы делиться запросами и управлять ими",
"shared_requests": "Shared requests are empty",
"shared_requests_logout": "Login to view your shared requests or create a new one",
"subscription": "Нет подписок",
"team_name": "Название команды пусто",
"teams": "Команды пустые",
"tests": "Для этого запроса нет тестов"
"tests": "Для этого запроса нет тестов",
"shortcodes": "Нет коротких ссылок"
},
"environment": {
"add_to_global": "Добавить в глобальное окружение",
@@ -278,57 +252,53 @@
"create_new": "Создать новое окружение",
"created": "Окружение создано",
"deleted": "Окружение удалено",
"duplicated": "Окружение продублировано",
"duplicated": "Environment duplicated",
"edit": "Редактировать окружение",
"empty_variables": "Переменные еще не добавлены",
"empty_variables": "No variables",
"global": "Global",
"global_variables": "Глобальные переменные",
"global_variables": "Global variables",
"import_or_create": "Импортировать или создать новое окружение",
"invalid_name": "Укажите допустимое имя для окружения",
"list": "Переменные окружения",
"my_environments": "Мои окружения",
"name": "Имя",
"name": "Name",
"nested_overflow": "максимальный уровень вложения переменных окружения - 10",
"new": "Новая среда",
"no_active_environment": "Нет активных окружений",
"no_environment": "Нет окружения",
"no_environment_description": "Не выбрано окружение, выберите что делать с переменными.",
"quick_peek": "Быстрый просмотр переменных",
"quick_peek": "Environment Quick Peek",
"replace_with_variable": "Replace with variable",
"scope": "Scope",
"secrets": "Секретные переменные",
"secret_value": "Секретное значение",
"select": "Выберите среду",
"set": "Выбрать окружение",
"set_as_environment": "Поместить значение в переменную",
"set": "Set environment",
"set_as_environment": "Set as environment",
"team_environments": "Окружения команды",
"title": "Окружения",
"updated": "Окружение обновлено",
"value": "Значение",
"variable": "Переменная",
"variables": "Переменные",
"value": "Value",
"variable": "Variable",
"variable_list": "Список переменных"
},
"error": {
"authproviders_load_error": "Unable to load auth providers",
"browser_support_sse": "Похоже, в этом браузере нет поддержки событий, отправленных сервером.",
"check_console_details": "Подробности смотрите в журнале консоли.",
"check_how_to_add_origin": "Инструкция как это сделать",
"check_how_to_add_origin": "Инструкция как добавить origin в настройки расширения",
"curl_invalid_format": "cURL неправильно отформатирован",
"danger_zone": "Опасная зона",
"delete_account": "Вы являетесь владельцем этой команды:",
"delete_account_description": "Прежде чем удалить аккаунт вам необходимо либо назначить владельцом другого пользователя, либо удалить команды в которых вы являетесь владельцем.",
"empty_profile_name": "Имя пользователя не может быть пустым",
"empty_req_name": "Пустое имя запроса",
"f12_details": "(F12 для подробностей)",
"gql_prettify_invalid_query": "Не удалось отформатировать, т.к. в запросе есть синтаксические ошибки. Устраните их и повторите попытку.",
"gql_prettify_invalid_query": "Не удалось определить недопустимый запрос, устранить синтаксические ошибки запроса и повторить попытку.",
"incomplete_config_urls": "Не заполнены URL конфигурации",
"incorrect_email": "Не корректный Email",
"invalid_link": "Не корректная ссылка",
"invalid_link_description": "Ссылка, по которой вы перешли, - недействительна, либо срок ее действия истек.",
"invalid_embed_link": "The embed does not exist or is invalid.",
"json_parsing_failed": "Не корректный JSON",
"json_prettify_invalid_body": "Не удалось определить формат строки, устраните синтаксические ошибки и повторите попытку.",
"json_prettify_invalid_body": "Не удалось определить недопустимое тело, устранить синтаксические ошибки json и повторить попытку.",
"network_error": "Похоже, возникла проблема с соединением. Попробуйте еще раз.",
"network_fail": "Не удалось отправить запрос",
"no_collections_to_export": "Нечего экспортировать. Для начала нужно создать коллекцию.",
@@ -336,10 +306,8 @@
"no_environments_to_export": "Нечего экспортировать. Для начала нужно создать переменные окружения.",
"no_results_found": "Совпадения не найдены",
"page_not_found": "Эта страница не найдена",
"please_install_extension": "Ничего страшного. Просто нужно установить специальное расширение в браузере.",
"please_install_extension": "Нужно установить специальное расширение и добавить этот домен как новый origin в настройках расширения.",
"proxy_error": "Proxy error",
"reading_files": "Произошла ошибка при чтении файла или нескольких файлов",
"same_profile_name": "Задано имя пользователя такое же как и было",
"script_fail": "Не удалось выполнить сценарий предварительного запроса",
"something_went_wrong": "Что-то пошло не так",
"test_script_fail": "Не удалось выполнить тестирование запроса"
@@ -347,12 +315,13 @@
"export": {
"as_json": "Экспорт как JSON",
"create_secret_gist": "Создать секретный Gist",
"create_secret_gist_tooltip_text": "Экспортировать как секретный Gist",
"failed": "Произошла ошибка во время экспорта",
"secret_gist_success": "Успешно экспортировано как секретный Gist",
"create_secret_gist_tooltip_text": "Export as secret Gist",
"failed": "Something went wrong while exporting",
"secret_gist_success": "Successfully exported as secret Gist",
"require_github": "Войдите через GitHub, чтобы создать секретную суть",
"title": "Экспорт",
"success": "Успешно экспортировано"
"success": "Successfully exported",
"gist_created": "Gist создан"
},
"filter": {
"all": "Все",
@@ -377,7 +346,7 @@
"switch_connection": "Изменить соединение"
},
"graphql_collections": {
"title": "Коллекции GraphQL"
"title": "GraphQL Collections"
},
"group": {
"time": "Время",
@@ -390,8 +359,8 @@
},
"helpers": {
"authorization": "Заголовок авторизации будет автоматически сгенерирован при отправке запроса.",
"collection_properties_authorization": "Этот заголовок авторизации будет подставляться при каждом запросе в этой коллекции.",
"collection_properties_header": "Этот заголовок будет подставляться при каждом запросе в этой коллекции.",
"collection_properties_authorization": " This authorization will be set for every request in this collection.",
"collection_properties_header": "This header will be set for every request in this collection.",
"generate_documentation_first": "Сначала создайте документацию",
"network_fail": "Невозможно достичь конечной точки API. Проверьте подключение к сети и попробуйте еще раз.",
"offline": "Кажется, вы не в сети. Данные в этой рабочей области могут быть устаревшими.",
@@ -411,12 +380,10 @@
"import": {
"collections": "Импортировать коллекции",
"curl": "Импортировать из cURL",
"environments_from_gist": "Импортировать из Gist",
"environments_from_gist_description": "Импортировать переменные окружения Hoppscotch из Gist",
"environments_from_gist": "Import From Gist",
"environments_from_gist_description": "Import Hoppscotch Environments From Gist",
"failed": "Ошибка импорта",
"file_size_limit_exceeded_warning_multiple_files": "Выбранные файлы превышают рекомендованный лимит в 10MB. Были импортированы только первые {files}",
"file_size_limit_exceeded_warning_single_file": "Размер выбранного в данный момент файла превышает рекомендуемый лимит в 10 МБ. Пожалуйста, выберите другой файл.",
"from_file": "Импортировать из одного или нескольких файлов",
"from_file": "Import from File",
"from_gist": "Импорт из Gist",
"from_gist_description": "Импортировать через Gist URL",
"from_insomnia": "Импортировать с Insomnia",
@@ -431,9 +398,9 @@
"from_postman_description": "Импортировать из коллекции Postman",
"from_url": "Импортировать из URL",
"gist_url": "Введите URL-адрес Gist",
"gql_collections_from_gist_description": "Импортировать GraphQL коллекцию из Gist",
"gql_collections_from_gist_description": "Import GraphQL Collections From Gist",
"hoppscotch_environment": "Hoppscotch Environment",
"hoppscotch_environment_description": "Импортировать окружение Hoppscotch из JSON файла",
"hoppscotch_environment_description": "Import Hoppscotch Environment JSON file",
"import_from_url_invalid_fetch": "Не удалить получить данные по этому URL",
"import_from_url_invalid_file_format": "Ошибка при импорте коллекций",
"import_from_url_invalid_type": "Неподдерживаемый тип. Поддерживаемые типы: 'hoppscotch', 'openapi', 'postman', 'insomnia'",
@@ -442,19 +409,16 @@
"json_description": "Импортировать из коллекции Hoppscotch",
"postman_environment": "Postman Environment",
"postman_environment_description": "Import Postman Environment from a JSON file",
"success": "Успешно импортировано",
"title": "Импортировать"
},
"inspections": {
"description": "Показать возможные ошибки",
"description": "Inspect possible errors",
"environment": {
"add_environment": "Добавить переменную",
"add_environment_value": "Заполнить значение",
"empty_value": "Значение переменной окружения '{variable}' пустое",
"not_found": "Переменная окружения “{environment}” не задана."
"add_environment": "Add to Environment",
"not_found": "Environment variable “{environment}” not found."
},
"header": {
"cookie": "Из-за ограничений безопасности в веб версии нельзя задать Cookie параметры. Пожалуйста, используйте Hoppscotch Desktop приложение или используйте заголовок Authorization вместо этого."
"cookie": "The browser doesn't allow Hoppscotch to set the Cookie Header. While we're working on the Hoppscotch Desktop App (coming soon), please use the Authorization Header instead."
},
"response": {
"401_error": "Please check your authentication credentials.",
@@ -463,12 +427,12 @@
"default_error": "Please check your request.",
"network_error": "Please check your network connection."
},
"title": "Помощник",
"title": "Inspector",
"url": {
"extension_not_installed": "Расширение не установлено.",
"extension_unknown_origin": "Убедитесь, что текущий домен добавлен в список доверенных ресурсов в расширении браузера",
"extention_enable_action": "Подключить расширение",
"extention_not_enabled": "Расширение в браузере не подключено."
"extension_not_installed": "Extension not installed.",
"extension_unknown_origin": "Make sure you've added the API endpoint's origin to the Hoppscotch Browser Extension list.",
"extention_enable_action": "Enable Browser Extension",
"extention_not_enabled": "Extension not enabled."
}
},
"layout": {
@@ -481,11 +445,11 @@
"modal": {
"close_unsaved_tab": "У вас есть не сохранённые изменения",
"collections": "Коллекции",
"confirm": "Подтвердите действие",
"confirm": "Подтверждать",
"customize_request": "Customize Request",
"edit_request": "Изменить запрос",
"import_export": "Импорт Экспорт",
"share_request": "Поделиться запросом"
"share_request": "Share Request"
},
"mqtt": {
"already_subscribed": "Вы уже подписаны на этот топик",
@@ -521,7 +485,7 @@
"doc": "Документы",
"graphql": "GraphQL",
"profile": "Профиль",
"realtime": "Realtime",
"realtime": "В реальном времени",
"rest": "REST",
"settings": "Настройки"
},
@@ -543,8 +507,8 @@
"roles": "Роли",
"roles_description": "Роли позволяют настраивать доступ конкретным людям к публичным коллекциям.",
"updated": "Профиль обновлен",
"viewer": "Читатель",
"viewer_description": "Могут только просматривать и использовать запросы."
"viewer": "Зритель",
"viewer_description": "Зрительно могут только просматривать и использовать запросы."
},
"remove": {
"star": "Удалить звезду"
@@ -566,11 +530,11 @@
"enter_curl": "Введите сюда команду cURL",
"generate_code": "Сгенерировать код",
"generated_code": "Сгенерированный код",
"go_to_authorization_tab": "Перейти на вкладку авторизации",
"go_to_body_tab": "Перейти на вкладку тела запроса",
"go_to_authorization_tab": "Go to Authorization",
"go_to_body_tab": "Go to Body tab",
"header_list": "Список заголовков",
"invalid_name": "Укажите имя для запроса",
"method": "Метод",
"method": "Методика",
"moved": "Запрос перемещён",
"name": "Имя запроса",
"new": "Новый запрос",
@@ -584,22 +548,22 @@
"payload": "Полезная нагрузка",
"query": "Запрос",
"raw_body": "Необработанное тело запроса",
"rename": "Переименовать запрос",
"rename": "Переименость запрос",
"renamed": "Запрос переименован",
"request_variables": "Переменные запроса",
"run": "Запустить",
"save": "Сохранить",
"save_as": "Сохранить как",
"saved": "Запрос сохранен",
"share": "Поделиться",
"share": "Делиться",
"share_description": "Поделиться Hoppscotch с друзьями",
"share_request": "Поделиться запросом",
"stop": "Стоп",
"share_request": "Share Request",
"stop": "Stop",
"title": "Запрос",
"type": "Тип запроса",
"url": "URL",
"variables": "Переменные",
"view_my_links": "Посмотреть мои ссылки"
"view_my_links": "Посмотреть мои ссылки",
"copy_link": "Копировать ссылку"
},
"response": {
"audio": "Аудио",
@@ -622,7 +586,7 @@
},
"settings": {
"accent_color": "Основной цвет",
"account": "Аккаунт",
"account": "Счет",
"account_deleted": "Ваш аккаунт был удалён",
"account_description": "Настройте параметры своей учетной записи.",
"account_email_description": "Ваш основной адрес электронной почты.",
@@ -675,29 +639,29 @@
"verify_email": "Подтвердить Email"
},
"shared_requests": {
"button": "Кнопка",
"button_info": "Создать кнопку 'Run in Hoppscotch' на свой сайт, блог или README.",
"copy_html": "Копировать HTML код",
"copy_link": "Копировать ссылку",
"copy_markdown": "Копировать Markdown",
"creating_widget": "Создание виджет",
"customize": "Настроить",
"deleted": "Запрос удален",
"description": "Выберите вид как вы поделитесь запросом, позже вы сможете дополнительно его настроить",
"embed": "Встраиваемое окно",
"embed_info": "Добавьте небольшую площадку 'Hoppscotch API Playground' на свой веб-сайт, блог или документацию.",
"link": "Ссылка",
"link_info": "Создайте общедоступную ссылку, которой можно поделиться с любым пользователем, имеющим доступ к просмотру.",
"modified": "Запрос изменен",
"not_found": "Такой ссылке не нашлось",
"open_new_tab": "Открыть в новом окне",
"button": "Button",
"button_info": "Create a 'Run in Hoppscotch' button for your website, blog or a README.",
"copy_html": "Copy HTML",
"copy_link": "Copy Link",
"copy_markdown": "Copy Markdown",
"creating_widget": "Creating widget",
"customize": "Customize",
"deleted": "Shared request deleted",
"description": "Select a widget, you can change and customize this later",
"embed": "Embed",
"embed_info": "Add a mini 'Hoppscotch API Playground' to your website, blog or documentation.",
"link": "Link",
"link_info": "Create a shareable link to share with anyone on the internet with view access.",
"modified": "Shared request modified",
"not_found": "Shared request not found",
"open_new_tab": "Open in new tab",
"preview": "Preview",
"run_in_hoppscotch": "Run in Hoppscotch",
"theme": {
"dark": "Темная",
"light": "Светлая",
"system": "Системная",
"title": "Тема"
"dark": "Dark",
"light": "Light",
"system": "System",
"title": "Theme"
}
},
"shortcut": {
@@ -705,7 +669,7 @@
"close_current_menu": "Закрыть текущее меню",
"command_menu": "Меню поиска и команд",
"help_menu": "Меню помощи",
"show_all": "Список горячих клавиш",
"show_all": "Горячие клавиши",
"title": "Общий"
},
"miscellaneous": {
@@ -732,19 +696,20 @@
"get_method": "Выберите метод GET",
"head_method": "Выберите метод HEAD",
"import_curl": "Импортировать из cURL",
"method": "Метод",
"method": "Методика",
"next_method": "Выберите следующий метод",
"post_method": "Выберите метод POST",
"previous_method": "Выбрать предыдущий метод",
"put_method": "Выберите метод PUT",
"rename": "Переименовать запрос",
"reset_request": "Сбросить запрос",
"save_request": "Сохранить запрос",
"save_request": "Сохарнить запрос",
"save_to_collections": "Сохранить в коллекции",
"send_request": "Послать запрос",
"share_request": "Поделиться запросом",
"show_code": "Сгенерировать фрагмент кода из запроса",
"title": "Запрос"
"share_request": "Share Request",
"show_code": "Generate code snippet",
"title": "Запрос",
"copy_request_link": "Копировать ссылку на запрос"
},
"response": {
"copy": "Копировать запрос в буфер обмена",
@@ -752,11 +717,11 @@
"title": "Запрос"
},
"theme": {
"black": "Переключить на черный режим",
"dark": "Переключить на тёмный режим",
"light": "Переключить на светлый режим",
"system": "Переключить на тему, исходя из настроек системы",
"title": "Внешний вид"
"black": "Черный режим",
"dark": "Тёмный режим",
"light": "Светлый режим",
"system": "Определяется системой",
"title": "Тема"
}
},
"show": {
@@ -765,11 +730,6 @@
"more": "Показать больше",
"sidebar": "Показать боковую панель"
},
"site_protection": {
"error_fetching_site_protection_status": "Something Went Wrong While Fetching Site Protection Status",
"login_to_continue": "Login to continue",
"login_to_continue_description": "You need to be logged in to access this Hoppscotch Enterprise Instance."
},
"socketio": {
"communication": "Коммуникация",
"connection_not_authorized": "Это SocketIO соединение не использует какую-либо авторизацию.",
@@ -779,89 +739,82 @@
"url": "URL"
},
"spotlight": {
"change_language": "Изменить язык",
"change_language": "Change Language",
"environments": {
"delete": "Удалить текущее окружение",
"duplicate": "Дублировать текущее окружение",
"duplicate_global": "Дублировать глобальное окружение",
"edit": "Редактировать текущее окружение",
"edit_global": "Редактировать глобальное окружение",
"new": "Создать новое окружение",
"new_variable": "Создать новую переменную окружения",
"title": "Окружение"
"delete": "Delete current environment",
"duplicate": "Duplicate current environment",
"duplicate_global": "Duplicate global environment",
"edit": "Edit current environment",
"edit_global": "Edit global environment",
"new": "Create new environment",
"new_variable": "Create a new environment variable",
"title": "Environments"
},
"general": {
"chat": "Чат с поддержкой",
"help_menu": "Помощь",
"open_docs": "Почитать документацию",
"open_github": "Открыть GitHub репозиторий",
"open_keybindings": "Горячие клавиши",
"social": "Соц. сети",
"title": "Общее"
"chat": "Chat with support",
"help_menu": "Help and support",
"open_docs": "Read Documentation",
"open_github": "Open GitHub repository",
"open_keybindings": "Keyboard shortcuts",
"social": "Social",
"title": "General"
},
"graphql": {
"connect": "Подключиться к серверу",
"disconnect": "Отключиться от сервера"
"connect": "Connect to server",
"disconnect": "Disconnect from server"
},
"miscellaneous": {
"invite": "Пригласить друзей в Hoppscotch",
"title": "Другое"
},
"phrases": {
"create_environment": "Создать окружение",
"create_workspace": "Создать пространство",
"import_collections": "Импортировать коллекцию",
"share_request": "Поделиться запросом",
"try": "Попробовать"
"invite": "Invite your friends to Hoppscotch",
"title": "Miscellaneous"
},
"request": {
"save_as_new": "Сохранить как новый запрос",
"select_method": "Выбрать метод",
"switch_to": "Переключиться",
"tab_authorization": "На вкладку авторизации",
"tab_body": "На вкладку тела запроса",
"tab_headers": "На вкладку заголовков",
"tab_parameters": "На вкладку параметров",
"tab_pre_request_script": "На вкладку пред-скрипта запроса",
"tab_query": "На вкладку запроса",
"tab_tests": "На вкладку тестов",
"tab_variables": "На вкладку переменных запроса"
"save_as_new": "Save as new request",
"select_method": "Select method",
"switch_to": "Switch to",
"tab_authorization": "Authorization tab",
"tab_body": "Body tab",
"tab_headers": "Headers tab",
"tab_parameters": "Parameters tab",
"tab_pre_request_script": "Pre-request script tab",
"tab_query": "Query tab",
"tab_tests": "Tests tab",
"tab_variables": "Variables tab"
},
"response": {
"copy": "Копировать содержимое ответа",
"download": "Сказать содержимое ответа как файл",
"title": "Ответ запроса"
"copy": "Copy response",
"download": "Download response as file",
"title": "Response"
},
"section": {
"interceptor": "Перехватчик",
"interface": "Интерфейс",
"theme": "Внешний вид",
"user": "Пользователь"
"interceptor": "Interceptor",
"interface": "Interface",
"theme": "Theme",
"user": "User"
},
"settings": {
"change_interceptor": "Изменить перехватчик",
"change_language": "Изменить язык",
"change_interceptor": "Change Interceptor",
"change_language": "Change Language",
"theme": {
"black": "Черная",
"dark": "Темная",
"light": "Светлая",
"system": "Как задано в системе"
"black": "Black",
"dark": "Dark",
"light": "Light",
"system": "System preference"
}
},
"tab": {
"close_current": "Закрыть текущую вкладку",
"close_others": "Закрыть все вкладки",
"duplicate": "Продублировать текущую вкладку",
"new_tab": "Открыть в новой вкладке",
"title": "Вкладки"
"close_current": "Close current tab",
"close_others": "Close all other tabs",
"duplicate": "Duplicate current tab",
"new_tab": "Open a new tab",
"title": "Tabs"
},
"workspace": {
"delete": "Удалить текущую команду",
"edit": "Редактировать текущую команду",
"invite": "Пригласить людей в команду",
"new": "Создать новую команду",
"switch_to_personal": "Переключить на персональное пространство",
"title": "Команды"
"delete": "Delete current team",
"edit": "Edit current team",
"invite": "Invite people to team",
"new": "Create new team",
"switch_to_personal": "Switch to your personal workspace",
"title": "Teams"
}
},
"sse": {
@@ -871,7 +824,7 @@
},
"state": {
"bulk_mode": "Множественное редактирование",
"bulk_mode_placeholder": "Каждый параметр должен начинаться с новой строки\nКлючи и значения разделяются двоеточием\nИспользуйте # для комментария",
"bulk_mode_placeholder": "Каждый параметр должен начинаться с новой строки\nКлючи и значения разедляются двоеточием\nИспользуйте # для комментария",
"cleared": "Очищено",
"connected": "Связаны",
"connected_to": "Подключено к {name}",
@@ -890,20 +843,20 @@
"download_failed": "Download failed",
"download_started": "Скачивание началось",
"enabled": "Включено",
"file_imported": "Файл успешно импортирован",
"file_imported": "Файл импортирован",
"finished_in": "Завершено через {duration} мс",
"hide": "Скрыть",
"hide": "Hide",
"history_deleted": "История удалена",
"linewrap": "Обернуть линии",
"loading": "Загрузка...",
"message_received": "Сообщение: {message} получено по топику: {topic}",
"mqtt_subscription_failed": "Что-то пошло не так, при попытке подписаться на топик: {topic}",
"none": "Не задан",
"none": "Никто",
"nothing_found": "Ничего не найдено для",
"published_error": "Что-то пошло не так при попытке опубликовать сообщение в топик {topic}: {message}",
"published_message": "Опубликовано сообщение: {message} в топик: {topic}",
"reconnection_error": "Не удалось переподключиться",
"show": "Показать",
"show": "Show",
"subscribed_failed": "Не удалось подписаться на топик: {topic}",
"subscribed_success": "Успешно подписался на топик: {topic}",
"unsubscribed_failed": "Не удалось отписаться от топика: {topic}",
@@ -918,6 +871,7 @@
"forum": "Задавайте вопросы и получайте ответы",
"github": "Подпишитесь на нас на Github",
"shortcuts": "Просматривайте приложение быстрее",
"team": "Свяжитесь с командой",
"title": "Служба поддержки",
"twitter": "Следуйте за нами на Twitter"
},
@@ -928,7 +882,7 @@
"close_others": "Закрыть остальные вкладки",
"collections": "Коллекции",
"documentation": "Документация",
"duplicate": "Дублировать вкладку",
"duplicate": "Duplicate Tab",
"environments": "Окружения",
"headers": "Заголовки",
"history": "История",
@@ -938,8 +892,7 @@
"queries": "Запросы",
"query": "Запрос",
"schema": "Схема",
"shared_requests": "Запросы в общем доступе",
"share_tab_request": "Поделиться запросом",
"shared_requests": "Shared Requests",
"socketio": "Socket.IO",
"sse": "SSE",
"tests": "Тесты",
@@ -968,6 +921,7 @@
"invite_tooltip": "Пригласить людей в Ваше рабочее пространство",
"invited_to_team": "{owner} приглашает Вас присоединиться к команде {team}",
"join": "Приглашение принято",
"join_beta": "Присоединяйтесь к бета-программе, чтобы получить доступ к командам.",
"join_team": "Присоединиться к {team}",
"joined_team": "Вы присоединились к команде {team}",
"joined_team_description": "Теперь Вы участник этой команды",
@@ -996,7 +950,6 @@
"permissions": "Разрешения",
"same_target_destination": "Таже цель и конечная точка",
"saved": "Команда сохранена",
"search_title": "Team Requests",
"select_a_team": "Выбрать команду",
"success_invites": "Принятые приглашения",
"title": "Команды",
@@ -1028,8 +981,16 @@
"workspace": {
"change": "Изменить пространство",
"personal": "Моё пространство",
"other_workspaces": "Пространства",
"team": "Пространство команды",
"title": "Рабочие пространства"
},
"shortcodes": {
"actions": "Действия",
"created_on": "Создано",
"deleted": "Удалёна",
"method": "Метод",
"not_found": "Короткая ссылка не найдена",
"short_code": "Короткая ссылка",
"url": "URL"
}
}

View File

@@ -68,9 +68,9 @@
"developer_option": "Developer options",
"developer_option_description": "Developer tools which helps in development and maintenance of Hoppscotch.",
"discord": "Discord",
"documentation": "Dokümanlar",
"documentation": "Dökümanlar",
"github": "GitHub",
"help": "Yardım, geri bildirim ve dokümanlar",
"help": "Yardım, geri bildirim ve dökümanlar",
"home": "Ana sayfa",
"invite": "Davet et",
"invite_description": "Hoppscotch'ta API'lerinizi oluşturmak ve yönetmek için basit ve sezgisel bir arayüz tasarladık. Hoppscotch, API'lerinizi oluşturmanıza, test etmenize, belgelemenize ve paylaşmanıza yardımcı olan bir araçtır.",
@@ -225,7 +225,7 @@
"body": "Bu isteğin bir gövdesi yok",
"collection": "Koleksiyon boş",
"collections": "Koleksiyonlar boş",
"documentation": "Dokümanları görmek için GraphQL uç noktasını bağlayın",
"documentation": "Dökümanları görmek için GraphQL uç noktasını bağlayın",
"endpoint": "Uç nokta boş olamaz",
"environments": "Ortamlar boş",
"folder": "Klasör boş",
@@ -735,7 +735,7 @@
"url": "Bağlantı"
},
"spotlight": {
"change_language": "Dil Değiştir",
"change_language": "Change Language",
"environments": {
"delete": "Delete current environment",
"duplicate": "Duplicate current environment",
@@ -744,7 +744,7 @@
"edit_global": "Edit global environment",
"new": "Create new environment",
"new_variable": "Create a new environment variable",
"title": "Ortamlar"
"title": "Environments"
},
"general": {
"chat": "Chat with support",

View File

@@ -43,19 +43,12 @@
@click="invokeAction('modals.support.toggle')"
/>
</div>
<div
class="flex"
:class="{
'flex-row-reverse gap-2':
workspaceSelectorFlagEnabled && !currentUser,
}"
>
<div class="flex">
<div
v-if="currentUser === null"
class="inline-flex items-center space-x-2"
>
<HoppButtonSecondary
v-if="!workspaceSelectorFlagEnabled"
:icon="IconUploadCloud"
:label="t('header.save_workspace')"
class="!focus-visible:text-emerald-600 !hover:text-emerald-600 hidden h-8 border border-emerald-600/25 bg-emerald-500/10 !text-emerald-500 hover:border-emerald-600/20 hover:bg-emerald-600/20 focus-visible:border-emerald-600/20 focus-visible:bg-emerald-600/20 md:flex"
@@ -67,22 +60,18 @@
@click="invokeAction('modals.login.toggle')"
/>
</div>
<TeamsMemberStack
v-else-if="
currentUser !== null &&
workspace.type === 'team' &&
selectedTeam &&
selectedTeam.teamMembers.length > 1
"
:team-members="selectedTeam.teamMembers"
show-count
class="mx-2"
@handle-click="handleTeamEdit()"
/>
<div
v-if="workspaceSelectorFlagEnabled || currentUser"
class="inline-flex items-center space-x-2"
>
<div v-else class="inline-flex items-center space-x-2">
<TeamsMemberStack
v-if="
workspace.type === 'team' &&
selectedTeam &&
selectedTeam.teamMembers.length > 1
"
:team-members="selectedTeam.teamMembers"
show-count
class="mx-2"
@handle-click="handleTeamEdit()"
/>
<div
class="flex h-8 divide-x divide-emerald-600/25 rounded border border-emerald-600/25 bg-emerald-500/10 focus-within:divide-emerald-600/20 focus-within:border-emerald-600/20 focus-within:bg-emerald-600/20 hover:divide-emerald-600/20 hover:border-emerald-600/20 hover:bg-emerald-600/20"
>
@@ -95,7 +84,6 @@
/>
<HoppButtonSecondary
v-if="
currentUser &&
workspace.type === 'team' &&
selectedTeam &&
selectedTeam?.myRole === 'OWNER'
@@ -136,7 +124,7 @@
</div>
</template>
</tippy>
<span v-if="currentUser" class="px-2">
<span class="px-2">
<tippy
interactive
trigger="click"
@@ -271,13 +259,6 @@ import {
const t = useI18n()
const toast = useToast()
/**
* Feature flag to enable the workspace selector login conversion
*/
const workspaceSelectorFlagEnabled = computed(
() => !!platform.platformFeatureFlags.workspaceSwitcherLogin?.value
)
/**
* Once the PWA code is initialized, this holds a method
* that can be called to show the user the installation
@@ -399,8 +380,6 @@ const inviteTeam = (team: { name: string }, teamID: string) => {
// Show the workspace selected team invite modal if the user is an owner of the team else show the default invite modal
const handleInvite = () => {
if (!currentUser.value) return invokeAction("modals.login.toggle")
if (
workspace.value.type === "team" &&
workspace.value.teamID &&

View File

@@ -32,6 +32,7 @@ import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { appendRESTCollections, restCollections$ } from "~/newstore/collections"
import MyCollectionImport from "~/components/importExport/ImportExportSteps/MyCollectionImport.vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import IconFolderPlus from "~icons/lucide/folder-plus"
import IconOpenAPI from "~icons/lucide/file"
@@ -54,15 +55,16 @@ import { teamCollectionsExporter } from "~/helpers/import-export/export/teamColl
import { GistSource } from "~/helpers/import-export/import/import-sources/GistSource"
import { ImporterOrExporter } from "~/components/importExport/types"
import { TeamWorkspace } from "~/services/workspace.service"
const t = useI18n()
const toast = useToast()
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType =
| {
type: "team-collections"
selectedTeam: TeamWorkspace
selectedTeam: SelectedTeam
}
| { type: "my-collections" }
@@ -431,7 +433,7 @@ const HoppTeamCollectionsExporter: ImporterOrExporter = {
props.collectionsType.selectedTeam
) {
const res = await teamCollectionsExporter(
props.collectionsType.selectedTeam.teamID
props.collectionsType.selectedTeam.id
)
if (E.isRight(res)) {
@@ -567,8 +569,8 @@ const hasTeamWriteAccess = computed(() => {
}
return (
collectionsType.selectedTeam.role === "EDITOR" ||
collectionsType.selectedTeam.role === "OWNER"
collectionsType.selectedTeam.myRole === "EDITOR" ||
collectionsType.selectedTeam.myRole === "OWNER"
)
})
@@ -576,17 +578,17 @@ const selectedTeamID = computed(() => {
const { collectionsType } = props
return collectionsType.type === "team-collections"
? collectionsType.selectedTeam?.teamID
? collectionsType.selectedTeam?.id
: undefined
})
const getCollectionJSON = async () => {
if (
props.collectionsType.type === "team-collections" &&
props.collectionsType.selectedTeam?.teamID
props.collectionsType.selectedTeam?.id
) {
const res = await getTeamCollectionJSON(
props.collectionsType.selectedTeam?.teamID
props.collectionsType.selectedTeam?.id
)
return E.isRight(res)

View File

@@ -56,25 +56,23 @@
</template>
<script setup lang="ts">
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { computed, nextTick, reactive, ref, watch } from "vue"
import { cloneDeep } from "lodash-es"
import {
HoppGQLRequest,
HoppRESTRequest,
isHoppRESTRequest,
} from "@hoppscotch/data"
import { computedWithControl } from "@vueuse/core"
import { useService } from "dioc/vue"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { cloneDeep } from "lodash-es"
import { computed, nextTick, reactive, ref, watch } from "vue"
import { GQLError } from "~/helpers/backend/GQLClient"
import * as TE from "fp-ts/TaskEither"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import {
createRequestInCollection,
updateTeamRequest,
} from "~/helpers/backend/mutations/TeamRequest"
import { Picked } from "~/helpers/types/HoppPicked"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import {
cascadeParentCollectionForHeaderAuth,
editGraphqlRequest,
@@ -82,10 +80,12 @@ import {
saveGraphqlRequestAs,
saveRESTRequestAs,
} from "~/newstore/collections"
import { GQLError } from "~/helpers/backend/GQLClient"
import { computedWithControl } from "@vueuse/core"
import { platform } from "~/platform"
import { GQLTabService } from "~/services/tab/graphql"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { TeamWorkspace } from "~/services/workspace.service"
import { GQLTabService } from "~/services/tab/graphql"
const t = useI18n()
const toast = useToast()
@@ -93,10 +93,12 @@ const toast = useToast()
const RESTTabs = useService(RESTTabService)
const GQLTabs = useService(GQLTabService)
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType =
| {
type: "team-collections"
selectedTeam: TeamWorkspace
selectedTeam: SelectedTeam
}
| { type: "my-collections"; selectedTeam: undefined }
@@ -190,7 +192,7 @@ watch(
}
)
const updateTeam = (newTeam: TeamWorkspace) => {
const updateTeam = (newTeam: SelectedTeam) => {
collectionsType.value.selectedTeam = newTeam
}
@@ -491,7 +493,7 @@ const updateTeamCollectionOrFolder = (
const data = {
title: requestUpdated.name,
request: JSON.stringify(requestUpdated),
teamID: collectionsType.value.selectedTeam.teamID,
teamID: collectionsType.value.selectedTeam.id,
}
pipe(
createRequestInCollection(collectionID, data),

View File

@@ -387,6 +387,7 @@ import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down"
import { computed, PropType, Ref, toRef } from "vue"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useI18n } from "@composables/i18n"
import { useColorMode } from "@composables/theming"
import { TeamCollection } from "~/helpers/teams/TeamCollection"
@@ -399,16 +400,17 @@ import * as O from "fp-ts/Option"
import { Picked } from "~/helpers/types/HoppPicked.js"
import { RESTTabService } from "~/services/tab/rest"
import { useService } from "dioc/vue"
import { TeamWorkspace } from "~/services/workspace.service"
const t = useI18n()
const colorMode = useColorMode()
const tabs = useService(RESTTabService)
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType =
| {
type: "team-collections"
selectedTeam: TeamWorkspace
selectedTeam: SelectedTeam
}
| { type: "my-collections"; selectedTeam: undefined }
@@ -612,7 +614,7 @@ const hasNoTeamAccess = computed(
() =>
props.collectionsType.type === "team-collections" &&
(props.collectionsType.selectedTeam === undefined ||
props.collectionsType.selectedTeam.role === "VIEWER")
props.collectionsType.selectedTeam.myRole === "VIEWER")
)
const isSelected = ({

View File

@@ -200,7 +200,7 @@ const toast = useToast()
defineProps<{
// Whether to activate the ability to pick items (activates 'select' events)
saveRequest: boolean
picked: Picked | null
picked: Picked
}>()
const collections = useReadonlyStream(graphqlCollections$, [], "deep")

View File

@@ -178,6 +178,7 @@ import { useI18n } from "@composables/i18n"
import { Picked } from "~/helpers/types/HoppPicked"
import { useReadonlyStream } from "~/composables/stream"
import { useLocalState } from "~/newstore/localstate"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import {
@@ -244,7 +245,7 @@ import {
} from "~/helpers/collection/collection"
import { currentReorderingStatus$ } from "~/newstore/reordering"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { TeamWorkspace, WorkspaceService } from "~/services/workspace.service"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
import { RESTTabService } from "~/services/tab/rest"
import { HoppInheritedProperty } from "~/helpers/types/HoppInheritedProperties"
@@ -273,14 +274,16 @@ const props = defineProps({
const emit = defineEmits<{
(event: "select", payload: Picked | null): void
(event: "update-team", team: TeamWorkspace): void
(event: "update-team", team: SelectedTeam): void
(event: "update-collection-type", type: CollectionType["type"]): void
}>()
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type CollectionType =
| {
type: "team-collections"
selectedTeam: TeamWorkspace
selectedTeam: SelectedTeam
}
| { type: "my-collections"; selectedTeam: undefined }
@@ -327,7 +330,9 @@ const requestMoveLoading = ref<string[]>([])
// TeamList-Adapter
const workspaceService = useService(WorkspaceService)
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
const teamListFetched = ref(false)
// Team Collection Adapter
const teamCollectionAdapter = new TeamCollectionAdapter(null)
@@ -373,7 +378,7 @@ watch(
filterTexts,
(newFilterText) => {
if (collectionsType.value.type === "team-collections") {
const selectedTeamID = collectionsType.value.selectedTeam?.teamID
const selectedTeamID = collectionsType.value.selectedTeam?.id
selectedTeamID &&
debouncedSearch(newFilterText, selectedTeamID)?.catch(() => {})
@@ -430,6 +435,28 @@ onMounted(() => {
}
})
watch(
() => myTeams.value,
(newTeams) => {
if (newTeams && !teamListFetched.value) {
teamListFetched.value = true
if (REMEMBERED_TEAM_ID.value && currentUser.value) {
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
if (team) updateSelectedTeam(team)
}
}
}
)
watch(
() => collectionsType.value.selectedTeam,
(newTeam) => {
if (newTeam) {
teamCollectionAdapter.changeTeamID(newTeam.id)
}
}
)
const switchToMyCollections = () => {
collectionsType.value.type = "my-collections"
collectionsType.value.selectedTeam = undefined
@@ -461,12 +488,11 @@ const expandTeamCollection = (collectionID: string) => {
teamCollectionAdapter.expandCollection(collectionID)
}
const updateSelectedTeam = (team: TeamWorkspace) => {
const updateSelectedTeam = (team: SelectedTeam) => {
if (team) {
collectionsType.value.type = "team-collections"
teamCollectionAdapter.changeTeamID(team.teamID)
collectionsType.value.selectedTeam = team
REMEMBERED_TEAM_ID.value = team.teamID
REMEMBERED_TEAM_ID.value = team.id
emit("update-team", team)
emit("update-collection-type", "team-collections")
}
@@ -475,14 +501,23 @@ const updateSelectedTeam = (team: TeamWorkspace) => {
const workspace = workspaceService.currentWorkspace
// Used to switch collection type and team when user switch workspace in the global workspace switcher
// Check if there is a teamID in the workspace, if yes, switch to team collections and select the team
// If there is no teamID, switch to my collections
watch(
workspace,
(newWorkspace) => {
if (newWorkspace.type === "personal") {
switchToMyCollections()
} else if (newWorkspace.type === "team") {
updateSelectedTeam(newWorkspace)
() => {
const space = workspace.value
return space.type === "personal" ? undefined : space.teamID
},
(teamID) => {
if (teamID) {
const team = myTeams.value?.find((t) => t.id === teamID)
if (team) {
updateSelectedTeam(team)
}
return
}
return switchToMyCollections()
},
{
immediate: true,
@@ -510,7 +545,7 @@ const hasTeamWriteAccess = computed(() => {
return false
}
const role = collectionsType.value.selectedTeam?.role
const role = collectionsType.value.selectedTeam?.myRole
return role === "OWNER" || role === "EDITOR"
})
@@ -725,7 +760,7 @@ const addNewRootCollection = (name: string) => {
})
pipe(
createNewRootCollection(name, collectionsType.value.selectedTeam.teamID),
createNewRootCollection(name, collectionsType.value.selectedTeam.id),
TE.match(
(err: GQLError<string>) => {
toast.error(`${getErrorMessage(err)}`)
@@ -796,7 +831,7 @@ const onAddRequest = (requestName: string) => {
const data = {
request: JSON.stringify(newRequest),
teamID: collectionsType.value.selectedTeam.teamID,
teamID: collectionsType.value.selectedTeam.id,
title: requestName,
}
@@ -1123,7 +1158,7 @@ const duplicateRequest = (payload: {
const data = {
request: JSON.stringify(newRequest),
teamID: collectionsType.value.selectedTeam.teamID,
teamID: collectionsType.value.selectedTeam.id,
title: `${request.name} - ${t("action.duplicate")}`,
}

View File

@@ -364,7 +364,6 @@ const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
teamID: team.id,
teamName: team.name,
type: "team",
role: team.myRole,
})
}
watch(

View File

@@ -46,38 +46,41 @@
</template>
<script setup lang="ts">
import { useReadonlyStream, useStream } from "@composables/stream"
import { Environment } from "@hoppscotch/data"
import { useService } from "dioc/vue"
import * as TE from "fp-ts/TaskEither"
import { pipe } from "fp-ts/function"
import { isEqual } from "lodash-es"
import { computed, ref, watch } from "vue"
import { isEqual } from "lodash-es"
import { platform } from "~/platform"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import { useReadonlyStream, useStream } from "@composables/stream"
import { useI18n } from "~/composables/i18n"
import { useToast } from "~/composables/toast"
import { defineActionHandler } from "~/helpers/actions"
import { GQLError } from "~/helpers/backend/GQLClient"
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import {
deleteEnvironment,
getSelectedEnvironmentIndex,
globalEnv$,
selectedEnvironmentIndex$,
setSelectedEnvironmentIndex,
} from "~/newstore/environments"
import TeamEnvironmentAdapter from "~/helpers/teams/TeamEnvironmentAdapter"
import { defineActionHandler } from "~/helpers/actions"
import { useLocalState } from "~/newstore/localstate"
import { platform } from "~/platform"
import { TeamWorkspace, WorkspaceService } from "~/services/workspace.service"
import { pipe } from "fp-ts/function"
import * as TE from "fp-ts/TaskEither"
import { GQLError } from "~/helpers/backend/GQLClient"
import { deleteEnvironment } from "~/newstore/environments"
import { deleteTeamEnvironment } from "~/helpers/backend/mutations/TeamEnvironment"
import { useToast } from "~/composables/toast"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
import { Environment } from "@hoppscotch/data"
const t = useI18n()
const toast = useToast()
type EnvironmentType = "my-environments" | "team-environments"
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
type EnvironmentsChooseType = {
type: EnvironmentType
selectedTeam: TeamWorkspace | undefined
selectedTeam: SelectedTeam
}
const environmentType = ref<EnvironmentsChooseType>({
@@ -99,7 +102,11 @@ const currentUser = useReadonlyStream(
platform.auth.getCurrentUser()
)
// TeamList-Adapter
const workspaceService = useService(WorkspaceService)
const teamListAdapter = workspaceService.acquireTeamListAdapter(null)
const myTeams = useReadonlyStream(teamListAdapter.teamList$, null)
const teamListFetched = ref(false)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
const adapter = new TeamEnvironmentAdapter(undefined)
@@ -111,17 +118,29 @@ const loading = computed(
() => adapterLoading.value && teamEnvironmentList.value.length === 0
)
watch(
() => myTeams.value,
(newTeams) => {
if (newTeams && !teamListFetched.value) {
teamListFetched.value = true
if (REMEMBERED_TEAM_ID.value && currentUser.value) {
const team = newTeams.find((t) => t.id === REMEMBERED_TEAM_ID.value)
if (team) updateSelectedTeam(team)
}
}
}
)
const switchToMyEnvironments = () => {
environmentType.value.selectedTeam = undefined
updateEnvironmentType("my-environments")
adapter.changeTeamID(undefined)
}
const updateSelectedTeam = (newSelectedTeam: TeamWorkspace | undefined) => {
const updateSelectedTeam = (newSelectedTeam: SelectedTeam | undefined) => {
if (newSelectedTeam) {
adapter.changeTeamID(newSelectedTeam.teamID)
environmentType.value.selectedTeam = newSelectedTeam
REMEMBERED_TEAM_ID.value = newSelectedTeam.teamID
REMEMBERED_TEAM_ID.value = newSelectedTeam.id
updateEnvironmentType("team-environments")
}
}
@@ -129,6 +148,15 @@ const updateEnvironmentType = (newEnvironmentType: EnvironmentType) => {
environmentType.value.type = newEnvironmentType
}
watch(
() => environmentType.value.selectedTeam,
(newTeam) => {
if (newTeam) {
adapter.changeTeamID(newTeam.id)
}
}
)
const workspace = workspaceService.currentWorkspace
// Switch to my environments if workspace is personal and to team environments if workspace is team
@@ -142,7 +170,8 @@ watch(workspace, (newWorkspace) => {
})
}
} else if (newWorkspace.type === "team") {
updateSelectedTeam(newWorkspace)
const team = myTeams.value?.find((t) => t.id === newWorkspace.teamID)
updateSelectedTeam(team)
}
})

View File

@@ -4,7 +4,7 @@
class="sticky top-upperPrimaryStickyFold z-10 flex flex-1 flex-shrink-0 justify-between overflow-x-auto border-b border-dividerLight bg-primary"
>
<HoppButtonSecondary
v-if="team === undefined || team.role === 'VIEWER'"
v-if="team === undefined || team.myRole === 'VIEWER'"
v-tippy="{ theme: 'tooltip' }"
disabled
class="!rounded-none"
@@ -28,7 +28,7 @@
:icon="IconHelpCircle"
/>
<HoppButtonSecondary
v-if="team !== undefined && team.role === 'VIEWER'"
v-if="team !== undefined && team.myRole === 'VIEWER'"
v-tippy="{ theme: 'tooltip' }"
disabled
:icon="IconImport"
@@ -84,7 +84,7 @@
)"
:key="`environment-${index}`"
:environment="environment"
:is-viewer="team?.role === 'VIEWER'"
:is-viewer="team?.myRole === 'VIEWER'"
@edit-environment="editEnvironment(environment)"
/>
</div>
@@ -103,16 +103,16 @@
:show="showModalDetails"
:action="action"
:editing-environment="editingEnvironment"
:editing-team-id="team?.teamID"
:editing-team-id="team?.id"
:editing-variable-name="editingVariableName"
:is-secret-option-selected="secretOptionSelected"
:is-viewer="team?.role === 'VIEWER'"
:is-viewer="team?.myRole === 'VIEWER'"
@hide-modal="displayModalEdit(false)"
/>
<EnvironmentsImportExport
v-if="showModalImportExport"
:team-environments="teamEnvironments"
:team-id="team?.teamID"
:team-id="team?.id"
environment-type="TEAM_ENV"
@hide-modal="displayModalImportExport(false)"
/>
@@ -129,14 +129,16 @@ import IconPlus from "~icons/lucide/plus"
import IconHelpCircle from "~icons/lucide/help-circle"
import IconImport from "~icons/lucide/folder-down"
import { defineActionHandler } from "~/helpers/actions"
import { TeamWorkspace } from "~/services/workspace.service"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
const t = useI18n()
const colorMode = useColorMode()
type SelectedTeam = GetMyTeamsQuery["myTeams"][number] | undefined
const props = defineProps<{
team: TeamWorkspace | undefined
team: SelectedTeam
teamEnvironments: TeamEnvironment[]
adapterError: GQLError<string> | null
loading: boolean
@@ -149,7 +151,7 @@ const editingEnvironment = ref<TeamEnvironment | null>(null)
const editingVariableName = ref("")
const secretOptionSelected = ref(false)
const isTeamViewer = computed(() => props.team?.role === "VIEWER")
const isTeamViewer = computed(() => props.team?.myRole === "VIEWER")
const displayModalAdd = (shouldDisplay: boolean) => {
action.value = "new"

View File

@@ -37,17 +37,13 @@ import { TeamNameCodec } from "~/helpers/backend/types/TeamName"
import { useI18n } from "@composables/i18n"
import { useToast } from "@composables/toast"
import { platform } from "~/platform"
import { useService } from "dioc/vue"
import { WorkspaceService } from "~/services/workspace.service"
import { useLocalState } from "~/newstore/localstate"
const t = useI18n()
const toast = useToast()
const props = defineProps<{
defineProps<{
show: boolean
switchWorkspaceAfterCreation?: boolean
}>()
const emit = defineEmits<{
@@ -56,12 +52,8 @@ const emit = defineEmits<{
const editingName = ref<string | null>(null)
const REMEMBERED_TEAM_ID = useLocalState("REMEMBERED_TEAM_ID")
const isLoading = ref(false)
const workspaceService = useService(WorkspaceService)
const addNewTeam = async () => {
isLoading.value = true
await pipe(
@@ -84,19 +76,8 @@ const addNewTeam = async () => {
// Handle GQL errors (use err obj)
}
},
(team) => {
() => {
toast.success(`${t("team.new_created")}`)
if (props.switchWorkspaceAfterCreation) {
REMEMBERED_TEAM_ID.value = team.id
workspaceService.changeWorkspace({
teamID: team.id,
teamName: team.name,
type: "team",
role: team.myRole,
})
}
hideModal()
}
)

View File

@@ -59,18 +59,14 @@
/>
</div>
<div
v-else-if="teamListAdapterError"
v-if="!loading && teamListAdapterError"
class="flex flex-col items-center py-4"
>
<icon-lucide-help-circle class="svg-icons mb-4" />
{{ t("error.something_went_wrong") }}
</div>
</div>
<TeamsAdd
:show="showModalAdd"
:switch-workspace-after-creation="true"
@hide-modal="displayModalAdd(false)"
/>
<TeamsAdd :show="showModalAdd" @hide-modal="displayModalAdd(false)" />
</div>
</template>
<script setup lang="ts">
@@ -85,7 +81,7 @@ import { useColorMode } from "@composables/theming"
import { GetMyTeamsQuery } from "~/helpers/backend/graphql"
import IconDone from "~icons/lucide/check"
import { useLocalState } from "~/newstore/localstate"
import { defineActionHandler, invokeAction } from "~/helpers/actions"
import { defineActionHandler } from "~/helpers/actions"
import { WorkspaceService } from "~/services/workspace.service"
import { useService } from "dioc/vue"
import { useElementVisibility, useIntervalFn } from "@vueuse/core"
@@ -158,7 +154,6 @@ const switchToTeamWorkspace = (team: GetMyTeamsQuery["myTeams"][number]) => {
teamID: team.id,
teamName: team.name,
type: "team",
role: team.myRole,
})
}
@@ -174,14 +169,11 @@ watch(
(user) => {
if (!user) {
switchToPersonalWorkspace()
teamListadapter.dispose()
}
}
)
const displayModalAdd = (shouldDisplay: boolean) => {
if (!currentUser.value) return invokeAction("modals.login.toggle")
showModalAdd.value = shouldDisplay
teamListadapter.fetchList()
}

View File

@@ -50,7 +50,6 @@ export default class TeamListAdapter {
}
public dispose() {
this.teamList$.next([])
this.isDispose = true
clearTimeout(this.timeoutHandle as any)
this.timeoutHandle = null

View File

@@ -1,10 +1,9 @@
import { pluck, distinctUntilChanged } from "rxjs/operators"
import { cloneDeep, defaultsDeep, has } from "lodash-es"
import { Observable } from "rxjs"
import { distinctUntilChanged, pluck } from "rxjs/operators"
import { nextTick } from "vue"
import { platform } from "~/platform"
import type { KeysMatching } from "~/types/ts-utils"
import DispatchingStore, { defineDispatchers } from "./DispatchingStore"
import type { KeysMatching } from "~/types/ts-utils"
export const HoppBgColors = ["system", "light", "dark", "black"] as const
@@ -70,63 +69,51 @@ export type SettingsDef = {
HAS_OPENED_SPOTLIGHT: boolean
}
export const getDefaultSettings = (): SettingsDef => {
const defaultSettings: SettingsDef = {
syncCollections: true,
syncHistory: true,
syncEnvironments: true,
export const getDefaultSettings = (): SettingsDef => ({
syncCollections: true,
syncHistory: true,
syncEnvironments: true,
WRAP_LINES: {
httpRequestBody: true,
httpResponseBody: true,
httpHeaders: true,
httpParams: true,
httpUrlEncoded: true,
httpPreRequest: true,
httpTest: true,
httpRequestVariables: true,
graphqlQuery: true,
graphqlResponseBody: true,
graphqlHeaders: false,
graphqlVariables: false,
graphqlSchema: true,
importCurl: true,
codeGen: true,
cookie: true,
},
WRAP_LINES: {
httpRequestBody: true,
httpResponseBody: true,
httpHeaders: true,
httpParams: true,
httpUrlEncoded: true,
httpPreRequest: true,
httpTest: true,
httpRequestVariables: true,
graphqlQuery: true,
graphqlResponseBody: true,
graphqlHeaders: false,
graphqlVariables: false,
graphqlSchema: true,
importCurl: true,
codeGen: true,
cookie: true,
},
CURRENT_INTERCEPTOR_ID: "",
CURRENT_INTERCEPTOR_ID: "browser", // TODO: Allow the platform definition to take this place
// TODO: Interceptor related settings should move under the interceptor systems
PROXY_URL: "https://proxy.hoppscotch.io/",
URL_EXCLUDES: {
auth: true,
httpUser: true,
httpPassword: true,
bearerToken: true,
oauth2Token: true,
},
THEME_COLOR: "indigo",
BG_COLOR: "system",
TELEMETRY_ENABLED: true,
EXPAND_NAVIGATION: false,
SIDEBAR: true,
SIDEBAR_ON_LEFT: false,
COLUMN_LAYOUT: true,
// TODO: Interceptor related settings should move under the interceptor systems
PROXY_URL: "https://proxy.hoppscotch.io/",
URL_EXCLUDES: {
auth: true,
httpUser: true,
httpPassword: true,
bearerToken: true,
oauth2Token: true,
},
THEME_COLOR: "indigo",
BG_COLOR: "system",
TELEMETRY_ENABLED: true,
EXPAND_NAVIGATION: false,
SIDEBAR: true,
SIDEBAR_ON_LEFT: false,
COLUMN_LAYOUT: true,
HAS_OPENED_SPOTLIGHT: false,
}
// Wait for platform to initialize before setting CURRENT_INTERCEPTOR_ID
nextTick(() => {
applySetting(
"CURRENT_INTERCEPTOR_ID",
platform?.interceptors.default || "browser"
)
})
return defaultSettings
}
HAS_OPENED_SPOTLIGHT: false,
})
type ApplySettingPayload = {
[K in keyof SettingsDef]: {

View File

@@ -64,6 +64,13 @@
@submit="renameReqName"
@hide-modal="showRenamingReqNameModal = false"
/>
<HoppSmartConfirmModal
:show="confirmingCloseForTabID !== null"
:confirm="t('modal.close_unsaved_tab')"
:title="t('confirm.save_unsaved_tab')"
@hide-modal="onCloseConfirmSaveTab"
@resolve="onResolveConfirmSaveTab"
/>
<HoppSmartConfirmModal
:show="confirmingCloseAllTabs"
:confirm="t('modal.close_unsaved_tab')"
@@ -71,36 +78,6 @@
@hide-modal="confirmingCloseAllTabs = false"
@resolve="onResolveConfirmCloseAllTabs"
/>
<HoppSmartModal
v-if="confirmingCloseForTabID !== null"
dialog
role="dialog"
aria-modal="true"
:title="t('modal.close_unsaved_tab')"
@close="confirmingCloseForTabID = null"
>
<template #body>
<div class="text-center">
{{ t("confirm.save_unsaved_tab") }}
</div>
</template>
<template #footer>
<span class="flex space-x-2">
<HoppButtonPrimary
v-focus
:label="t?.('action.yes')"
outline
@click="onResolveConfirmSaveTab"
/>
<HoppButtonSecondary
:label="t?.('action.no')"
filled
outline
@click="onCloseConfirmSaveTab"
/>
</span>
</template>
</HoppSmartModal>
<CollectionsSaveRequest
v-if="savingRequest"
mode="rest"

View File

@@ -11,7 +11,6 @@ import { InspectorsPlatformDef } from "./inspectors"
import { ServiceClassInstance } from "dioc"
import { IOPlatformDef } from "./io"
import { SpotlightPlatformDef } from "./spotlight"
import { Ref } from "vue"
export type PlatformDef = {
ui?: UIPlatformDef
@@ -46,11 +45,6 @@ export type PlatformDef = {
* If a value is not given, then the value is assumed to be true
*/
promptAsUsingCookies?: boolean
/**
* Whether to show the A/B testing workspace switcher click login flow or not
*/
workspaceSwitcherLogin?: Ref<boolean>
}
}

View File

@@ -5,24 +5,13 @@ import { useStreamStatic } from "~/composables/stream"
import TeamListAdapter from "~/helpers/teams/TeamListAdapter"
import { platform } from "~/platform"
import { min } from "lodash-es"
import { TeamMemberRole } from "~/helpers/backend/graphql"
/**
* Defines a workspace and its information
*/
export type PersonalWorkspace = {
type: "personal"
}
export type TeamWorkspace = {
type: "team"
teamID: string
teamName: string
role: TeamMemberRole | null | undefined
}
export type Workspace = PersonalWorkspace | TeamWorkspace
export type Workspace =
| { type: "personal" }
| { type: "team"; teamID: string; teamName: string }
export type WorkspaceServiceEvent = {
type: "managed-team-list-adapter-polled"

View File

@@ -1260,7 +1260,7 @@ dependencies = [
[[package]]
name = "hoppscotch-desktop"
version = "24.3.3"
version = "24.3.2"
dependencies = [
"cocoa 0.25.0",
"hex_color",

194
pnpm-lock.yaml generated
View File

@@ -89,9 +89,6 @@ importers:
'@nestjs/core':
specifier: 10.2.7
version: 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.6.0)
'@nestjs/event-emitter':
specifier: 2.0.4
version: 2.0.4(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)
'@nestjs/graphql':
specifier: 12.0.9
version: 12.0.9(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(graphql@16.8.1)(reflect-metadata@0.1.13)
@@ -107,9 +104,6 @@ importers:
'@nestjs/schedule':
specifier: 4.0.1
version: 4.0.1(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)
'@nestjs/terminus':
specifier: 10.2.3
version: 10.2.3(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(@prisma/client@5.8.1)(reflect-metadata@0.1.13)(rxjs@7.6.0)
'@nestjs/throttler':
specifier: 5.0.1
version: 5.0.1(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(reflect-metadata@0.1.13)
@@ -198,6 +192,9 @@ importers:
specifier: 7.6.0
version: 7.6.0
devDependencies:
'@faker-js/faker':
specifier: 8.4.1
version: 8.4.1
'@nestjs/cli':
specifier: 10.2.1
version: 10.2.1
@@ -3039,7 +3036,7 @@ packages:
peerDependencies:
vue: 3.3.9
dependencies:
vue: 3.3.9(typescript@4.9.5)
vue: 3.3.9(typescript@5.3.2)
/@codemirror/autocomplete@6.13.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.25.1)(@lezer/common@1.2.1):
resolution: {integrity: sha512-SuDrho1klTINfbcMPnyro1ZxU9xJtwDMtb62R8TjL/tOl71IoOsvBo1a9x+hDvHhIzkTcJHy2VC+rmpGgYkRSw==}
@@ -4051,6 +4048,11 @@ packages:
resolution: {integrity: sha512-R11tGE6yIFwqpaIqcfkcg7AICXzFg14+5h5v0TfF/9+RMDL6jhzCy/pxHVOfbALGdtVYdt6JdR21tuxEgl34dw==}
dev: false
/@faker-js/faker@8.4.1:
resolution: {integrity: sha512-XQ3cU+Q8Uqmrbf2e0cIC/QN43sTBSC8KF12u29Mb47tWrt2hAgBXSgpZMj4Ao8Uk0iJcU99QsOCaIL8934obCg==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0, npm: '>=6.14.13'}
dev: true
/@fontsource-variable/inter@5.0.15:
resolution: {integrity: sha512-CdQPQQgOVxg6ifmbrqYZeUqtQf7p2wPn6EvJ4M+vdNnsmYZgYwPPPQDNlIOU7LCUlSGaN26v6H0uA030WKn61g==}
@@ -6057,7 +6059,7 @@ packages:
peerDependencies:
vue: 3.3.9
dependencies:
vue: 3.3.9(typescript@4.9.5)
vue: 3.3.9(typescript@5.3.2)
/@humanwhocodes/config-array@0.11.14:
resolution: {integrity: sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==}
@@ -6203,8 +6205,8 @@ packages:
vue-i18n:
optional: true
dependencies:
'@intlify/message-compiler': 10.0.0-alpha.5
'@intlify/shared': 10.0.0-alpha.5
'@intlify/message-compiler': 10.0.0-alpha.3
'@intlify/shared': 10.0.0-alpha.3
jsonc-eslint-parser: 1.4.1
source-map: 0.6.1
vue-i18n: 9.8.0(vue@3.3.9)
@@ -6284,11 +6286,11 @@ packages:
'@intlify/shared': 9.2.2
dev: false
/@intlify/message-compiler@10.0.0-alpha.5:
resolution: {integrity: sha512-8Fr+1EsuxaCRWrrj3ZQ22DQU4vQzBxDHMMlLcN/6uM3R3kObTc0+Ip6EFrHXTsNm5LGETxlJlpi328U1dth/aA==}
/@intlify/message-compiler@10.0.0-alpha.3:
resolution: {integrity: sha512-WjM1KAl5enpOfprfVAJ3FzwACmizZFPgyV0sn+QXoWH8BG2ahVkf7uVEqQH0mvUr2rKKaScwpzhH3wZ5F7ZdPw==}
engines: {node: '>= 16'}
dependencies:
'@intlify/shared': 10.0.0-alpha.5
'@intlify/shared': 10.0.0-alpha.3
source-map-js: 1.2.0
dev: true
@@ -6323,8 +6325,8 @@ packages:
'@intlify/shared': 9.8.0
source-map-js: 1.2.0
/@intlify/shared@10.0.0-alpha.5:
resolution: {integrity: sha512-uCXI2IM9B2fwR7whGJ+DNBGxI02cSgD4E0ItY1ls++Vubrt92rD86o/9AUb9S/6jyK2hy3kFaTzublQIylMAsA==}
/@intlify/shared@10.0.0-alpha.3:
resolution: {integrity: sha512-fi2q48i+C6sSCAt3vOj/9LD3tkr1wcvLt+ifZEHrpPiwHCyKLDYGp5qBNUHUBBA/iqFTeWdtHUbHE9z9OeTXkw==}
engines: {node: '>= 16'}
dev: true
@@ -6396,7 +6398,7 @@ packages:
optional: true
dependencies:
'@intlify/bundle-utils': 7.0.0
'@intlify/shared': 10.0.0-alpha.5
'@intlify/shared': 10.0.0-alpha.3
'@rollup/pluginutils': 4.2.1
debug: 4.3.4(supports-color@9.4.0)
fast-glob: 3.3.2
@@ -6423,7 +6425,7 @@ packages:
optional: true
dependencies:
'@intlify/bundle-utils': 3.4.0(vue-i18n@9.8.0)
'@intlify/shared': 10.0.0-alpha.5
'@intlify/shared': 10.0.0-alpha.3
'@rollup/pluginutils': 4.2.1
debug: 4.3.4(supports-color@9.4.0)
fast-glob: 3.3.2
@@ -7191,17 +7193,6 @@ packages:
transitivePeerDependencies:
- encoding
/@nestjs/event-emitter@2.0.4(@nestjs/common@10.2.7)(@nestjs/core@10.2.7):
resolution: {integrity: sha512-quMiw8yOwoSul0pp3mOonGz8EyXWHSBTqBy8B0TbYYgpnG1Ix2wGUnuTksLWaaBiiOTDhciaZ41Y5fJZsSJE1Q==}
peerDependencies:
'@nestjs/common': ^8.0.0 || ^9.0.0 || ^10.0.0
'@nestjs/core': ^8.0.0 || ^9.0.0 || ^10.0.0
dependencies:
'@nestjs/common': 10.2.7(reflect-metadata@0.1.13)(rxjs@7.6.0)
'@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.6.0)
eventemitter2: 6.4.9
dev: false
/@nestjs/graphql@12.0.9(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(graphql@16.8.1)(reflect-metadata@0.1.13):
resolution: {integrity: sha512-A5oRD5GMwzaNZum06KSxKtqhC7LvS4p7v0hLGzvNWPuLrBjAjAnm/2NV8IV8lYiJXpPHNtSnnekZH9uvy/hZlw==}
peerDependencies:
@@ -7341,63 +7332,6 @@ packages:
- chokidar
dev: true
/@nestjs/terminus@10.2.3(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(@prisma/client@5.8.1)(reflect-metadata@0.1.13)(rxjs@7.6.0):
resolution: {integrity: sha512-iX7gXtAooePcyQqFt57aDke5MzgdkBeYgF5YsFNNFwOiAFdIQEhfv3PR0G+HlH9F6D7nBCDZt9U87Pks/qHijg==}
peerDependencies:
'@grpc/grpc-js': '*'
'@grpc/proto-loader': '*'
'@mikro-orm/core': '*'
'@mikro-orm/nestjs': '*'
'@nestjs/axios': ^1.0.0 || ^2.0.0 || ^3.0.0
'@nestjs/common': ^9.0.0 || ^10.0.0
'@nestjs/core': ^9.0.0 || ^10.0.0
'@nestjs/microservices': ^9.0.0 || ^10.0.0
'@nestjs/mongoose': ^9.0.0 || ^10.0.0
'@nestjs/sequelize': ^9.0.0 || ^10.0.0
'@nestjs/typeorm': ^9.0.0 || ^10.0.0
'@prisma/client': '*'
mongoose: '*'
reflect-metadata: 0.1.x || 0.2.x
rxjs: 7.x
sequelize: '*'
typeorm: '*'
peerDependenciesMeta:
'@grpc/grpc-js':
optional: true
'@grpc/proto-loader':
optional: true
'@mikro-orm/core':
optional: true
'@mikro-orm/nestjs':
optional: true
'@nestjs/axios':
optional: true
'@nestjs/microservices':
optional: true
'@nestjs/mongoose':
optional: true
'@nestjs/sequelize':
optional: true
'@nestjs/typeorm':
optional: true
'@prisma/client':
optional: true
mongoose:
optional: true
sequelize:
optional: true
typeorm:
optional: true
dependencies:
'@nestjs/common': 10.2.7(reflect-metadata@0.1.13)(rxjs@7.6.0)
'@nestjs/core': 10.2.7(@nestjs/common@10.2.7)(@nestjs/platform-express@10.2.7)(reflect-metadata@0.1.13)(rxjs@7.6.0)
'@prisma/client': 5.8.1(prisma@5.8.1)
boxen: 5.1.2
check-disk-space: 3.4.0
reflect-metadata: 0.1.13
rxjs: 7.6.0
dev: false
/@nestjs/testing@10.2.7(@nestjs/common@10.2.7)(@nestjs/core@10.2.7)(@nestjs/platform-express@10.2.7):
resolution: {integrity: sha512-d2SIqiJIf/7NSILeNNWSdRvTTpHSouGgisGHwf5PVDC7z4/yXZw/wPO9eJhegnxFlqk6n2LW4QBTmMzbqjAfHA==}
peerDependencies:
@@ -9659,7 +9593,7 @@ packages:
regenerator-runtime: 0.13.11
systemjs: 6.15.1
terser: 5.31.0
vite: 3.2.4(@types/node@17.0.27)(terser@5.31.0)
vite: 3.2.4(@types/node@18.18.8)(sass@1.58.0)(terser@5.31.0)
/@vitejs/plugin-legacy@2.3.0(terser@5.31.0)(vite@4.5.0):
resolution: {integrity: sha512-Bh62i0gzQvvT8AeAAb78nOnqSYXypkRmQmOTImdPZ39meHR9e2une3AIFmVo4s1SDmcmJ6qj18Sa/lRc/14KaA==}
@@ -10147,7 +10081,7 @@ packages:
dependencies:
'@vue/compiler-ssr': 3.3.9
'@vue/shared': 3.3.9
vue: 3.3.9(typescript@4.9.5)
vue: 3.3.9(typescript@5.3.2)
/@vue/shared@3.2.45:
resolution: {integrity: sha512-Ewzq5Yhimg7pSztDV+RH1UDKBzmtqieXQlpTVm2AwraoRL/Rks96mvd8Vgi7Lj+h+TH8dv7mXD3FRZR3TUvbSg==}
@@ -10209,7 +10143,7 @@ packages:
'@types/web-bluetooth': 0.0.14
'@vueuse/metadata': 8.9.4
'@vueuse/shared': 8.9.4(vue@3.3.9)
vue: 3.3.9(typescript@4.9.5)
vue: 3.3.9(typescript@5.3.2)
vue-demi: 0.14.7(vue@3.3.9)
/@vueuse/core@9.12.0(vue@3.3.9):
@@ -10268,7 +10202,7 @@ packages:
vue:
optional: true
dependencies:
vue: 3.3.9(typescript@4.9.5)
vue: 3.3.9(typescript@5.3.2)
vue-demi: 0.14.7(vue@3.3.9)
/@vueuse/shared@9.12.0(vue@3.3.9):
@@ -10674,12 +10608,6 @@ packages:
estraverse: 1.9.3
dev: false
/ansi-align@3.0.1:
resolution: {integrity: sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==}
dependencies:
string-width: 4.2.3
dev: false
/ansi-colors@4.1.1:
resolution: {integrity: sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==}
engines: {node: '>=6'}
@@ -11249,20 +11177,6 @@ packages:
/boolbase@1.0.0:
resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
/boxen@5.1.2:
resolution: {integrity: sha512-9gYgQKXx+1nP8mP7CzFyaUARhg7D3n1dF/FnErWmu9l6JvGpNUN278h0aSb+QjoiKSWG+iZ3uHrcqk0qrY9RQQ==}
engines: {node: '>=10'}
dependencies:
ansi-align: 3.0.1
camelcase: 6.3.0
chalk: 4.1.2
cli-boxes: 2.2.1
string-width: 4.2.3
type-fest: 0.20.2
widest-line: 3.1.0
wrap-ansi: 7.0.0
dev: false
/bplist-parser@0.2.0:
resolution: {integrity: sha512-z0M+byMThzQmD9NILRniCUXYsYpjwnlO8N5uCFaCqIOpqRsJCrQL9NK3JsD67CN5a08nF5oIL2bD6loTdHOuKw==}
engines: {node: '>= 5.10.0'}
@@ -11451,6 +11365,7 @@ packages:
/camelcase@6.3.0:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
dev: true
/caniuse-lite@1.0.30001616:
resolution: {integrity: sha512-RHVYKov7IcdNjVHJFNY/78RdG4oGVjbayxv8u5IO74Wv7Hlq4PnJE6mo/OjFijjVFNy5ijnCt6H3IIo4t+wfEw==}
@@ -11572,11 +11487,6 @@ packages:
engines: {node: '>=4.0.0'}
dev: false
/check-disk-space@3.4.0:
resolution: {integrity: sha512-drVkSqfwA+TvuEhFipiR1OC9boEGZL5RrWvVsOthdcvQNXyCCuKkEiTOTXZ7qxSf/GLwq4GvzfrQD/Wz325hgw==}
engines: {node: '>=16'}
dev: false
/check-error@1.0.3:
resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==}
dependencies:
@@ -11668,11 +11578,6 @@ packages:
engines: {node: '>=6'}
dev: true
/cli-boxes@2.2.1:
resolution: {integrity: sha512-y4coMcylgSCdVinjiDBuR8PCC2bLjyGTwEmPb9NHR/QaNU6EUOXcTY/s6VjGMD6ENSEaeQYHCY0GNGS5jfMwPw==}
engines: {node: '>=6'}
dev: false
/cli-cursor@3.1.0:
resolution: {integrity: sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==}
engines: {node: '>=8'}
@@ -14125,10 +14030,6 @@ packages:
engines: {node: '>=6'}
dev: false
/eventemitter2@6.4.9:
resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==}
dev: false
/eventemitter3@3.1.2:
resolution: {integrity: sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==}
dev: false
@@ -23147,7 +23048,7 @@ packages:
'@types/eslint': 8.56.10
eslint: 8.57.0
rollup: 2.79.1
vite: 3.2.4(@types/node@17.0.27)(terser@5.31.0)
vite: 3.2.4(@types/node@18.18.8)(sass@1.58.0)(terser@5.31.0)
/vite-plugin-eslint@1.8.1(eslint@8.57.0)(vite@4.5.0):
resolution: {integrity: sha512-PqdMf3Y2fLO9FsNPmMX+//2BF5SF8nEWspZdgl4kSt7UvHDRHVVfHvxsD7ULYzZrJDGRxR81Nq7TOFgwMnUang==}
@@ -23441,40 +23342,6 @@ packages:
- supports-color
dev: true
/vite@3.2.4(@types/node@17.0.27)(terser@5.31.0):
resolution: {integrity: sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
'@types/node': '>= 14'
less: '*'
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
dependencies:
'@types/node': 17.0.27
esbuild: 0.15.18
postcss: 8.4.32
resolve: 1.22.8
rollup: 2.79.1
terser: 5.31.0
optionalDependencies:
fsevents: 2.3.3
/vite@3.2.4(@types/node@18.18.8)(sass@1.58.0)(terser@5.31.0):
resolution: {integrity: sha512-Z2X6SRAffOUYTa+sLy3NQ7nlHFU100xwanq1WDwqaiFiCe+25zdxP1TfCS5ojPV2oDDcXudHIoPnI1Z/66B7Yw==}
engines: {node: ^14.18.0 || >=16.0.0}
@@ -23894,7 +23761,7 @@ packages:
'@vue/composition-api':
optional: true
dependencies:
vue: 3.3.9(typescript@4.9.5)
vue: 3.3.9(typescript@5.3.2)
/vue-eslint-parser@9.4.2(eslint@8.47.0):
resolution: {integrity: sha512-Ry9oiGmCAK91HrKMtCrKFWmSFWvYkpGglCeFAIqDdr9zdXmMMpJOmUJS7WWsW7fX81h6mwHmUZCQQ1E0PkSwYQ==}
@@ -24108,7 +23975,7 @@ packages:
vue: 3.3.9
dependencies:
sortablejs: 1.14.0
vue: 3.3.9(typescript@4.9.5)
vue: 3.3.9(typescript@5.3.2)
/w3c-hr-time@1.0.2:
resolution: {integrity: sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==}
@@ -24397,13 +24264,6 @@ packages:
dependencies:
string-width: 4.2.3
/widest-line@3.1.0:
resolution: {integrity: sha512-NsmoXalsWVDMGupxZ5R08ka9flZjjiLvHVAWYOKtiKM8ujtZWr9cRffak+uSE48+Ob8ObalXpwyeUiyDD6QFgg==}
engines: {node: '>=8'}
dependencies:
string-width: 4.2.3
dev: false
/windows-release@4.0.0:
resolution: {integrity: sha512-OxmV4wzDKB1x7AZaZgXMVsdJ1qER1ed83ZrTYd5Bwq2HfJVg3DJS8nqlAG4sMoJ7mu8cuRmLEYyU13BKwctRAg==}
engines: {node: '>=10'}

View File

@@ -1,4 +1,4 @@
FROM node:20-alpine3.19 as base_builder
FROM node:18-alpine3.19 as base_builder
WORKDIR /usr/src/app