From 20f0180dd0ef6a3895c3b677d9694136fe1c9725 Mon Sep 17 00:00:00 2001 From: Wonwoo Choi Date: Tue, 3 Oct 2023 16:41:27 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A6=B0=ED=8A=B8=20=EC=97=90=EB=9F=AC=20?= =?UTF-8?q?=EC=88=98=EC=A0=95,=20any=20=ED=83=80=EC=9E=85=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0=20(#250)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 린트 에러 수정, any 타입 제거 * 테스트 수정 * 테스트 수정 2 * POST user에 빠진 조건 추가 --- .eslintrc.js | 2 - src/api/handlers/emails.ts | 32 +++---- src/api/handlers/groups.ts | 25 +++--- src/api/handlers/login.ts | 43 +++++---- src/api/handlers/users.ts | 135 ++++++++++------------------- src/model/email_addresses.ts | 28 +++--- src/model/groups.ts | 58 +++++++++---- src/model/hosts.ts | 28 ++++-- src/model/model.ts | 6 +- src/model/oauth.ts | 15 ++-- src/model/permissions.ts | 15 ++-- src/model/shells.ts | 9 +- src/model/transaction.ts | 5 +- src/model/users.ts | 86 ++++++++++-------- src/oidc/configuration.ts | 11 ++- src/oidc/routes.ts | 1 - test/api/email.test.ts | 2 +- test/api/groups.test.ts | 44 +++++----- test/api/login.test.ts | 7 +- test/api/shells.test.ts | 2 - test/api/users.test.ts | 18 ++-- test/model/email_addresses.test.ts | 18 ++-- test/model/groups.test.ts | 3 +- test/model/hosts.test.ts | 4 +- test/model/model.test.ts | 8 +- test/model/permissions.test.ts | 23 +++-- test/model/users.test.ts | 46 +++++----- 27 files changed, 352 insertions(+), 322 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index b0edda0..93cd714 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,7 +10,5 @@ module.exports = { 'no-sequences': 'error', 'no-constant-condition': ['error', { checkLoops: false }], '@typescript-eslint/array-type': ['error', { default: 'generic' }], - '@typescript-eslint/no-unused-vars': 'warn', - '@typescript-eslint/no-explicit-any': 'warn', }, }; diff --git a/src/api/handlers/emails.ts b/src/api/handlers/emails.ts index c3d4464..4b8838c 100644 --- a/src/api/handlers/emails.ts +++ b/src/api/handlers/emails.ts @@ -1,4 +1,5 @@ import { IMiddleware } from 'koa-router'; +import z from 'zod'; import Config from '../../config'; import { EmailAddress } from '../../model/email_addresses'; import { EmailInUseError, InvalidEmailError, ResendLimitExeededError } from '../../model/errors'; @@ -7,23 +8,19 @@ import { sendEmail } from '../email'; import emailVerificationTemplate from '../templates/verification_email_template'; export function sendVerificationEmail(model: Model, config: Config): IMiddleware { - return async (ctx, next) => { - const body: any = ctx.request.body; - - if (body == null || typeof body !== 'object') { - ctx.status = 400; - return; - } + const bodySchema = z.object({ + emailLocal: z.string().trim().nonempty(), + emailDomain: z.string().trim().nonempty(), + }); - let { emailLocal, emailDomain } = body; - emailLocal = emailLocal.trim(); - emailDomain = emailDomain.trim(); - - if (!emailLocal || !emailDomain) { + return async (ctx, next) => { + const bodyResult = bodySchema.safeParse(ctx.request.body); + if (!bodyResult.success) { ctx.status = 400; return; } + const { emailLocal, emailDomain } = bodyResult.data; if (emailDomain !== 'snu.ac.kr') { ctx.status = 400; return; @@ -82,15 +79,18 @@ export function sendVerificationEmail(model: Model, config: Config): IMiddleware } export function checkVerificationEmailToken(model: Model): IMiddleware { - return async (ctx, next) => { - const body: any = ctx.request.body; + const bodySchema = z.object({ + token: z.string().nonempty(), + }); - if (body == null || typeof body !== 'object') { + return async (ctx, next) => { + const bodyResult = bodySchema.safeParse(ctx.request.body); + if (!bodyResult.success) { ctx.status = 400; return; } - const { token } = body; + const { token } = bodyResult.data; let emailAddress: EmailAddress; let result; diff --git a/src/api/handlers/groups.ts b/src/api/handlers/groups.ts index 18e8deb..dbef10f 100644 --- a/src/api/handlers/groups.ts +++ b/src/api/handlers/groups.ts @@ -1,4 +1,5 @@ import { IMiddleware } from 'koa-router'; +import z from 'zod'; import { BadParameterError } from '../../model/errors'; import Model from '../../model/model'; import { User } from '../../model/users'; @@ -64,7 +65,7 @@ export function listMembers(model: Model): IMiddleware { } if (!owner) { - ctx.status = 401; + ctx.status = 403; return; } @@ -107,7 +108,7 @@ export function listPending(model: Model): IMiddleware { } if (!owner) { - ctx.status = 401; + ctx.status = 403; return; } @@ -160,16 +161,18 @@ export function applyGroup(model: Model): IMiddleware { } export function acceptGroup(model: Model): IMiddleware { + const bodySchema = z.number().array().nonempty(); + return async (ctx, next) => { if (typeof ctx.state.userIdx === 'number') { const gid = Number(ctx.params.gid); - const body: any = ctx.request.body; - if (body === null || !(body instanceof Array) || !body.every(v => typeof v === 'number')) { + const bodyResult = bodySchema.safeParse(ctx.request.body); + if (!bodyResult.success) { ctx.status = 400; return; } - const users: Array = body; + const users = bodyResult.data; try { await model.pgDo(async tr => { @@ -177,7 +180,7 @@ export function acceptGroup(model: Model): IMiddleware { const owner = await model.groups.checkOwner(tr, group.idx, ctx.state.userIdx); if (!owner) { - ctx.status = 401; + ctx.status = 403; return; } @@ -202,16 +205,18 @@ export function acceptGroup(model: Model): IMiddleware { } export function rejectGroup(model: Model): IMiddleware { + const bodySchema = z.number().array().nonempty(); + return async (ctx, next) => { if (typeof ctx.state.userIdx === 'number') { const gid = Number(ctx.params.gid); - const body: any = ctx.request.body; - if (body === null || !(body instanceof Array) || !body.every(v => typeof v === 'number')) { + const bodyResult = bodySchema.safeParse(ctx.request.body); + if (!bodyResult.success) { ctx.status = 400; return; } - const users: Array = body; + const users = bodyResult.data; try { await model.pgDo(async tr => { @@ -219,7 +224,7 @@ export function rejectGroup(model: Model): IMiddleware { const owner = await model.groups.checkOwner(tr, group.idx, ctx.state.userIdx); if (!owner) { - ctx.status = 401; + ctx.status = 403; return; } diff --git a/src/api/handlers/login.ts b/src/api/handlers/login.ts index cc5b105..74d00e6 100644 --- a/src/api/handlers/login.ts +++ b/src/api/handlers/login.ts @@ -1,21 +1,30 @@ import { IMiddleware } from 'koa-router'; +import z from 'zod'; import Config from '../../config'; import { AuthorizationError, ControllableError, NoSuchEntryError } from '../../model/errors'; import Model from '../../model/model'; import { SignatureError, verifyPubkeyReq } from '../pubkey'; +const loginBodySchema = z.object({ + username: z.string(), + password: z.string(), +}); + +const legacyLoginBodySchema = z.object({ + member_account: z.string(), + member_password: z.string(), +}); + export function login(model: Model): IMiddleware { return async ctx => { - const body: any = ctx.request.body; - - if (!body || typeof body !== 'object') { + const bodyResult = loginBodySchema.safeParse(ctx.request.body); + if (!bodyResult.success) { ctx.status = 400; return; } - const { username, password } = body; - + const { username, password } = bodyResult.data; try { const userIdx = await model.pgDo(tr => model.users.authenticate(tr, username, password)); await ctx.state.setSession(userIdx); @@ -36,14 +45,13 @@ export function login(model: Model): IMiddleware { export function loginPAM(model: Model): IMiddleware { return async ctx => { - const body: any = ctx.request.body; - - if (!body || typeof body !== 'object') { + const bodyResult = loginBodySchema.safeParse(ctx.request.body); + if (!bodyResult.success) { ctx.status = 400; return; } - const { username, password } = body; + const { username, password } = bodyResult.data; try { await model.pgDo(async tr => { try { @@ -83,24 +91,14 @@ export function loginPAM(model: Model): IMiddleware { export function loginLegacy(model: Model, config: Config): IMiddleware { return async ctx => { - const body: any = ctx.request.body; - - if (!body || typeof body !== 'object') { + const bodyResult = legacyLoginBodySchema.safeParse(ctx.request.body); + if (!bodyResult.success) { ctx.status = 400; return; } - const username = body.member_account; - const password = body.member_password; - - if (!username || !password) { - // 200 means failure - ctx.status = 200; - return; - } - + const { member_account: username, member_password: password } = bodyResult.data; let userIdx: number; - try { await model.pgDo(async tr => { try { @@ -112,6 +110,7 @@ export function loginLegacy(model: Model, config: Config): IMiddleware { throw new AuthorizationError(); } } catch (e) { + // 200 means failure ctx.status = 200; throw e; } diff --git a/src/api/handlers/users.ts b/src/api/handlers/users.ts index 4a8e4b8..5396060 100644 --- a/src/api/handlers/users.ts +++ b/src/api/handlers/users.ts @@ -1,6 +1,7 @@ import { createPublicKey } from 'crypto'; import { jwtVerify } from 'jose'; import { IMiddleware } from 'koa-router'; +import z from 'zod'; import Config from '../../config'; import { EmailAddress } from '../../model/email_addresses'; import { InvalidEmailError, ResendLimitExeededError, UserExistsError } from '../../model/errors'; @@ -9,20 +10,23 @@ import { sendEmail } from '../email'; import changePasswordTemplate from '../templates/change_password_email_template'; export function createUser(model: Model, config: Config): IMiddleware { - return async (ctx, next) => { - const body: any = ctx.request.body; + const bodySchema = z.object({ + username: z.string().nonempty().max(20).regex(/^[a-z][a-z0-9]+$/), + name: z.string().nonempty(), + password: z.string().min(8), + preferredLanguage: z.enum(['ko', 'en']), + studentNumbers: z.string().regex(/^(\d{5}-\d{3}|\d{4}-\d{4,5})$/).array().nonempty(), + token: z.string().nonempty(), + }); - if (body == null || typeof body !== 'object') { + return async (ctx, next) => { + const bodyResult = bodySchema.safeParse(ctx.request.body); + if (!bodyResult.success) { ctx.status = 400; return; } - const token = body.token; - if (!token) { - ctx.status = 401; - return; - } - + const token = bodyResult.data.token; let emailAddress: EmailAddress; // check verification token @@ -35,32 +39,7 @@ export function createUser(model: Model, config: Config): IMiddleware { return; } - const { username, name, password, preferredLanguage, studentNumbers } = body; - - if ( - !username || !name || !password || !preferredLanguage - || !studentNumbers || studentNumbers.constructor !== Array || studentNumbers.length === 0 - ) { - ctx.status = 400; - return; - } - - // validates inputs - if (!/^[a-z][a-z0-9]+$/.test(username) || username.length > 20) { - ctx.status = 400; - return; - } - - if (password.length < 8) { - ctx.status = 400; - return; - } - - if (!(['ko', 'en'].includes(preferredLanguage))) { - ctx.status = 400; - return; - } - + const { username, name, password, preferredLanguage, studentNumbers } = bodyResult.data; try { // acquires access exclusive lock on 'users' await model.pgDo(async tr => { @@ -85,22 +64,7 @@ export function createUser(model: Model, config: Config): IMiddleware { await model.emailAddresses.validate(tr, userIdx, emailAddressIdx); await model.emailAddresses.removeToken(tr, token); - const validateStudentNumber = (snuid: string) => { - const regexList = [ - /^\d\d\d\d\d-\d\d\d$/, - /^\d\d\d\d-\d\d\d\d$/, - /^\d\d\d\d-\d\d\d\d\d$/, - ]; - for (const regex of regexList) { - if (regex.test(snuid)) { - return; - } - } - throw new Error('Invalid student number'); - }; - for (const studentNumber of studentNumbers) { - validateStudentNumber(studentNumber); await model.users.addStudentNumber(tr, userIdx, studentNumber); } }, ['users']); @@ -118,23 +82,19 @@ export function createUser(model: Model, config: Config): IMiddleware { } export function sendChangePasswordEmail(model: Model, config: Config): IMiddleware { - return async (ctx, next) => { - const body: any = ctx.request.body; + const bodySchema = z.object({ + emailLocal: z.string().trim().nonempty(), + emailDomain: z.string().trim().nonempty(), + }); - if (body == null || typeof body !== 'object') { - ctx.status = 400; - return; - } - - let { emailLocal, emailDomain } = body; - emailLocal = emailLocal.trim(); - emailDomain = emailDomain.trim(); - - if (!emailLocal || !emailDomain) { + return async (ctx, next) => { + const bodyResult = bodySchema.safeParse(ctx.request.body); + if (!bodyResult.success) { ctx.status = 400; return; } + const { emailLocal, emailDomain } = bodyResult.data; let token = ''; let resendCount = -1; try { @@ -177,15 +137,18 @@ export function sendChangePasswordEmail(model: Model, config: Config): IMiddlewa } export function checkChangePasswordEmailToken(model: Model): IMiddleware { - return async (ctx, next) => { - const body: any = ctx.request.body; + const bodySchema = z.object({ + token: z.string().nonempty(), + }); - if (body == null || typeof body !== 'object') { + return async (ctx, next) => { + const bodyResult = bodySchema.safeParse(ctx.request.body); + if (!bodyResult.success) { ctx.status = 400; return; } - const { token } = body; + const { token } = bodyResult.data; try { await model.pgDo(async tr => { @@ -208,26 +171,19 @@ export function checkChangePasswordEmailToken(model: Model): IMiddleware { } export function changePassword(model: Model): IMiddleware { - return async (ctx, next) => { - const body: any = ctx.request.body; - - if (body == null || typeof body !== 'object') { - ctx.status = 400; - return; - } - - const { newPassword, token } = body; + const bodySchema = z.object({ + newPassword: z.string().min(8), + token: z.string().nonempty(), + }); - if (!newPassword || !token) { - ctx.status = 400; - return; - } - - if (newPassword.length < 8) { + return async (ctx, next) => { + const bodyResult = bodySchema.safeParse(ctx.request.body); + if (!bodyResult.success) { ctx.status = 400; return; } + const { newPassword, token } = bodyResult.data; try { // check token validity await model.pgDo(async tr => { @@ -278,6 +234,10 @@ export function getUserShell(model: Model): IMiddleware { } export function changeUserShell(model: Model): IMiddleware { + const bodySchema = z.object({ + shell: z.string().nonempty(), + }); + return async (ctx, next) => { // authorize const userIdx = ctx.state.userIdx; @@ -286,20 +246,13 @@ export function changeUserShell(model: Model): IMiddleware { return; } - const body: any = ctx.request.body; - - if (body == null || typeof body !== 'object') { - ctx.status = 400; - return; - } - - const { shell } = body; - - if (!shell || typeof shell !== 'string') { + const bodyResult = bodySchema.safeParse(ctx.request.body); + if (!bodyResult.success) { ctx.status = 400; return; } + const { shell } = bodyResult.data; try { await model.pgDo(async tr => { await model.users.changeShell(tr, userIdx, shell); diff --git a/src/model/email_addresses.ts b/src/model/email_addresses.ts index fbdfc87..834c351 100644 --- a/src/model/email_addresses.ts +++ b/src/model/email_addresses.ts @@ -1,16 +1,20 @@ import * as crypto from 'crypto'; import moment from 'moment'; import { ExpiredTokenError, NoSuchEntryError } from './errors'; -import Model from './model'; import Transaction from './transaction'; +interface EmailAddressRow { + address_local: string; + address_domain: string; +} + export interface EmailAddress { local: string; domain: string; } export default class EmailAddresses { - constructor(private readonly model: Model) { + constructor() { } /** @@ -23,7 +27,7 @@ export default class EmailAddresses { public async create(tr: Transaction, local: string, domain: string): Promise { const query = 'INSERT INTO email_addresses(address_local, address_domain) VALUES ($1, $2) ' + 'ON CONFLICT (LOWER(address_local), LOWER(address_domain)) DO UPDATE SET address_local = $1 RETURNING idx'; - const result = await tr.query(query, [local, domain]); + const result = await tr.query<{ idx: number }>(query, [local, domain]); const idx = result.rows[0].idx; return idx; } @@ -31,7 +35,7 @@ export default class EmailAddresses { public async getIdxByAddress(tr: Transaction, local: string, domain: string): Promise { const query = 'SELECT idx FROM email_addresses WHERE LOWER(address_local) = LOWER($1)' + ' AND LOWER(address_domain) = LOWER($2)'; - const result = await tr.query(query, [local, domain]); + const result = await tr.query<{ idx: number }>(query, [local, domain]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -45,7 +49,7 @@ export default class EmailAddresses { public async isValidatedEmail(tr: Transaction, emailAddressIdx: number): Promise { const query = 'SELECT owner_idx FROM email_addresses WHERE idx = $1'; - const result = await tr.query(query, [emailAddressIdx]); + const result = await tr.query<{ owner_idx: number }>(query, [emailAddressIdx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -63,7 +67,7 @@ export default class EmailAddresses { const randomBytes = await this.asyncRandomBytes(32); const token = randomBytes.toString('hex'); const expires = moment().add(1, 'day').toDate(); - const result = await tr.query(query, [emailIdx, token, expires]); + await tr.query(query, [emailIdx, token, expires]); return token; } @@ -75,7 +79,7 @@ export default class EmailAddresses { public async getResendCount(tr: Transaction, token: string): Promise { const query = 'SELECT resend_count FROM email_verification_tokens WHERE token = $1'; - const result = await tr.query(query, [token]); + const result = await tr.query<{ resend_count: number }>(query, [token]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -86,11 +90,11 @@ export default class EmailAddresses { const query = 'SELECT e.address_local AS address_local, e.address_domain AS address_domain' + ' FROM email_addresses AS e' + ' INNER JOIN email_verification_tokens AS v ON v.token = $1 AND v.email_idx = e.idx'; - const result = await tr.query(query, [token]); + const result = await tr.query(query, [token]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } - const ret: EmailAddress = { + const ret = { local: result.rows[0].address_local, domain: result.rows[0].address_domain, }; @@ -99,7 +103,7 @@ export default class EmailAddresses { public async removeToken(tr: Transaction, token: string): Promise { const query = 'DELETE FROM email_verification_tokens WHERE token = $1 RETURNING idx'; - const result = await tr.query(query, [token]); + const result = await tr.query<{ idx: number }>(query, [token]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -108,7 +112,7 @@ export default class EmailAddresses { public async ensureTokenNotExpired(tr: Transaction, token: string): Promise { const query = 'SELECT expires FROM email_verification_tokens WHERE token = $1'; - const result = await tr.query(query, [token]); + const result = await tr.query<{ expires: string }>(query, [token]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -125,7 +129,7 @@ export default class EmailAddresses { ownerIdx: number, ): Promise> { const query = 'SELECT address_local, address_domain FROM email_addresses WHERE owner_idx = $1'; - const result = await tr.query(query, [ownerIdx]); + const result = await tr.query(query, [ownerIdx]); return result.rows.map(row => ({ local: row.address_local, diff --git a/src/model/groups.ts b/src/model/groups.ts index fcf4f80..c3cd4f4 100644 --- a/src/model/groups.ts +++ b/src/model/groups.ts @@ -1,8 +1,16 @@ import { NoSuchEntryError } from './errors'; -import Model from './model'; import Transaction from './transaction'; import { Translation } from './translation'; -import { User } from './users'; + +interface GroupRow { + idx: number; + owner_group_idx: number | null; + name_ko: string; + name_en: string; + description_ko: string; + description_en: string; + identifier: string; +} export interface Group { idx: number; @@ -12,6 +20,19 @@ export interface Group { identifier: string; } +interface GroupUserInfoRow { + idx: number; + name_ko: string; + name_en: string; + description_ko: string; + description_en: string; + identifier: string; + is_pending: boolean; + is_member: boolean; + is_direct_member: boolean; + is_owner: boolean; +} + export interface GroupUserInfo { idx: number; name: Translation; @@ -27,13 +48,18 @@ interface GroupReachable { [groupIdx: number]: Array; } +interface GroupRelationshipRow { + supergroup_idx: number; + subgroup_idx: number; +} + export interface GroupRelationship { supergroupIdx: number; subgroupIdx: number; } export default class Groups { - constructor(private readonly model: Model) { + constructor() { } public async create( @@ -44,7 +70,7 @@ export default class Groups { ): Promise { const query = 'INSERT INTO groups(name_ko, name_en, description_ko, ' + 'description_en, identifier) VALUES ($1, $2, $3, $4, $5) RETURNING idx'; - const result = await tr.query(query, [ + const result = await tr.query<{ idx: number }>(query, [ name.ko, name.en, description.ko, @@ -57,7 +83,7 @@ export default class Groups { public async delete(tr: Transaction, groupIdx: number): Promise { const query = 'DELETE FROM groups WHERE idx = $1 RETURNING idx'; - const result = await tr.query(query, [groupIdx]); + const result = await tr.query<{ idx: number }>(query, [groupIdx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -67,13 +93,13 @@ export default class Groups { public async getGroupReachableArray(tr: Transaction, groupIdx: number): Promise> { const query = 'SELECT subgroup_idx FROM group_reachable_cache WHERE supergroup_idx = $1'; - const result = await tr.query(query, [groupIdx]); + const result = await tr.query<{ subgroup_idx: number }>(query, [groupIdx]); return result.rows.map(row => row.subgroup_idx); } public async getByIdx(tr: Transaction, idx: number): Promise { const query = 'SELECT * FROM groups WHERE idx = $1'; - const result = await tr.query(query, [idx]); + const result = await tr.query(query, [idx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -116,14 +142,14 @@ export default class Groups { WHERE g.owner_group_idx IS NOT NULL ORDER BY g.idx, umem.user_idx `; - const result = await tr.query(query, [userIdx]); + const result = await tr.query(query, [userIdx]); return result.rows.map(row => this.rowToGroupUserInfo(row)); } public async checkOwner(tr: Transaction, groupIdx: number, userIdx: number): Promise { const query = 'SELECT EXISTS (SELECT 1 FROM user_memberships mem INNER JOIN groups g ' + 'ON g.owner_group_idx = mem.group_idx WHERE mem.user_idx = $1 AND g.idx = $2)'; - const result = await tr.query(query, [userIdx, groupIdx]); + const result = await tr.query<{ exists: boolean }>(query, [userIdx, groupIdx]); return result.rows[0].exists; } @@ -134,14 +160,14 @@ export default class Groups { ): Promise { const query = 'INSERT INTO group_relations(supergroup_idx, subgroup_idx) ' + 'VALUES ($1, $2) RETURNING idx'; - const result = await tr.query(query, [supergroupIdx, subgroupIdx]); + const result = await tr.query<{ idx: number }>(query, [supergroupIdx, subgroupIdx]); await this.updateGroupReachableCache(tr); return result.rows[0].idx; } public async deleteGroupRelation(tr: Transaction, groupRelationIdx: number): Promise { const query = 'DELETE FROM group_relations WHERE idx = $1 RETURNING idx'; - const result = await tr.query(query, [groupRelationIdx]); + const result = await tr.query<{ idx: number }>(query, [groupRelationIdx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -191,7 +217,7 @@ export default class Groups { private async getAllGroupRelation(tr: Transaction): Promise> { const query = 'SELECT supergroup_idx, subgroup_idx FROM group_relations'; - const result = await tr.query(query); + const result = await tr.query(query); return result.rows.map(row => this.rowToGroupRelation(row)); } @@ -220,12 +246,12 @@ export default class Groups { private async getAllIdx(tr: Transaction): Promise> { const query = 'SELECT idx FROM groups'; - const result = await tr.query(query); + const result = await tr.query<{ idx: number }>(query); return result.rows.map(row => row.idx); } - private rowToGroup(row: any): Group { + private rowToGroup(row: GroupRow): Group { return { idx: row.idx, ownerGroupIdx: row.owner_group_idx, @@ -241,7 +267,7 @@ export default class Groups { }; } - private rowToGroupUserInfo(row: any): GroupUserInfo { + private rowToGroupUserInfo(row: GroupUserInfoRow): GroupUserInfo { return { idx: row.idx, name: { @@ -260,7 +286,7 @@ export default class Groups { }; } - private rowToGroupRelation(row: any): GroupRelationship { + private rowToGroupRelation(row: GroupRelationshipRow): GroupRelationship { return { supergroupIdx: row.supergroup_idx, subgroupIdx: row.subgroup_idx, diff --git a/src/model/hosts.ts b/src/model/hosts.ts index 4e1bd8d..9cb6703 100644 --- a/src/model/hosts.ts +++ b/src/model/hosts.ts @@ -2,6 +2,14 @@ import { AuthorizationError, NoSuchEntryError } from './errors'; import Model from './model'; import Transaction from './transaction'; +interface HostRow { + idx: number; + name: string; + host: string; + host_group: number; + host_pubkey?: Buffer | null; +} + export interface Host { idx: number; name: string; @@ -10,6 +18,12 @@ export interface Host { hostPubkey?: Buffer; } +interface HostGroupRow { + idx: number; + name: string; + required_permission: number | null; +} + export interface HostGroup { idx: number; name: string; @@ -27,7 +41,7 @@ export default class Hosts { pubkey?: Uint8Array, ): Promise { const query = 'INSERT INTO hosts(name, host, host_pubkey) VALUES ($1, $2, $3) RETURNING idx'; - const result = await tr.query(query, [name, host, pubkey]); + const result = await tr.query<{ idx: number }>(query, [name, host, pubkey]); return result.rows[0].idx; } @@ -41,7 +55,7 @@ export default class Hosts { public async addHostGroup(tr: Transaction, name: string): Promise { const query = 'INSERT INTO host_groups(name) VALUES ($1) RETURNING idx'; - const result = await tr.query(query, [name]); + const result = await tr.query<{ idx: number }>(query, [name]); return result.rows[0].idx; } @@ -82,7 +96,7 @@ export default class Hosts { // forbid inet authentication with pubkey registered const query = `SELECT idx, name, host, host_group, host_pubkey FROM hosts WHERE host(host) = $1 ${unsafeBypassPubkey ? '' : 'AND host_pubkey IS NULL'}`; - const result = await tr.query(query, [inet]); + const result = await tr.query(query, [inet]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -92,7 +106,7 @@ export default class Hosts { public async getHostByPubkey(tr: Transaction, pubkey: Uint8Array): Promise { const query = 'SELECT idx, name, host, host_group, host_pubkey FROM hosts WHERE host_pubkey = $1'; - const result = await tr.query(query, [pubkey]); + const result = await tr.query(query, [pubkey]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -101,7 +115,7 @@ export default class Hosts { public async getHostGroupByIdx(tr: Transaction, hostGroupIdx: number): Promise { const query = 'SELECT idx, name, required_permission FROM host_groups WHERE idx = $1'; - const result = await tr.query(query, [hostGroupIdx]); + const result = await tr.query(query, [hostGroupIdx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -128,7 +142,7 @@ export default class Hosts { } } - private rowToHost(row: any): Host { + private rowToHost(row: HostRow): Host { return { idx: row.idx, name: row.name, @@ -138,7 +152,7 @@ export default class Hosts { }; } - private rowToHostGroup(row: any): HostGroup { + private rowToHostGroup(row: HostGroupRow): HostGroup { return { idx: row.idx, name: row.name, diff --git a/src/model/model.ts b/src/model/model.ts index 8da06fb..34a9fa4 100644 --- a/src/model/model.ts +++ b/src/model/model.ts @@ -33,10 +33,10 @@ export default class Model { this.pgPool = new pg.Pool(this.pgConfig); this.users = new Users(this); - this.emailAddresses = new EmailAddresses(this); - this.groups = new Groups(this); + this.emailAddresses = new EmailAddresses(); + this.groups = new Groups(); this.permissions = new Permissions(this); - this.shells = new Shells(this); + this.shells = new Shells(); this.hosts = new Hosts(this); this.oauth = new OAuth(); } diff --git a/src/model/oauth.ts b/src/model/oauth.ts index a85f9fa..058079f 100644 --- a/src/model/oauth.ts +++ b/src/model/oauth.ts @@ -6,11 +6,13 @@ import Transaction from './transaction'; export default class OAuth { public async getClientById(tr: Transaction, id: string): Promise { - const client = await tr.query( + const client = await tr.query< + { client_id: string; client_secret: string; client_name: string } + >( 'SELECT client_id, client_secret, client_name FROM oauth_clients WHERE client_id = $1', [id], ); - const redirectUri = await tr.query( + const redirectUri = await tr.query<{ redirect_uri: string }>( 'SELECT redirect_uri FROM oauth_client_redirect_uris WHERE client_id = $1', [id], ); @@ -28,9 +30,12 @@ export default class OAuth { } public async isFirstParty(tr: Transaction, id: string): Promise { - const result = await tr.query('SELECT first_party FROM oauth_clients WHERE client_id = $1', [ - id, - ]); + const result = await tr.query<{ first_party: boolean }>( + 'SELECT first_party FROM oauth_clients WHERE client_id = $1', + [ + id, + ], + ); return Boolean(result.rows[0]?.first_party); } } diff --git a/src/model/permissions.ts b/src/model/permissions.ts index 728019c..511dafd 100644 --- a/src/model/permissions.ts +++ b/src/model/permissions.ts @@ -14,13 +14,18 @@ export default class Permissions { ): Promise { const query = 'INSERT INTO permissions(name_ko, name_en, description_ko, ' + 'description_en) VALUES ($1, $2, $3, $4) RETURNING idx'; - const result = await tr.query(query, [name.ko, name.en, description.ko, description.en]); + const result = await tr.query<{ idx: number }>(query, [ + name.ko, + name.en, + description.ko, + description.en, + ]); return result.rows[0].idx; } public async delete(tr: Transaction, permissionIdx: number): Promise { const query = 'DELETE FROM permissions WHERE idx = $1 RETURNING idx'; - const result = await tr.query(query, [permissionIdx]); + const result = await tr.query<{ idx: number }>(query, [permissionIdx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -34,13 +39,13 @@ export default class Permissions { ): Promise { const query = 'INSERT INTO permission_requirements(group_idx, permission_idx) ' + 'VALUES ($1, $2) RETURNING idx'; - const result = await tr.query(query, [groupIdx, permissionIdx]); + const result = await tr.query<{ idx: number }>(query, [groupIdx, permissionIdx]); return result.rows[0].idx; } public async deletePermissionRequirement(tr: Transaction, idx: number): Promise { const query = 'DELETE FROM permission_requirements WHERE idx = $1 RETURNING idx'; - const result = await tr.query(query, [idx]); + const result = await tr.query<{ idx: number }>(query, [idx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -49,7 +54,7 @@ export default class Permissions { public async getAllPermissionRequirements(tr: Transaction, idx: number): Promise> { const query = 'SELECT group_idx FROM permission_requirements WHERE permission_idx = $1'; - const result = await tr.query(query, [idx]); + const result = await tr.query<{ group_idx: number }>(query, [idx]); return result.rows.map(row => row.group_idx); } diff --git a/src/model/shells.ts b/src/model/shells.ts index 6868ef9..f93e378 100644 --- a/src/model/shells.ts +++ b/src/model/shells.ts @@ -1,23 +1,22 @@ -import Model from './model'; import Transaction from './transaction'; export default class Shells { - constructor(private readonly model: Model) { + constructor() { } public async getShells(tr: Transaction): Promise> { const query = 'SELECT shell from shells'; - const result = await tr.query(query); + const result = await tr.query<{ shell: string }>(query); return result.rows.map(row => row.shell); } public async addShell(tr: Transaction, shell: string): Promise { const query = 'INSERT INTO shells(shell) VALUES ($1)'; - const result = await tr.query(query, [shell]); + await tr.query(query, [shell]); } public async removeShell(tr: Transaction, shell: string): Promise { const query = 'DELETE FROM shells WHERE shell = $1'; - const result = await tr.query(query, [shell]); + await tr.query(query, [shell]); } } diff --git a/src/model/transaction.ts b/src/model/transaction.ts index 05ac7c3..e921915 100644 --- a/src/model/transaction.ts +++ b/src/model/transaction.ts @@ -12,7 +12,10 @@ export default class Transaction { ) { } - public query(query: string, values?: Array): Promise { + public query( + query: string, + values?: Array, + ): Promise> { this.ensureNotTerminated(); return this.client.query(query, values); } diff --git a/src/model/users.ts b/src/model/users.ts index 25444d6..eed2dda 100644 --- a/src/model/users.ts +++ b/src/model/users.ts @@ -15,6 +15,15 @@ import Transaction from './transaction'; // see language enum in schema.sql export type Language = 'ko' | 'en'; +interface UserRow { + idx: number; + username: string | null; + name: string; + uid: number; + shell: string; + preferred_language: Language; +} + export interface User { idx: number; username: string | null; @@ -24,6 +33,12 @@ export interface User { preferredLanguage: Language; } +interface UserMembershipRow { + idx: number; + user_idx: number; + group_idx: number; +} + export interface UserMembership { idx: number; userIdx: number; @@ -54,7 +69,7 @@ export default class Users { + 'VALUES ($1, $2, $3, $4, $5, $6) RETURNING idx'; const passwordDigest = await argon2.hash(password); const uid = await this.generateUid(tr); - const result = await tr.query(query, [ + const result = await tr.query<{ idx: number }>(query, [ username, passwordDigest, name, @@ -69,7 +84,7 @@ export default class Users { public async delete(tr: Transaction, userIdx: number): Promise { const query = 'DELETE FROM users WHERE idx = $1 RETURNING idx'; - const result = await tr.query(query, [userIdx]); + const result = await tr.query<{ idx: number }>(query, [userIdx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -80,7 +95,7 @@ export default class Users { public async getAll(tr: Transaction): Promise> { const query = 'SELECT idx, username, name, uid, shell FROM users'; - const result = await tr.query(query); + const result = await tr.query(query); const users: Array = []; result.rows.forEach(row => users.push(this.rowToUser(row))); return users; @@ -88,7 +103,7 @@ export default class Users { public async getAllSorted(tr: Transaction): Promise> { const query = 'SELECT idx, username, name, uid, shell FROM users ORDER BY uid'; - const result = await tr.query(query); + const result = await tr.query(query); const users: Array = []; result.rows.forEach(row => users.push(this.rowToUser(row))); return users; @@ -114,7 +129,7 @@ export default class Users { public async getByUsername(tr: Transaction, username: string): Promise { const query = 'SELECT idx, username, name, uid, shell FROM users WHERE username = $1'; - const result = await tr.query(query, [username]); + const result = await tr.query(query, [username]); if (result.rows.length !== 1) { throw new NoSuchEntryError(); } @@ -123,7 +138,7 @@ export default class Users { public async getByUserIdx(tr: Transaction, userIdx: number): Promise { const query = 'SELECT idx, username, name, uid, shell FROM users WHERE idx = $1'; - const result = await tr.query(query, [userIdx]); + const result = await tr.query(query, [userIdx]); if (result.rows.length !== 1) { throw new NoSuchEntryError(); } @@ -137,7 +152,7 @@ export default class Users { ): Promise { const query = 'SELECT owner_idx FROM email_addresses WHERE LOWER(address_local) = LOWER($1) AND address_domain = $2'; - const result = await tr.query(query, [emailLocal, emailDomain]); + const result = await tr.query<{ owner_idx: number }>(query, [emailLocal, emailDomain]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -146,7 +161,10 @@ export default class Users { public async authenticate(tr: Transaction, username: string, password: string): Promise { const query = 'SELECT idx, password_digest, activated FROM users WHERE username = $1'; - const result = await tr.query(query, [username]); + const result = await tr.query<{ idx: number; password_digest: string; activated: boolean }>( + query, + [username], + ); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -178,23 +196,23 @@ export default class Users { public async updateLastLoginAt(tr: Transaction, userIdx: number): Promise { const query = 'UPDATE users SET last_login_at = NOW() WHERE idx = $1'; - const result = await tr.query(query, [userIdx]); + await tr.query(query, [userIdx]); } public async activate(tr: Transaction, userIdx: number): Promise { const query = 'UPDATE users SET activated = TRUE WHERE idx = $1'; - const result = await tr.query(query, [userIdx]); + await tr.query(query, [userIdx]); } public async deactivate(tr: Transaction, userIdx: number): Promise { const query = 'UPDATE users SET activated = FALSE WHERE idx = $1'; - const result = await tr.query(query, [userIdx]); + await tr.query(query, [userIdx]); } public async generateUid(tr: Transaction): Promise { tr.ensureHasAccessExclusiveLock('users'); const minUid = this.model.config.posix.minUid; - const getNewUidResult = await tr.query( + const getNewUidResult = await tr.query<{ uid: number }>( 'SELECT b.uid + 1 AS uid FROM users AS a RIGHT OUTER JOIN ' + 'users AS b ON a.uid = b.uid + 1 WHERE a.uid IS NULL AND b.uid + 1 >= $1 ORDER BY b.uid LIMIT 1', [minUid], @@ -210,7 +228,7 @@ export default class Users { const randomBytes = await this.asyncRandomBytes(32); const token = randomBytes.toString('hex'); const expires = moment().add(1, 'day').toDate(); - const result = await tr.query(query, [userIdx, token, expires]); + await tr.query(query, [userIdx, token, expires]); return token; } @@ -222,7 +240,7 @@ export default class Users { public async getResendCount(tr: Transaction, token: string): Promise { const query = 'SELECT resend_count FROM password_change_tokens WHERE token = $1'; - const result = await tr.query(query, [token]); + const result = await tr.query<{ resend_count: number }>(query, [token]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -231,7 +249,7 @@ export default class Users { public async removeToken(tr: Transaction, token: string): Promise { const query = 'DELETE FROM password_change_tokens WHERE token = $1 RETURNING idx'; - const result = await tr.query(query, [token]); + const result = await tr.query<{ idx: number }>(query, [token]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -240,7 +258,7 @@ export default class Users { public async ensureTokenNotExpired(tr: Transaction, token: string): Promise { const query = 'SELECT expires FROM password_change_tokens WHERE token = $1'; - const result = await tr.query(query, [token]); + const result = await tr.query<{ expires: string }>(query, [token]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -259,7 +277,7 @@ export default class Users { ): Promise { const passwordDigest = await argon2.hash(newPassword); const query = 'UPDATE users SET password_digest = $1 WHERE idx = $2 RETURNING idx'; - const result = await tr.query(query, [passwordDigest, userIdx]); + const result = await tr.query<{ idx: number }>(query, [passwordDigest, userIdx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -268,7 +286,7 @@ export default class Users { public async changeShell(tr: Transaction, userIdx: number, shell: string): Promise { const query = 'UPDATE users SET shell = $1 WHERE idx = $2 RETURNING idx'; - const result = await tr.query(query, [shell, userIdx]); + const result = await tr.query<{ idx: number }>(query, [shell, userIdx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -278,7 +296,7 @@ export default class Users { public async getShell(tr: Transaction, userIdx: number): Promise { const query = 'SELECT shell FROM users WHERE idx = $1'; - const result = await tr.query(query, [userIdx]); + const result = await tr.query<{ shell: string }>(query, [userIdx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -291,13 +309,13 @@ export default class Users { groupIdx: number, ): Promise { const query = 'INSERT INTO user_memberships(user_idx, group_idx) VALUES ($1, $2) RETURNING idx'; - const result = await tr.query(query, [userIdx, groupIdx]); + const result = await tr.query<{ idx: number }>(query, [userIdx, groupIdx]); return result.rows[0].idx; } public async deleteUserMembership(tr: Transaction, userMembershipIdx: number): Promise { const query = 'DELETE FROM user_memberships WHERE idx = $1 RETURNING idx'; - const result = await tr.query(query, [userMembershipIdx]); + const result = await tr.query<{ idx: number }>(query, [userMembershipIdx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -326,7 +344,7 @@ export default class Users { groupIdx: number, ): Promise { const query = 'SELECT idx FROM user_memberships WHERE user_idx = $1 AND group_idx = $2'; - const result = await tr.query(query, [userIdx, groupIdx]); + const result = await tr.query<{ idx: number }>(query, [userIdx, groupIdx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -338,7 +356,7 @@ export default class Users { userIdx: number, ): Promise> { const query = 'SELECT idx, user_idx, group_idx FROM user_memberships WHERE user_idx = $1'; - const result = await tr.query(query, [userIdx]); + const result = await tr.query(query, [userIdx]); return result.rows.map(row => this.rowToUserMembership(row)); } @@ -358,7 +376,7 @@ export default class Users { query += ' LIMIT $2 OFFSET $3'; params.push(pagination.count, pagination.start); } - const result = await tr.query(query, params); + const result = await tr.query(query, params); return result.rows.map(row => this.rowToUser(row)); } @@ -369,7 +387,7 @@ export default class Users { ): Promise { const query = 'INSERT INTO pending_user_memberships (user_idx, group_idx) VALUES ($1, $2) RETURNING idx'; - const result = await tr.query(query, [userIdx, groupIdx]); + const result = await tr.query<{ idx: number }>(query, [userIdx, groupIdx]); return result.rows[0].idx; } @@ -378,7 +396,7 @@ export default class Users { userMembershipIdx: number, ): Promise { const query = 'DELETE FROM pending_user_memberships WHERE idx = $1 RETURNING idx'; - const result = await tr.query(query, [userMembershipIdx]); + const result = await tr.query<{ idx: number }>(query, [userMembershipIdx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -407,7 +425,7 @@ export default class Users { groupIdx: number, ): Promise { const query = 'SELECT idx FROM pending_user_memberships WHERE user_idx = $1 AND group_idx = $2'; - const result = await tr.query(query, [userIdx, groupIdx]); + const result = await tr.query<{ idx: number }>(query, [userIdx, groupIdx]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -448,7 +466,7 @@ export default class Users { const query = 'SELECT u.* FROM pending_user_memberships AS pum ' + 'INNER JOIN users AS u ON pum.user_idx = u.idx ' + 'WHERE pum.group_idx = $1 ORDER BY pum.idx'; - const result = await tr.query(query, [groupIdx]); + const result = await tr.query(query, [groupIdx]); return result.rows.map(row => this.rowToUser(row)); } @@ -471,7 +489,7 @@ export default class Users { public async getUserIdxByPasswordToken(tr: Transaction, token: string): Promise { const query = 'SELECT user_idx FROM password_change_tokens WHERE token = $1'; - const result = await tr.query(query, [token]); + const result = await tr.query<{ user_idx: number }>(query, [token]); if (result.rows.length === 0) { throw new NoSuchEntryError(); } @@ -485,7 +503,7 @@ export default class Users { const query = 'SELECT sn.student_number FROM users u ' + 'LEFT OUTER JOIN student_numbers AS sn ON sn.owner_idx = u.idx ' + 'WHERE u.idx = $1'; - const result = await tr.query(query, [userIdx]); + const result = await tr.query<{ idx: number; student_number: string }>(query, [userIdx]); return result.rows.map(row => row.student_number); } @@ -496,7 +514,7 @@ export default class Users { const query = 'SELECT u.idx, sn.student_number FROM users u ' + 'LEFT OUTER JOIN student_numbers AS sn ON sn.owner_idx = u.idx ' + 'WHERE u.idx = ANY($1)'; - const result = await tr.query(query, [userIndices]); + const result = await tr.query<{ idx: number; student_number: string }>(query, [userIndices]); const map = new Map>(); for (const row of result.rows) { const idx = row.idx; @@ -516,11 +534,11 @@ export default class Users { ): Promise { const query = 'INSERT INTO student_numbers(student_number, owner_idx) VALUES ($1, $2) RETURNING idx'; - const result = await tr.query(query, [studentNumber, userIdx]); + const result = await tr.query<{ idx: number }>(query, [studentNumber, userIdx]); return result.rows[0].idx; } - private rowToUser(row: any): User { + private rowToUser(row: UserRow): User { return { idx: row.idx, username: row.username, @@ -531,7 +549,7 @@ export default class Users { }; } - private rowToUserMembership(row: any): UserMembership { + private rowToUserMembership(row: UserMembershipRow): UserMembership { return { idx: row.idx, userIdx: row.user_idx, diff --git a/src/oidc/configuration.ts b/src/oidc/configuration.ts index 62c55aa..0175ed0 100644 --- a/src/oidc/configuration.ts +++ b/src/oidc/configuration.ts @@ -19,7 +19,7 @@ export default function createOIDCConfig(model: Model, oidcConfig: Config['oidc' return { adapter, - findAccount: async (ctx, id) => { + findAccount: async (_ctx, id) => { const [username, groups, name, student_id, email] = await model.pgDo(async tr => { // get name and username const userResult = await model.users.getByUserIdx(tr, Number(id)); @@ -53,9 +53,12 @@ export default function createOIDCConfig(model: Model, oidcConfig: Config['oidc' // get groups const groupSet = await model.users.getUserReachableGroups(tr, Number(id)); - const groupResult = await tr.query('SELECT identifier FROM groups WHERE idx = ANY($1)', [[ - ...groupSet, - ]]); + const groupResult = await tr.query<{ identifier: string }>( + 'SELECT identifier FROM groups WHERE idx = ANY($1)', + [[ + ...groupSet, + ]], + ); const groups = groupResult.rows.map(r => r.identifier); return [username, groups, name, student_id, email]; diff --git a/src/oidc/routes.ts b/src/oidc/routes.ts index 2e00c0f..e98cbe4 100644 --- a/src/oidc/routes.ts +++ b/src/oidc/routes.ts @@ -6,7 +6,6 @@ import type OIDCProvider from 'oidc-provider'; import * as z from 'zod'; import Model from '../model/model'; -import OIDCAccount from './account'; const loginSchema = z.object({ username: z.string().nonempty(), diff --git a/test/api/email.test.ts b/test/api/email.test.ts index 86e6faa..d2529a2 100644 --- a/test/api/email.test.ts +++ b/test/api/email.test.ts @@ -27,7 +27,7 @@ test('check token api', async t => { await model.pgDo(async tr => { const emailAddressIdx = await model.emailAddresses.create(tr, local, domain); await model.emailAddresses.generateVerificationToken(tr, emailAddressIdx); - const result = await tr.query( + const result = await tr.query<{ token: string }>( 'SELECT token FROM email_verification_tokens WHERE email_idx = $1', [emailAddressIdx], ); diff --git a/test/api/groups.test.ts b/test/api/groups.test.ts index 2057138..5496ff4 100644 --- a/test/api/groups.test.ts +++ b/test/api/groups.test.ts @@ -2,7 +2,7 @@ import test from 'ava'; import * as request from 'supertest'; import { v4 as uuid } from 'uuid'; import { GroupUserInfo } from '../../src/model/groups'; -import { app, config, model } from '../_setup'; +import { app, model } from '../_setup'; import { createGroup, createGroupRelation, createUser } from '../_test_utils'; test('group listing', async t => { @@ -26,7 +26,7 @@ test('group listing', async t => { await model.groups.setOwnerGroup(tr, indirectGroupIdx, noneGroupIdx); await model.groups.setOwnerGroup(tr, pendingGroupIdx, noneGroupIdx); await model.groups.setOwnerGroup(tr, ownerGroupIdx, memberGroupIdx); - await await createGroupRelation(tr, model, memberGroupIdx, indirectGroupIdx); + await createGroupRelation(tr, model, memberGroupIdx, indirectGroupIdx); const userIdx = await model.users.create(tr, username, password, uuid(), '/bin/bash', 'en'); await model.users.addUserMembership(tr, userIdx, memberGroupIdx); @@ -96,7 +96,7 @@ test('member listing', async t => { t.is(response.status, 200); response = await agent.get(`/api/group/${groupIdx}/members`); - t.is(response.status, 401); + t.is(response.status, 403); await model.pgDo(async tr => { await model.users.addUserMembership(tr, userIdx, ownerGroupIdx); @@ -144,7 +144,7 @@ test('pending listing', async t => { t.is(response.status, 200); response = await agent.get(`/api/group/${groupIdx}/pending`); - t.is(response.status, 401); + t.is(response.status, 403); await model.pgDo(async tr => { await model.users.addUserMembership(tr, userIdx, groupIdx); @@ -235,7 +235,11 @@ test('accept group requests', async t => { const agent = request.agent(app); let response; - response = await agent.post(`/api/group/${groupIdx}/accept`).send([]); + await model.pgDo(async tr => { + await model.users.addPendingUserMembership(tr, memberIdx, groupIdx); + }); + + response = await agent.post(`/api/group/${groupIdx}/accept`).send([memberIdx]); t.is(response.status, 401); response = await agent.post('/api/login').send({ @@ -244,18 +248,14 @@ test('accept group requests', async t => { }); t.is(response.status, 200); - response = await agent.post(`/api/group/${groupIdx}/accept`).send([]); - t.is(response.status, 401); - - await model.pgDo(async tr => { - await model.groups.setOwnerGroup(tr, groupIdx, ownerGroupIdx); - }); + response = await agent.post(`/api/group/${groupIdx}/accept`).send([memberIdx]); + t.is(response.status, 403); response = await agent.post(`/api/group/${groupIdx}/accept`).send([]); - t.is(response.status, 200); + t.is(response.status, 400); await model.pgDo(async tr => { - await model.users.addPendingUserMembership(tr, memberIdx, groupIdx); + await model.groups.setOwnerGroup(tr, groupIdx, ownerGroupIdx); }); response = await agent.post(`/api/group/${groupIdx}/accept`).send([memberIdx]); @@ -287,7 +287,11 @@ test('reject group requests', async t => { const agent = request.agent(app); let response; - response = await agent.post(`/api/group/${groupIdx}/reject`).send([]); + await model.pgDo(async tr => { + await model.users.addPendingUserMembership(tr, memberIdx, groupIdx); + }); + + response = await agent.post(`/api/group/${groupIdx}/reject`).send([memberIdx]); t.is(response.status, 401); response = await agent.post('/api/login').send({ @@ -296,18 +300,14 @@ test('reject group requests', async t => { }); t.is(response.status, 200); - response = await agent.post(`/api/group/${groupIdx}/reject`).send([]); - t.is(response.status, 401); - - await model.pgDo(async tr => { - await model.groups.setOwnerGroup(tr, groupIdx, ownerGroupIdx); - }); + response = await agent.post(`/api/group/${groupIdx}/reject`).send([memberIdx]); + t.is(response.status, 403); response = await agent.post(`/api/group/${groupIdx}/reject`).send([]); - t.is(response.status, 200); + t.is(response.status, 400); await model.pgDo(async tr => { - await model.users.addPendingUserMembership(tr, memberIdx, groupIdx); + await model.groups.setOwnerGroup(tr, groupIdx, ownerGroupIdx); }); response = await agent.post(`/api/group/${groupIdx}/reject`).send([memberIdx]); diff --git a/test/api/login.test.ts b/test/api/login.test.ts index 3065c02..80ca734 100644 --- a/test/api/login.test.ts +++ b/test/api/login.test.ts @@ -1,7 +1,7 @@ import test from 'ava'; import moment from 'moment'; import * as request from 'supertest'; -import * as tweetnacl from 'tweetnacl'; +import tweetnacl from 'tweetnacl'; import { v4 as uuid } from 'uuid'; import { app, config, model } from '../_setup'; @@ -41,7 +41,7 @@ test('test login with credential', async t => { await model.pgDo(async tr => { const query = 'SELECT last_login_at FROM users WHERE idx = $1'; - const result = await tr.query(query, [userIdx]); + const result = await tr.query<{ last_login_at: string }>(query, [userIdx]); const lastLogin = moment(result.rows[0].last_login_at); t.true(lastLogin.isBetween(moment().subtract(10, 'seconds'), moment().add(10, 'seconds'))); }); @@ -238,12 +238,11 @@ test.serial('test PAM login with credential and pubkey', async t => { test('test checkLogin', async t => { let username = ''; let password = ''; - let userIdx = -1; await model.pgDo(async tr => { username = uuid(); password = uuid(); - userIdx = await model.users.create( + await model.users.create( tr, username, password, diff --git a/test/api/shells.test.ts b/test/api/shells.test.ts index 488dde4..a5475db 100644 --- a/test/api/shells.test.ts +++ b/test/api/shells.test.ts @@ -4,11 +4,9 @@ import { v4 as uuid } from 'uuid'; import { app, model } from '../_setup'; test('test getShells', async t => { - let result; const newShell = uuid(); await model.pgDo(async tr => { await model.shells.addShell(tr, newShell); - result = await model.shells.getShells(tr); }); const agent = request.agent(app); diff --git a/test/api/users.test.ts b/test/api/users.test.ts index 6e3d448..11075f7 100644 --- a/test/api/users.test.ts +++ b/test/api/users.test.ts @@ -29,7 +29,7 @@ test('create user step by step', async t => { let token = ''; await model.pgDo(async tr => { const idx = await model.emailAddresses.getIdxByAddress(tr, emailLocal, emailDomain); - const result = await tr.query( + const result = await tr.query<{ token: string }>( 'SELECT token FROM email_verification_tokens WHERE email_idx = $1', [idx], ); @@ -44,7 +44,7 @@ test('create user step by step', async t => { studentNumbers, }); // request without session token will be fail - t.is(response.status, 401); + t.is(response.status, 400); response = await agent.post('/api/email/check-token').send({ token, @@ -168,9 +168,12 @@ test('change password', async t => { let token; await model.pgDo(async tr => { - const result = await tr.query('SELECT token FROM password_change_tokens WHERE user_idx = $1', [ - userIdx, - ]); + const result = await tr.query<{ token: string }>( + 'SELECT token FROM password_change_tokens WHERE user_idx = $1', + [ + userIdx, + ], + ); t.is(result.rows.length, 1); token = result.rows[0].token; }); @@ -234,10 +237,9 @@ test('verification email resend limit', async t => { const emailLocal = uuid(); const emailDomain = 'snu.ac.kr'; const resendLimit = config.email.resendLimit; - let emailIdx = -1; await model.pgDo(async tr => { - emailIdx = await model.emailAddresses.create(tr, emailLocal, emailDomain); + await model.emailAddresses.create(tr, emailLocal, emailDomain); }); const agent = request.agent(app); @@ -327,7 +329,7 @@ async function verifyResult(t: ExecutionContext, indcies: Array) { } } -async function cleanUpUsers(t: ExecutionContext, indices: Array) { +async function cleanUpUsers(_t: ExecutionContext, indices: Array) { for (let i = 0; i < NUMBER_OF_USERS_TO_CREATE; i++) { await model.pgDo(async tr => { await model.users.delete(tr, indices[i]); diff --git a/test/model/email_addresses.test.ts b/test/model/email_addresses.test.ts index 79a27a6..8ff6477 100644 --- a/test/model/email_addresses.test.ts +++ b/test/model/email_addresses.test.ts @@ -13,15 +13,15 @@ test('create extra email', async t => { await model.emailAddresses.validate(tr, userIdx, emailAddressIdx); - const query = 'SELECT * FROM email_addresses WHERE idx = $1'; - const result = await tr.query(query, [emailAddressIdx]); + const query = 'SELECT owner_idx FROM email_addresses WHERE idx = $1'; + const result = await tr.query<{ owner_idx: number }>(query, [emailAddressIdx]); t.is(result.rows[0].owner_idx, userIdx); }, ['users']); }); test('generate verification token', async t => { await model.pgDo(async tr => { - const userIdx = await createUser(tr, model); + await createUser(tr, model); const emailAddressIdx = await createEmailAddress(tr, model); await model.emailAddresses.generateVerificationToken(tr, emailAddressIdx); @@ -33,14 +33,14 @@ test('generate verification token', async t => { test('get email address by token', async t => { await model.pgDo(async tr => { - const userIdx = await createUser(tr, model); + await createUser(tr, model); const emailLocal = uuid(); const emailDomain = uuid(); const emailAddressIdx = await model.emailAddresses.create(tr, emailLocal, emailDomain); await model.emailAddresses.generateVerificationToken(tr, emailAddressIdx); - const tokenResult = await tr.query( - 'SELECT * FROM email_verification_tokens WHERE email_idx = $1', + const tokenResult = await tr.query<{ token: string }>( + 'SELECT token FROM email_verification_tokens WHERE email_idx = $1', [emailAddressIdx], ); const token: string = tokenResult.rows[0].token; @@ -80,7 +80,7 @@ test('verification token request with identical email idx', async t => { const newToken = await model.emailAddresses.generateVerificationToken(tr, emailAddressIdx); const query = 'SELECT token FROM email_verification_tokens WHERE email_idx = $1'; - const result = await tr.query(query, [emailAddressIdx]); + const result = await tr.query<{ token: string }>(query, [emailAddressIdx]); const token = result.rows[0].token; t.is(newToken, token); t.not(oldToken, token); @@ -93,7 +93,7 @@ test('token expiration', async t => { const emailDomain = uuid(); const emailAddressIdx = await model.emailAddresses.create(tr, emailLocal, emailDomain); const token = await model.emailAddresses.generateVerificationToken(tr, emailAddressIdx); - const expiryResult = await tr.query( + const expiryResult = await tr.query<{ expires: string }>( 'SELECT expires FROM email_verification_tokens WHERE token = $1', [token], ); @@ -148,7 +148,7 @@ test('reset resend count of expired verification token', async t => { await model.pgDo(async tr => { const emailIdx = await model.emailAddresses.create(tr, uuid(), uuid()); let token = await model.emailAddresses.generateVerificationToken(tr, emailIdx); - const expiryResult = await tr.query( + const expiryResult = await tr.query<{ expires: string }>( 'SELECT expires FROM email_verification_tokens WHERE token = $1', [token], ); diff --git a/test/model/groups.test.ts b/test/model/groups.test.ts index 643b6c9..0ae9f2d 100644 --- a/test/model/groups.test.ts +++ b/test/model/groups.test.ts @@ -136,8 +136,7 @@ test('get group list about user', async t => { test('get reachable group object', async t => { await model.pgDo(async tr => { const g: Array = []; - const range: Array = [...Array(5).keys()]; - for (const _ of range) { + for (let idx = 0; idx < 5; idx++) { g.push(await createGroup(tr, model)); } diff --git a/test/model/hosts.test.ts b/test/model/hosts.test.ts index 3d1f281..229a930 100644 --- a/test/model/hosts.test.ts +++ b/test/model/hosts.test.ts @@ -23,7 +23,7 @@ test('add host and host group', async t => { await model.pgDo(async tr => { const hostIdx = await model.hosts.addHost(tr, name, host); const query = 'SELECT idx FROM hosts WHERE host = $1'; - const result = await tr.query(query, [host]); + const result = await tr.query<{ idx: number }>(query, [host]); t.is(result.rows[0].idx, hostIdx); let byInet = await model.hosts.getHostByInet(tr, host); t.is(byInet.idx, hostIdx); @@ -47,7 +47,7 @@ test('add host with pubkey', async t => { await model.pgDo(async tr => { const hostIdx = await model.hosts.addHost(tr, name, host, keyPair.publicKey); const query = 'SELECT idx FROM hosts WHERE host = $1'; - const result = await tr.query(query, [host]); + const result = await tr.query<{ idx: number }>(query, [host]); t.is(result.rows[0].idx, hostIdx); let byInet = await model.hosts.getHostByPubkey(tr, keyPair.publicKey); t.is(byInet.idx, hostIdx); diff --git a/test/model/model.test.ts b/test/model/model.test.ts index c5eea4d..f502322 100644 --- a/test/model/model.test.ts +++ b/test/model/model.test.ts @@ -49,7 +49,7 @@ test('resolve deadlock', async t => { await model.pgDo(async tr => { for (let table = 1; table <= 2; table++) { for (let idx = 1; idx <= 2; idx++) { - const result = await tr.query( + const result = await tr.query<{ value: number }>( 'SELECT value FROM dead_lock_test_' + table + ' WHERE idx = ' + idx, ); t.is(result.rowCount, 1); @@ -97,7 +97,7 @@ test('resolve serialization failure', async t => { await tr.query('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE'); transactionStages[0]++; await ensureStageReach(1, 1); - const queryResult = await tr.query( + const queryResult = await tr.query<{ value: number }>( 'SELECT value from serialization_error_test WHERE idx = 1', ); t.is(queryResult.rowCount, 1); @@ -114,7 +114,7 @@ test('resolve serialization failure', async t => { await tr.query('SET TRANSACTION ISOLATION LEVEL SERIALIZABLE'); transactionStages[1]++; await ensureStageReach(0, 1); - const queryResult = await tr.query( + const queryResult = await tr.query<{ value: number }>( 'SELECT value from serialization_error_test WHERE idx = 1', ); t.is(queryResult.rowCount, 1); @@ -129,7 +129,7 @@ test('resolve serialization failure', async t => { await Promise.all(promises); await model.pgDo(async tr => { - const queryResult = await tr.query( + const queryResult = await tr.query<{ value: number }>( 'SELECT value from serialization_error_test WHERE idx = 1', ); t.is(queryResult.rowCount, 1); diff --git a/test/model/permissions.test.ts b/test/model/permissions.test.ts index e4e4810..d9420b2 100644 --- a/test/model/permissions.test.ts +++ b/test/model/permissions.test.ts @@ -1,7 +1,5 @@ import test from 'ava'; -import { v4 as uuid } from 'uuid'; -import { NoSuchEntryError } from '../../src/model/errors'; import { Translation } from '../../src/model/translation'; import { model } from '../_setup'; @@ -22,15 +20,15 @@ test('create and delete permissions', async t => { const permissionIdx = await model.permissions.create(tr, name, description); const query = 'SELECT * FROM permissions WHERE idx = $1'; - const result = await tr.query(query, [permissionIdx]); + const result = await tr.query<{ [name: string]: unknown }>(query, [permissionIdx]); t.truthy(result.rows[0]); - t.deepEqual(result.rows[0].name_ko, name.ko); - t.deepEqual(result.rows[0].name_en, name.en); - t.deepEqual(result.rows[0].description_ko, description.ko); - t.deepEqual(result.rows[0].description_en, description.en); + t.is(result.rows[0].name_ko, name.ko); + t.is(result.rows[0].name_en, name.en); + t.is(result.rows[0].description_ko, description.ko); + t.is(result.rows[0].description_en, description.en); - const deletedPermissionIdx = await model.permissions.delete(tr, permissionIdx); + await model.permissions.delete(tr, permissionIdx); const emptyResult = await tr.query(query, [permissionIdx]); t.is(emptyResult.rows.length, 0); @@ -43,13 +41,13 @@ test('create and delete permission requirements', async t => { const permissionIdx = await createPermission(tr, model); const idx = await model.permissions.addPermissionRequirement(tr, groupIdx, permissionIdx); - const query = 'SELECT * FROM permission_requirements WHERE idx = $1'; - const result = await tr.query(query, [idx]); + const query = 'SELECT group_idx FROM permission_requirements WHERE idx = $1'; + const result = await tr.query<{ group_idx: number }>(query, [idx]); t.truthy(result.rows[0]); t.is(result.rows[0].group_idx, groupIdx); - const deletedIdx = await model.permissions.deletePermissionRequirement(tr, idx); + await model.permissions.deletePermissionRequirement(tr, idx); const emptyResult = await tr.query(query, [idx]); t.is(emptyResult.rows.length, 0); @@ -59,8 +57,7 @@ test('create and delete permission requirements', async t => { test('check user permission', async t => { await model.pgDo(async tr => { const g: Array = []; - const range: Array = [...Array(5).keys()]; - for (const _ of range) { + for (let idx = 0; idx < 5; idx++) { g.push(await createGroup(tr, model)); } diff --git a/test/model/users.test.ts b/test/model/users.test.ts index 42be812..bdd0a7e 100644 --- a/test/model/users.test.ts +++ b/test/model/users.test.ts @@ -83,8 +83,10 @@ test('add and delete user membership', async t => { const groupIdx = await createGroup(tr, model); const userMembershipIdx = await model.users.addUserMembership(tr, userIdx, groupIdx); - const query = 'SELECT * FROM user_memberships WHERE idx = $1'; - const result = await tr.query(query, [userMembershipIdx]); + const query = 'SELECT user_idx, group_idx FROM user_memberships WHERE idx = $1'; + const result = await tr.query<{ user_idx: number; group_idx: number }>(query, [ + userMembershipIdx, + ]); t.truthy(result.rows[0]); t.is(result.rows[0].user_idx, userIdx); @@ -110,8 +112,8 @@ test('get all user memberships', async t => { const groupIdx1 = await createGroup(tr, model); const groupIdx2 = await createGroup(tr, model); - const idx1 = await model.users.addUserMembership(tr, userIdx, groupIdx1); - const idx2 = await model.users.addUserMembership(tr, userIdx, groupIdx2); + await model.users.addUserMembership(tr, userIdx, groupIdx1); + await model.users.addUserMembership(tr, userIdx, groupIdx2); const result = await model.users.getAllUserMemberships(tr, userIdx); @@ -137,8 +139,8 @@ test('get all user membership users', async t => { await model.pgDo(async tr => { const groupIdx = await createGroup(tr, model); const userIdx = await createUser(tr, model); - const studentNumberIdx = await model.users.addStudentNumber(tr, userIdx, uuid()); - const membership = await model.users.addUserMembership(tr, userIdx, groupIdx); + await model.users.addStudentNumber(tr, userIdx, uuid()); + await model.users.addUserMembership(tr, userIdx, groupIdx); const allMembership = await model.users.getAllMembershipUsers(tr, groupIdx); @@ -165,8 +167,8 @@ test('get all pending user membership users', async t => { await model.pgDo(async tr => { const groupIdx = await createGroup(tr, model); const userIdx = await createUser(tr, model); - const studentNumberIdx = await model.users.addStudentNumber(tr, userIdx, uuid()); - const membership = await model.users.addPendingUserMembership(tr, userIdx, groupIdx); + await model.users.addStudentNumber(tr, userIdx, uuid()); + await model.users.addPendingUserMembership(tr, userIdx, groupIdx); const allPendingMembership = await model.users.getAllPendingMembershipUsers(tr, groupIdx); @@ -235,7 +237,7 @@ test('change password', async t => { await model.pgDo(async tr => { const username = uuid(); const password = uuid(); - const emailIdx = await model.emailAddresses.create(tr, uuid(), uuid()); + await model.emailAddresses.create(tr, uuid(), uuid()); const userIdx = await model.users.create(tr, username, password, uuid(), '/bin/bash', 'en'); const newPassword = uuid(); @@ -250,13 +252,13 @@ test('change password token request with identical email idx', async t => { await model.pgDo(async tr => { const emailLocal = uuid(); const emailDomain = uuid(); - const emailAddressIdx = await model.emailAddresses.create(tr, emailLocal, emailDomain); + await model.emailAddresses.create(tr, emailLocal, emailDomain); const userIdx = await model.users.create(tr, uuid(), uuid(), uuid(), '/bin/bash', 'en'); const oldToken = await model.users.generatePasswordChangeToken(tr, userIdx); const newToken = await model.users.generatePasswordChangeToken(tr, userIdx); const query = 'SELECT token FROM password_change_tokens WHERE user_idx = $1'; - const result = await tr.query(query, [userIdx]); + const result = await tr.query<{ token: string }>(query, [userIdx]); const token = result.rows[0].token; t.is(newToken, token); t.not(oldToken, token); @@ -267,10 +269,10 @@ test('token expiration', async t => { await model.pgDo(async tr => { const emailLocal = uuid(); const emailDomain = uuid(); - const emailAddressIdx = await model.emailAddresses.create(tr, emailLocal, emailDomain); + await model.emailAddresses.create(tr, emailLocal, emailDomain); const userIdx = await model.users.create(tr, uuid(), uuid(), uuid(), '/bin/bash', 'en'); const token = await model.users.generatePasswordChangeToken(tr, userIdx); - const expiryResult = await tr.query( + const expiryResult = await tr.query<{ expires: string }>( 'SELECT expires FROM password_change_tokens WHERE token = $1', [token], ); @@ -299,7 +301,7 @@ test('change user shell', async t => { await model.pgDo(async tr => { const emailLocal = uuid(); const emailDomain = uuid(); - const emailAddressIdx = await model.emailAddresses.create(tr, emailLocal, emailDomain); + await model.emailAddresses.create(tr, emailLocal, emailDomain); const userIdx = await model.users.create(tr, uuid(), uuid(), uuid(), '/bin/bash', 'en'); const newShell = uuid(); await model.shells.addShell(tr, newShell); @@ -312,10 +314,10 @@ test('change user shell', async t => { test('reset resend count of expired password change token', async t => { await model.pgDo(async tr => { - const emailIdx = await model.emailAddresses.create(tr, uuid(), uuid()); + await model.emailAddresses.create(tr, uuid(), uuid()); const userIdx = await model.users.create(tr, uuid(), uuid(), uuid(), '/bin/bash', 'en'); let token = await model.users.generatePasswordChangeToken(tr, userIdx); - const expiryResult = await tr.query( + const expiryResult = await tr.query<{ expires: string }>( 'SELECT expires FROM password_change_tokens WHERE token = $1', [token], ); @@ -344,7 +346,7 @@ test('legacy mssql password (sha512)', async t => { ), }); await model.pgDo(async tr => { - const result = await tr.query( + const result = await tr.query<{ idx: number }>( 'INSERT INTO users (username, password_digest, name, uid, shell, ' + 'preferred_language) VALUES ($1, $2, \'OLDoge\', 10, \'/bin/bash\', \'en\') RETURNING idx', [username, legacyPasswordDigest], @@ -353,10 +355,12 @@ test('legacy mssql password (sha512)', async t => { // doge should be able to login using password stored in old doggy password format await model.users.authenticate(tr, username, password); // doge should be automatically migrated to brand-new password format - const selectResult: string = - (await tr.query('SELECT password_digest FROM users WHERE username=$1', [username])).rows[0] - .password_digest; - t.is(selectResult.split('$')[1], 'argon2id'); + const selectResult = await tr.query<{ password_digest: string }>( + 'SELECT password_digest FROM users WHERE username=$1', + [username], + ); + const digest = selectResult.rows[0].password_digest; + t.is(digest.split('$')[1], 'argon2id'); // doge should be able to login using password stored in brand-new password format. wow. await model.users.authenticate(tr, username, password); await model.users.delete(tr, userIdx);