Skip to content

Commit

Permalink
Add invite (#142)
Browse files Browse the repository at this point in the history
  • Loading branch information
nzws authored Mar 22, 2023
1 parent b2456f6 commit fff26d0
Show file tree
Hide file tree
Showing 24 changed files with 972 additions and 239 deletions.
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ MASTODON_ACCESS_TOKEN="hoge"
SERVER_TOKEN="hoge"
FRONTEND_ENDPOINT="http://localhost:3000"
ENABLE_QUEUE_DASHBOARD_YOU_HAVE_TO_PROTECT_IT="true"
ALLOW_ANONYMOUS_INVITE="true"

# S3 storage for high-request but relatively small amount of data: thumbnail etc
# Example: Cloudflare R2
Expand Down
1 change: 1 addition & 0 deletions .env.production
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ MASTODON_ACCESS_TOKEN="hoge"
JWT_EDGE_PRIVATE_KEY=""
JWT_EDGE_PUBLIC_KEY=""
SERVER_TOKEN=""
ALLOW_ANONYMOUS_INVITE="true"

# S3 storage for high-request but relatively small amount of data: thumbnail etc
STATIC_STORAGE_S3_ID=""
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
-- CreateTable
CREATE TABLE "Invite" (
"inviteId" VARCHAR(100) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"createdById" INTEGER,
"usedById" INTEGER,
"tenantId" INTEGER,
"usedAt" TIMESTAMP(3),

CONSTRAINT "Invite_pkey" PRIMARY KEY ("inviteId")
);

-- AddForeignKey
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_createdById_fkey" FOREIGN KEY ("createdById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_usedById_fkey" FOREIGN KEY ("usedById") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "Invite" ADD CONSTRAINT "Invite_tenantId_fkey" FOREIGN KEY ("tenantId") REFERENCES "Tenant"("id") ON DELETE SET NULL ON UPDATE CASCADE;
27 changes: 22 additions & 5 deletions apps/server/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,12 @@ model User {
avatarUrl String? @db.VarChar(200)
config Json
tenants Tenant[]
lives Live[]
comments Comment[]
Images Image[]
tenants Tenant[]
lives Live[]
comments Comment[]
images Image[]
invites Invite[] @relation("CreatedByUser")
usedInvites Invite[] @relation("UsedByUser")
}

model Tenant {
Expand All @@ -44,7 +46,8 @@ model Tenant {
owner User @relation(fields: [ownerId], references: [id])
lives Live[]
Images Image[]
images Image[]
Invite Invite[]
}

model AuthProvider {
Expand Down Expand Up @@ -142,3 +145,17 @@ model Comment {
@@unique([liveId, sourceUrl, sourceId])
@@index([liveId, createdAt, isDeleted])
}

model Invite {
inviteId String @id @db.VarChar(100)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
createdById Int?
usedById Int?
tenantId Int?
usedAt DateTime?
createdBy User? @relation("CreatedByUser", fields: [createdById], references: [id])
usedBy User? @relation("UsedByUser", fields: [usedById], references: [id])
tenant Tenant? @relation(fields: [tenantId], references: [id])
}
55 changes: 55 additions & 0 deletions apps/server/src/controllers/v1/invites/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Methods } from 'api-types/api/v1/invites';
import { invites } from '../../../models';
import {
InviteDisabledError,
NoTenantError,
TooManyInvitesError
} from '../../../models/invite';
import { APIRoute, UserState } from '../../../utils/types';

type Response = Methods['post']['resBody'];

export const postV1Invites: APIRoute<
never,
never,
never,
Response,
UserState
> = async ctx => {
try {
const invite = await invites.createInvite(ctx.state.user);

ctx.body = {
invite: invites.getPublic(invite)
};
} catch (e) {
if (e instanceof TooManyInvitesError) {
ctx.status = 400;
ctx.body = {
errorCode: 'invalid_request',
message: '招待コードは5個まで発行できます。'
};
return;
}

if (e instanceof NoTenantError) {
ctx.status = 400;
ctx.body = {
errorCode: 'invalid_request',
message: '招待コードを作成するには、既に配信者である必要があります。'
};
return;
}

if (e instanceof InviteDisabledError) {
ctx.status = 400;
ctx.body = {
errorCode: 'invalid_request',
message: '招待コードの新規作成は現在管理者が無効にしています。'
};
return;
}

throw e;
}
};
18 changes: 18 additions & 0 deletions apps/server/src/controllers/v1/invites/get-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Methods } from 'api-types/api/v1/invites';
import { invites } from '../../../models';
import { APIRoute, UserState } from '../../../utils/types';

type Response = Methods['get']['resBody'];

export const getV1InvitesGetList: APIRoute<
never,
never,
never,
Response,
UserState
> = async ctx => {
const list = await invites.getList(ctx.state.user.id);
const publicList = list.map(invites.getPublic);

ctx.body = publicList;
};
100 changes: 100 additions & 0 deletions apps/server/src/controllers/v1/tenants/create.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { JSONSchemaType } from 'ajv';
import { Methods } from 'api-types/api/v1/tenants';
import { tenants } from '../../../models';
import {
InvalidSlugError,
InviteAlreadyUsedError,
NoInviteError,
SlugAlreadyUsedError
} from '../../../models/tenant';
import { APIRoute, UserState } from '../../../utils/types';
import { validateWithType } from '../../../utils/validate';

type Request = Methods['post']['reqBody'];
type Response = Methods['post']['resBody'];

const reqBodySchema: JSONSchemaType<Request> = {
type: 'object',
properties: {
inviteCode: {
type: 'string',
minLength: 1,
maxLength: 100
},
slug: {
type: 'string',
minLength: 1,
maxLength: 100
}
},
required: ['inviteCode', 'slug'],
additionalProperties: false
};

export const postV1Tenants: APIRoute<
never,
never,
Request,
Response,
UserState
> = async ctx => {
if (!validateWithType(reqBodySchema, ctx.request.body)) {
ctx.status = 400;
ctx.body = {
errorCode: 'invalid_request'
};
return;
}
const body = ctx.request.body;
const newSlug = body.slug?.toLowerCase();

try {
const tenant = await tenants.createTenantByInvite(
newSlug,
ctx.state.user,
body.inviteCode
);

ctx.body = {
tenant: tenants.getPublic(tenant)
};
} catch (e) {
if (e instanceof InvalidSlugError) {
ctx.status = 400;
ctx.body = {
errorCode: 'invalid_request',
message: '配信者IDには半角英数字のみ使用できます'
};
return;
}

if (e instanceof SlugAlreadyUsedError) {
ctx.status = 400;
ctx.body = {
errorCode: 'invalid_request',
message: '配信者IDはすでに使われています'
};
return;
}

if (e instanceof InviteAlreadyUsedError) {
ctx.status = 400;
ctx.body = {
errorCode: 'invalid_request',
message: '招待コードはすでに使われています'
};
return;
}

if (e instanceof NoInviteError) {
ctx.status = 400;
ctx.body = {
errorCode: 'invalid_request',
message: '招待コードが見つかりません'
};
return;
}

throw e;
}
};
38 changes: 20 additions & 18 deletions apps/server/src/controllers/v1/tenants/patch.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { JSONSchemaType } from 'ajv';
import { Methods } from 'api-types/api/v1/tenants/_tenantId@number';
import { tenants } from '../../../models';
import { InvalidSlugError, SlugAlreadyUsedError } from '../../../models/tenant';
import { APIRoute, TenantState, UserState } from '../../../utils/types';
import { validateWithType } from '../../../utils/validate';

type Request = Methods['patch']['reqBody'];
type Response = Methods['patch']['resBody'];

const slugRegex = /^[a-z0-9]+$/;

const reqBodySchema: JSONSchemaType<Request> = {
type: 'object',
properties: {
Expand Down Expand Up @@ -61,23 +60,26 @@ export const patchV1Tenants: APIRoute<
const newSlug = body.slug?.toLowerCase();

if (newSlug && newSlug !== ctx.state.tenant.slug) {
if (!slugRegex.test(newSlug)) {
ctx.status = 400;
ctx.body = {
errorCode: 'invalid_request',
message: '配信者IDには半角英数字のみ使用できます'
};
return;
}
try {
await tenants.checkIsValidSlug(newSlug);
} catch (e) {
if (e instanceof InvalidSlugError) {
ctx.status = 400;
ctx.body = {
errorCode: 'invalid_request',
message: '配信者IDには半角英数字のみ使用できます'
};
return;
}

const tenant = await tenants.get(undefined, body.slug);
if (tenant) {
ctx.status = 409;
ctx.body = {
errorCode: 'invalid_request',
message: 'この配信者IDは既に使われています'
};
return;
if (e instanceof SlugAlreadyUsedError) {
ctx.status = 400;
ctx.body = {
errorCode: 'invalid_request',
message: '配信者IDはすでに使われています'
};
return;
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AuthProviders } from './auth-provider';
import { Lives } from './live';
import { Comments } from './comment';
import { Images } from './image';
import { Invites } from './invite';

export { prisma };
export const users = Users(prisma.user);
Expand All @@ -13,3 +14,4 @@ export const authProviders = AuthProviders(prisma.authProvider);
export const lives = Lives(prisma.live);
export const comments = Comments(prisma.comment);
export const images = Images(prisma.image);
export const invites = Invites(prisma.invite);
Loading

1 comment on commit fff26d0

@vercel
Copy link

@vercel vercel bot commented on fff26d0 Mar 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

knzklive2 – ./

knzklive2-git-production-knzklive.vercel.app
knzk.live
www.knzk.live
knzklive2-knzklive.vercel.app

Please sign in to comment.