Skip to content

Commit

Permalink
feat: 招待コード発行の軌跡を残す (#30)
Browse files Browse the repository at this point in the history
  • Loading branch information
nacika-ins authored Aug 31, 2024
2 parents 29dc97f + 511175a commit 7ccf449
Show file tree
Hide file tree
Showing 8 changed files with 162 additions and 20 deletions.
2 changes: 1 addition & 1 deletion packages/backend/.idea/codeStyles/Project.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion packages/backend/.idea/codeStyles/codeStyleConfig.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
export class UserIdToRegistrationTicket1725101670794 {
name = 'UserIdToRegistrationTicket1725101670794'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "requestUserId" character varying(32)`);
await queryRunner.query(`COMMENT ON COLUMN "registration_ticket"."requestUserId" IS 'The request user ID.'`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "invitedUserId" character varying(32)`);
await queryRunner.query(`COMMENT ON COLUMN "registration_ticket"."invitedUserId" IS 'The invited user ID.'`);
await queryRunner.query(`CREATE INDEX "IDX_0b96e37dbfcc3c151b9a84b1a9" ON "registration_ticket" ("requestUserId") `);
await queryRunner.query(`CREATE INDEX "IDX_cfcbf86bed74362ef7e0d43a0c" ON "registration_ticket" ("invitedUserId") `);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "usedAt" TIMESTAMP`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "FK_0b96e37dbfcc3c151b9a84b1a95" FOREIGN KEY ("requestUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD CONSTRAINT "FK_cfcbf86bed74362ef7e0d43a0c3" FOREIGN KEY ("invitedUserId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "FK_cfcbf86bed74362ef7e0d43a0c3"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP CONSTRAINT "FK_0b96e37dbfcc3c151b9a84b1a95"`);
await queryRunner.query(`DROP INDEX "public"."IDX_cfcbf86bed74362ef7e0d43a0c"`);
await queryRunner.query(`DROP INDEX "public"."IDX_0b96e37dbfcc3c151b9a84b1a9"`);
await queryRunner.query(`COMMENT ON COLUMN "registration_ticket"."invitedUserId" IS 'The invited user ID.'`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "invitedUserId"`);
await queryRunner.query(`COMMENT ON COLUMN "registration_ticket"."requestUserId" IS 'The request user ID.'`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "requestUserId"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "usedAt"`);

}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@

export class RegistrationTicketToUserPending1725104341375 {
name = 'RegistrationTicketToUserPending1725104341375'

async up(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_pending" ADD "registrationTicketId" character varying(32)`);
await queryRunner.query(`COMMENT ON COLUMN "user_pending"."registrationTicketId" IS 'The registration ticket ID.'`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "usedAt"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "usedAt" TIMESTAMP WITH TIME ZONE`);
await queryRunner.query(`COMMENT ON COLUMN "registration_ticket"."usedAt" IS 'The date and time the ticket was used.'`);
await queryRunner.query(`CREATE INDEX "IDX_80e9a0b6943dd3e821b9bdd3cf" ON "user_pending" ("registrationTicketId") `);
await queryRunner.query(`ALTER TABLE "user_pending" ADD CONSTRAINT "FK_80e9a0b6943dd3e821b9bdd3cf3" FOREIGN KEY ("registrationTicketId") REFERENCES "registration_ticket"("id") ON DELETE CASCADE ON UPDATE NO ACTION`);
}

async down(queryRunner) {
await queryRunner.query(`ALTER TABLE "user_pending" DROP CONSTRAINT "FK_80e9a0b6943dd3e821b9bdd3cf3"`);
await queryRunner.query(`DROP INDEX "public"."IDX_80e9a0b6943dd3e821b9bdd3cf"`);
await queryRunner.query(`COMMENT ON COLUMN "registration_ticket"."usedAt" IS 'The date and time the ticket was used.'`);
await queryRunner.query(`ALTER TABLE "registration_ticket" DROP COLUMN "usedAt"`);
await queryRunner.query(`ALTER TABLE "registration_ticket" ADD "usedAt" TIMESTAMP`);
await queryRunner.query(`COMMENT ON COLUMN "user_pending"."registrationTicketId" IS 'The registration ticket ID.'`);
await queryRunner.query(`ALTER TABLE "user_pending" DROP COLUMN "registrationTicketId"`);
}

}
37 changes: 36 additions & 1 deletion packages/backend/src/models/entities/RegistrationTicket.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
import { id } from '../id.js';
import { User } from './User.js';

@Entity()
export class RegistrationTicket {
Expand All @@ -14,4 +15,38 @@ export class RegistrationTicket {
length: 64,
})
public code: string;

@Index()
@Column({
...id(),
nullable: true,
comment: 'The request user ID.',
})
public requestUserId: User['id'] | null;

@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public requestUser: User | null;

@Index()
@Column({
...id(),
nullable: true,
comment: 'The invited user ID.',
})
public invitedUserId: User['id'] | null;

@ManyToOne(type => User, {
onDelete: 'CASCADE',
})
@JoinColumn()
public invitedUser: User | null;

@Column('timestamp with time zone', {
nullable: true,
comment: 'The date and time the ticket was used.',
})
public usedAt: Date | null;
}
17 changes: 16 additions & 1 deletion packages/backend/src/models/entities/UserPending.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { PrimaryColumn, Entity, Index, Column } from 'typeorm';
import { PrimaryColumn, Entity, Index, Column, ManyToOne, JoinColumn } from 'typeorm';
import { id } from '../id.js';
import { RegistrationTicket } from './RegistrationTicket.js';

@Entity()
export class UserPending {
Expand Down Expand Up @@ -29,4 +30,18 @@ export class UserPending {
length: 128,
})
public password: string;

@Index()
@Column({
...id(),
nullable: true,
comment: 'The registration ticket ID.',
})
public registrationTicketId: string | null;

@ManyToOne(type => RegistrationTicket, {
onDelete: 'CASCADE',
})
@JoinColumn()
public registrationTicket: RegistrationTicket | null;
}
63 changes: 48 additions & 15 deletions packages/backend/src/server/api/SignupApiService.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Inject, Injectable } from '@nestjs/common';
import rndstr from 'rndstr';
import bcrypt from 'bcryptjs';
import { IsNull, Not } from 'typeorm';
import { DI } from '@/di-symbols.js';
import type { RegistrationTicketsRepository, UsedUsernamesRepository, UserPendingsRepository, UserProfilesRepository, UsersRepository } from '@/models/index.js';
import type { Config } from '@/config.js';
Expand All @@ -15,7 +16,6 @@ import { FastifyReplyError } from '@/misc/fastify-reply-error.js';
import { bindThis } from '@/decorators.js';
import { SigninService } from './SigninService.js';
import type { FastifyRequest, FastifyReply } from 'fastify';
import { IsNull } from 'typeorm';

@Injectable()
export class SignupApiService {
Expand Down Expand Up @@ -67,7 +67,7 @@ export class SignupApiService {
const body = request.body;

const instance = await this.metaService.fetch(true);

// Verify *Captcha
// ただしテスト時はこの機構は障害となるため無効にする
if (process.env.NODE_ENV !== 'test') {
Expand All @@ -76,7 +76,7 @@ export class SignupApiService {
throw new FastifyReplyError(400, err);
});
}

if (instance.enableRecaptcha && instance.recaptchaSecretKey) {
await this.captchaService.verifyRecaptcha(instance.recaptchaSecretKey, body['g-recaptcha-response']).catch(err => {
throw new FastifyReplyError(400, err);
Expand All @@ -89,44 +89,48 @@ export class SignupApiService {
});
}
}

const username = body['username'];
const password = body['password'];
const host: string | null = process.env.NODE_ENV === 'test' ? (body['host'] ?? null) : null;
const invitationCode = body['invitationCode'];
const emailAddress = body['emailAddress'];

if (instance.emailRequiredForSignup) {
if (emailAddress == null || typeof emailAddress !== 'string') {
reply.code(400);
return;
}

const res = await this.emailService.validateEmailForAccount(emailAddress);
if (!res.available) {
reply.code(400);
return;
}
}

if (instance.disableRegistration) {
if (invitationCode == null || typeof invitationCode !== 'string') {
reply.code(400);
return;
}

const ticket = await this.registrationTicketsRepository.findOneBy({
code: invitationCode,
usedAt: IsNull(),
});

if (ticket == null) {
reply.code(400);
return;
}

this.registrationTicketsRepository.delete(ticket.id);

await this.registrationTicketsRepository.update({ id: ticket.id }, {
usedAt: new Date(),
invitedUserId: null,
});
}

if (instance.emailRequiredForSignup) {
if (await this.usersRepository.findOneBy({ usernameLower: username.toLowerCase(), host: IsNull() })) {
throw new FastifyReplyError(400, 'DUPLICATED_USERNAME');
Expand All @@ -143,13 +147,19 @@ export class SignupApiService {
const salt = await bcrypt.genSalt(8);
const hash = await bcrypt.hash(password, salt);

const ticket = instance.disableRegistration ? await this.registrationTicketsRepository.findOneBy({
code: invitationCode,
usedAt: Not(IsNull()),
}) : undefined;

await this.userPendingsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
code,
email: emailAddress!,
username: username,
password: hash,
registrationTicketId: ticket?.id ?? undefined,
});

const link = `${this.config.url}/signup-complete/${code}`;
Expand All @@ -165,12 +175,26 @@ export class SignupApiService {
const { account, secret } = await this.signupService.signup({
username, password, host,
});


if (instance.disableRegistration) {
const ticket = await this.registrationTicketsRepository.findOneBy({
code: invitationCode,
usedAt: Not(IsNull()),
});

if (ticket) {
this.registrationTicketsRepository.update({ id: ticket.id }, {
usedAt: new Date(),
invitedUserId: account.id,
});
}
}

const res = await this.userEntityService.pack(account, account, {
detail: true,
includeSecrets: true,
});

return {
...res,
token: secret,
Expand All @@ -195,7 +219,16 @@ export class SignupApiService {
passwordHash: pendingUser.password,
});

this.userPendingsRepository.delete({
if (pendingUser.registrationTicketId) {
await this.registrationTicketsRepository.update({
id: pendingUser.registrationTicketId,
}, {
usedAt: new Date(),
invitedUserId: account.id,
});
}

await this.userPendingsRepository.delete({
id: pendingUser.id,
});

Expand Down
7 changes: 6 additions & 1 deletion packages/backend/src/server/api/endpoints/invite.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@ export const meta = {

export const paramDef = {
type: 'object',
properties: {},
properties: {
userId: { type: 'string', format: 'misskey:id' },
},
required: [],
} as const;

Expand All @@ -42,6 +44,8 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
private idService: IdService,
) {
super(meta, paramDef, async (ps, me) => {
const userId = ps.userId || me.id;

const code = rndstr({
length: 8,
chars: '2-9A-HJ-NP-Z', // [0-9A-Z] w/o [01IO] (32 patterns)
Expand All @@ -50,6 +54,7 @@ export default class extends Endpoint<typeof meta, typeof paramDef> {
await this.registrationTicketsRepository.insert({
id: this.idService.genId(),
createdAt: new Date(),
requestUserId: userId,
code,
});

Expand Down

0 comments on commit 7ccf449

Please sign in to comment.