Skip to content

Commit

Permalink
Merge pull request #22 from gdsc-kaist/feature/random-password
Browse files Browse the repository at this point in the history
Feature/random password
  • Loading branch information
Byunk authored Dec 23, 2023
2 parents 380e81a + 89179b1 commit fd61fe4
Show file tree
Hide file tree
Showing 13 changed files with 341 additions and 159 deletions.
2 changes: 1 addition & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ const sessionMiddleware = session({
export function createApp() {
const app = express();

if (process.env.NODE_ENV !== 'test') {
if (process.env.NODE_ENV !== 'test' && process.env.NODE_ENV !== undefined) {
initDB()
.then(() => {
logger.info('Database connected');
Expand Down
45 changes: 45 additions & 0 deletions src/db/postgre.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,51 @@ export const sequelize = new Sequelize(
},
);

const {
Organization,
User,
Budget,
Income,
Expense,
Transaction,
Account,
} = require('../model');

Organization.hasOne(User, {
onDelete: 'CASCADE',
});
User.belongsTo(Organization);

Organization.hasMany(Budget, {
onDelete: 'CASCADE',
});
Budget.belongsTo(Organization);

Organization.hasMany(Account, {
onDelete: 'CASCADE',
});
Account.belongsTo(Organization);

Budget.hasMany(Income, {
onDelete: 'CASCADE',
});
Income.belongsTo(Budget);

Budget.hasMany(Expense, {
onDelete: 'CASCADE',
});
Expense.belongsTo(Budget);

Income.hasMany(Transaction, {
onDelete: 'CASCADE',
});
Transaction.belongsTo(Income);

Expense.hasMany(Transaction, {
onDelete: 'CASCADE',
});
Transaction.belongsTo(Expense);

sequelize.authenticate().catch((err) => {
logger.error('Unable to connect to the PostgreSQL database:');
throw err;
Expand Down
10 changes: 8 additions & 2 deletions src/db/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,18 @@ export async function initDB() {
];

for (const model of models) {
if (process.env.NODE_ENV === 'test') {
logger.debug(`Syncing ${model.name}`);

if (
process.env.NODE_ENV === undefined ||
process.env.NODE_ENV === 'test'
) {
await model.sync({ force: true });
} else if (process.env.NODE_ENV === 'development') {
await model.sync({ alter: true });
} else {
await model.sync();
// ! It drops columns that are not in the model
await model.sync({ alter: true });
}
}
}
35 changes: 0 additions & 35 deletions src/model/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,41 +7,6 @@ import Transaction from './transaction';
import AuditPeriod from './audit_period';
import Account from './account';

Organization.hasOne(User, {
onDelete: 'CASCADE',
});
User.belongsTo(Organization);

Organization.hasMany(Budget, {
onDelete: 'CASCADE',
});
Budget.belongsTo(Organization);

Organization.hasMany(Account, {
onDelete: 'CASCADE',
});
Account.belongsTo(Organization);

Budget.hasMany(Income, {
onDelete: 'CASCADE',
});
Income.belongsTo(Budget);

Budget.hasMany(Expense, {
onDelete: 'CASCADE',
});
Expense.belongsTo(Budget);

Income.hasMany(Transaction, {
onDelete: 'CASCADE',
});
Transaction.belongsTo(Income);

Expense.hasMany(Transaction, {
onDelete: 'CASCADE',
});
Transaction.belongsTo(Expense);

export {
Organization,
User,
Expand Down
5 changes: 5 additions & 0 deletions src/model/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ class User extends Model {
declare id: number;
declare email: string;
declare password: string;
declare initialPassword: string;
declare role: string;
declare isDisabled: boolean; // 계정 비활성화 여부
declare OrganizationId: number;
Expand All @@ -29,6 +30,10 @@ User.init(
type: DataTypes.STRING(64),
allowNull: false,
},
initialPassword: {
type: DataTypes.STRING(64),
allowNull: true,
},
role: {
type: DataTypes.ENUM('admin', 'user'),
allowNull: false,
Expand Down
40 changes: 33 additions & 7 deletions src/routes/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import logger from '../config/winston';
import { QueryTypes } from 'sequelize';
import { schemaName } from '../utils/common';
import { sequelize } from '../db';
import { compare } from 'bcrypt';

export function createUsersRouter() {
const router = express.Router();
Expand All @@ -24,7 +25,9 @@ export function createUsersRouter() {
`SELECT
U."id",
U."email",
O."name" organization_name,
O."name" organization_name,
U."password",
U."initialPassword",
U."role",
U."isDisabled"
FROM ${schemaName}."organizations" as O
Expand All @@ -33,13 +36,24 @@ export function createUsersRouter() {
ORDER BY O."name"`,
queryOptions,
);
res.json(users);

const userReponse = [];
let user: any;
for (user of users) {
if (await compare(user.initialPassword, user.password)) {
user.password = user.initialPassword;
} else {
user.password = null;
}
delete user.initialPassword;
userReponse.push(user);
}
res.json(userReponse);
}),
);

// 계정 생성 (default password: password)
// 계정 생성
// TODO: email sanitize (kaist email만 가능하도록)
// TODO: 유저 정보 반환
router.post(
'/',
wrapAsync(async (req: Request, res: Response, next: NextFunction) => {
Expand All @@ -52,16 +66,23 @@ export function createUsersRouter() {
);
await UserService.checkDuplicateUserByEmail(req.body.email);

const initial_password = 'password';
const initial_password = UserService.generateRandomPassword();
const encrypted_password =
await UserService.encrypt(initial_password);

await User.create({
const user = await User.create({
email: req.body.email,
password: encrypted_password,
initialPassword: initial_password,
OrganizationId: organization.id,
});
res.sendStatus(200);
res.json({
email: req.body.email,
password: initial_password,
role: user.role,
is_disabled: user.isDisabled,
organization_id: organization.id,
});
}),
);

Expand Down Expand Up @@ -103,9 +124,14 @@ export function createUsersRouter() {

const new_password = req.body.new_password;
UserService.checkPasswordCondition(new_password);
UserService.checkNewPasswordNotChanged(
new_password,
req.body.password,
);

const encrypted_password = await UserService.encrypt(new_password);
user.password = encrypted_password;

await user.save();
res.sendStatus(200);
}),
Expand Down
21 changes: 21 additions & 0 deletions src/service/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,24 @@ export function checkPasswordCondition(password: string) {
throw new BadRequestError('비밀번호는 8자 이상 12자 이하여야 합니다.');
}
}

export function checkNewPasswordNotChanged(
new_password: string,
existing_password: string,
) {
if (new_password == existing_password) {
throw new BadRequestError(
'새로운 비밀번호가 기존 비밀번호와 동일합니다.',
);
}
}

export function generateRandomPassword(length: number = 12) {
const charset =
'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%';
let password = '';
for (let i = 0, n = charset.length; i < length; ++i) {
password += charset.charAt(Math.floor(Math.random() * n));
}
return password;
}
56 changes: 43 additions & 13 deletions swagger/routes/user.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ paths:
- 계정
responses:
'200':
description: OK
content:
application/json:
schema:
Expand All @@ -16,7 +15,7 @@ paths:
'401':
description: UnauthorizedError. 비밀번호가 일치하지 않습니다.
post:
description: 계정 생성. 기본 비밀번호는 'password'로 생성됨.
description: 계정 생성
tags:
- 계정
requestBody:
Expand All @@ -37,7 +36,37 @@ paths:
- email
responses:
'200':
description: OK
content:
application/json:
schema:
type: object
properties:
email:
type: string
description: 이메일
example: [email protected]
password:
type: string
description: 비밀번호
example: abcdeABCDE!@
role:
type: string
description: 권한
example: user
is_disabled:
type: boolean
description: 계정 비활성화 여부
example: false
organization_id:
type: integer
description: 피감기관 id
example: 1
required:
- email
- password
- role
- is_disabled
- organization_id
'400':
description: BadRequestError. 비밀번호는 8자 이상 12자 이하여야 합니다.
'409':
Expand Down Expand Up @@ -153,6 +182,14 @@ definitions:
type: string
description: 이메일
example: [email protected]
organization_name:
type: string
description: 피감기관 이름
example: 감사원
password:
type: string
description: 비밀번호 (변경이 이루어졌다면 null, 처음 발급된 비밀번호라면 비밀번호가 노출됨)
example: abcdeABCDE!@
role:
type: string
enum: ['user', 'admin']
Expand All @@ -162,17 +199,10 @@ definitions:
type: boolean
description: 계정 비활성화 여부
example: false
createdAt:
type: timestamp
description: 생성일자
example: 2020-01-01 00:00:00
updatedAt:
type: timestamp
description: 수정일자
example: 2020-01-01 00:00:00
required:
- id
- email
- organization_name
- password
- role
- isDisabled
- createdAt
- updatedAt
2 changes: 2 additions & 0 deletions test/mock/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './user';
export * from './organization';
11 changes: 11 additions & 0 deletions test/mock/organization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const mockOrganization1 = {
name: '학부총학생회',
};

export const mockOrganization2 = {
name: '감사원',
};

export const mockOrganization3 = {
name: '동아리연합회',
};
32 changes: 32 additions & 0 deletions test/mock/user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
export const mockPassword = 'abcdefghjk!@';

export const mockEmail1 = '[email protected]';
export const mockEmail2 = '[email protected]';
export const mockEmail3 = '[email protected]';

export function createMockUser1(organizationId: number | string) {
return {
email: mockEmail1,
password: mockPassword,
initialPassword: mockPassword,
OrganizationId: organizationId,
};
}

export function createMockUser2(organizationId: number | string) {
return {
email: mockEmail2,
password: mockPassword,
initialPassword: mockPassword,
OrganizationId: organizationId,
};
}

export function createMockUser3(organizationId: number | string) {
return {
email: mockEmail3,
password: mockPassword,
initialPassword: mockPassword,
OrganizationId: organizationId,
};
}
Loading

0 comments on commit fd61fe4

Please sign in to comment.