Commit baefa8f8 authored by nanahira's avatar nanahira

Merge branch 'try-rebuild'

parents f21a7164 90a80e72
FROM node:10-buster-slim FROM node:16-bullseye-slim
RUN mkdir -p /usr/src/app RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app WORKDIR /usr/src/app
COPY package*.json /usr/src/app/ COPY package*.json /usr/src/app/
RUN npm ci && npm cache clean --force RUN npm ci && && npm run build && npm cache clean --force
COPY . /usr/src/app COPY . /usr/src/app
CMD [ "npm", "start" ] CMD [ "npm", "start" ]
let config = process.env['NODE_ENV'] == 'development' ? require('./conf-dev').default : { let config = process.env['NODE_ENV'] == 'development' ? require('./conf-dev').default : {
DATABASE: process.env['DATABASE'], DATABASE: process.env['DATABASE'],
synchronize: !process.env['DB_NO_SYNC'],
Mail: { Mail: {
SMTP_HOST: process.env['SMTP_HOST'], SMTP_HOST: process.env['SMTP_HOST'],
SMTP_USERNAME: process.env['SMTP_USERNAME'], SMTP_USERNAME: process.env['SMTP_USERNAME'],
......
This diff is collapsed.
...@@ -2,8 +2,7 @@ ...@@ -2,8 +2,7 @@
"scripts": { "scripts": {
"start": "node server.js", "start": "node server.js",
"dev": "cross-env NODE_ENV=development nodemon server.js", "dev": "cross-env NODE_ENV=development nodemon server.js",
"tsc": "tsc", "build": "tsc"
"prestart": "tsc"
}, },
"dependencies": { "dependencies": {
"aliyun-oss-upload-stream": "^1.3.0", "aliyun-oss-upload-stream": "^1.3.0",
...@@ -22,8 +21,8 @@ ...@@ -22,8 +21,8 @@
"log4js": "^1.1.1", "log4js": "^1.1.1",
"mime": "^1.3.4", "mime": "^1.3.4",
"nodemailer": "^3.1.8", "nodemailer": "^3.1.8",
"pg": "^6.1.5", "pg": "^8.7.1",
"typeorm": "^0.0.11", "typeorm": "^0.2.37",
"uuid": "^3.0.1" "uuid": "^3.0.1"
}, },
"devDependencies": { "devDependencies": {
...@@ -36,12 +35,12 @@ ...@@ -36,12 +35,12 @@
"@types/koa-jwt": "^2.1.2", "@types/koa-jwt": "^2.1.2",
"@types/koa-router": "^7.0.22", "@types/koa-router": "^7.0.22",
"@types/log4js": "^0.0.32", "@types/log4js": "^0.0.32",
"@types/node": "^7.0.12", "@types/node": "^16.10.1",
"@types/nodemailer": "^1.3.32", "@types/nodemailer": "^1.3.32",
"@types/pg": "^6.1.38", "@types/pg": "^6.1.38",
"@types/uuid": "^2.0.29", "@types/uuid": "^2.0.29",
"cross-env": "^4.0.0", "cross-env": "^4.0.0",
"nodemon": "^1.11.0", "nodemon": "^1.11.0",
"typescript": "^2.2.2" "typescript": "^4.4.3"
} }
} }
...@@ -7,6 +7,7 @@ import privateRouter from './src/privateRoutes'; ...@@ -7,6 +7,7 @@ import privateRouter from './src/privateRoutes';
import { createConnection } from 'typeorm'; import { createConnection } from 'typeorm';
import config from './config'; import config from './config';
import {Token, User, UserNameChangeHistory} from './src/model'; import {Token, User, UserNameChangeHistory} from './src/model';
import { setConnection } from './src/controllers/entity-manager';
const app = new Koa(); const app = new Koa();
...@@ -61,23 +62,27 @@ app.use(privateRouter.routes()); ...@@ -61,23 +62,27 @@ app.use(privateRouter.routes());
app.use(privateRouter.allowedMethods()); app.use(privateRouter.allowedMethods());
createConnection({
driver: {
type: 'postgres',
url: config.DATABASE
},
entities: [User, Token, UserNameChangeHistory],
autoSchemaSync: true,
logging: {
logQueries: process.env['NODE_ENV'] === 'development',
logFailedQueryError: process.env['NODE_ENV'] === 'development',
}
}).then(() => {
app.listen(3000, () => {
console.log('Server is running at port %s', 3000);
});
}).catch(e => { async function main() {
console.log(e); console.log('Creating database connection.');
}); try {
const conn = await createConnection({
type: 'postgres',
url: config.DATABASE,
entities: [User, Token, UserNameChangeHistory],
synchronize: config.synchronize,
/*logging: {
logQueries: process.env['NODE_ENV'] === 'development',
logFailedQueryError: process.env['NODE_ENV'] === 'development',
}*/
});
setConnection(conn);
console.log('Starting server.');
app.listen(3000, () => {
console.log('Server is running at port %s', 3000);
});
} catch (e) {
console.error(`Create database failed: ${e.toString()}`);
}
}
main();
import { Context } from 'koa'; import { Context } from 'koa';
import { SignIn, SignUp, Token, User } from '../model'; import { SignIn, SignUp, Token, User } from '../model';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import { getEntityManager } from 'typeorm'; import { getEntityManager } from './entity-manager';
import * as Bluebird from 'bluebird'; import * as Bluebird from 'bluebird';
import tp from '../mail'; import tp from '../mail';
import * as uuid from 'uuid'; import * as uuid from 'uuid';
...@@ -95,7 +95,7 @@ export const signup = async (ctx: Context) => { ...@@ -95,7 +95,7 @@ export const signup = async (ctx: Context) => {
ip_address: ctx.request.ip ip_address: ctx.request.ip
}); });
const user = await getEntityManager().persist(newUser); const user = await getEntityManager().save(newUser);
const key = uuid.v1(); const key = uuid.v1();
let _token: Token = new Token({ let _token: Token = new Token({
...@@ -105,7 +105,7 @@ export const signup = async (ctx: Context) => { ...@@ -105,7 +105,7 @@ export const signup = async (ctx: Context) => {
type: 'activate' type: 'activate'
}); });
await getEntityManager().persist(_token); await getEntityManager().save(_token);
const url = new URL('https://accounts.moecube.com/activate'); const url = new URL('https://accounts.moecube.com/activate');
url.searchParams.set('key', key); url.searchParams.set('key', key);
...@@ -117,7 +117,7 @@ export const signup = async (ctx: Context) => { ...@@ -117,7 +117,7 @@ export const signup = async (ctx: Context) => {
text: `单击链接 或将链接复制到网页地址栏并回车 来激活账号 ${url}`, text: `单击链接 或将链接复制到网页地址栏并回车 来激活账号 ${url}`,
html: views.activate({ locale: 'zh-CN', username: user.username, url }) html: views.activate({ locale: 'zh-CN', username: user.username, url })
}).catch(function(error) { }).catch(function(error) {
console.log(`Failed to send mail to ${user.username}<${user.email}>`); console.log(`Failed to send mail to ${user.username}<${user.email}>: ${error.toString()}`);
}); });
const token = createToken({ const token = createToken({
...@@ -160,7 +160,7 @@ export const forgot = async (ctx: Context) => { ...@@ -160,7 +160,7 @@ export const forgot = async (ctx: Context) => {
}); });
await getEntityManager().persist(token); await getEntityManager().save(token);
const url = new URL('https://accounts.moecube.com/reset'); const url = new URL('https://accounts.moecube.com/reset');
url.searchParams.set('key', key); url.searchParams.set('key', key);
...@@ -194,7 +194,7 @@ export const resetPassword = async (ctx: Context) => { ...@@ -194,7 +194,7 @@ export const resetPassword = async (ctx: Context) => {
} }
const userRep = getEntityManager().getRepository(User); const userRep = getEntityManager().getRepository(User);
let user: User | undefined = await userRep.findOneById(u.user_id); let user: User | undefined = await userRep.findOne({where: {id: u.user_id}});
if (!user) { if (!user) {
ctx.throw('i_user_unexists', 400); ctx.throw('i_user_unexists', 400);
...@@ -206,7 +206,7 @@ export const resetPassword = async (ctx: Context) => { ...@@ -206,7 +206,7 @@ export const resetPassword = async (ctx: Context) => {
user.password_hash = (await Bluebird.promisify(crypto.pbkdf2)(u.password, salt, 64000, 32, 'sha256')).toString('hex'); user.password_hash = (await Bluebird.promisify(crypto.pbkdf2)(u.password, salt, 64000, 32, 'sha256')).toString('hex');
user.salt = salt; user.salt = salt;
ctx.body = await getEntityManager().persist(user); ctx.body = await getEntityManager().save(user);
await tokenReq.remove(token); await tokenReq.remove(token);
...@@ -235,7 +235,7 @@ export const activate = async (ctx: Context) => { ...@@ -235,7 +235,7 @@ export const activate = async (ctx: Context) => {
user.active = true; user.active = true;
user.email = token.data; user.email = token.data;
ctx.body = await userReq.persist(user); ctx.body = await userReq.save(user);
let tokens: Token[] = await tokenReq.find({ user_id: user.id, type: 'activate' }); let tokens: Token[] = await tokenReq.find({ user_id: user.id, type: 'activate' });
...@@ -249,5 +249,9 @@ export const authUser = async (ctx: Context) => { ...@@ -249,5 +249,9 @@ export const authUser = async (ctx: Context) => {
const userReq = getEntityManager().getRepository(User); const userReq = getEntityManager().getRepository(User);
ctx.status = 200; ctx.status = 200;
ctx.body = await userReq.findOne({ id: user.id }); const gotUser = await userReq.findOne({ id: user.id });
}; if (gotUser) {
\ No newline at end of file gotUser.cleanSensitiveData()
}
ctx.body = gotUser;
};
import { Connection } from "typeorm"
let connection: Connection;
export function setConnection(conn: Connection) {
connection = conn;
}
export function getEntityManager() {
return connection.manager;
}
...@@ -2,7 +2,7 @@ import { Context } from 'koa'; ...@@ -2,7 +2,7 @@ import { Context } from 'koa';
import {Token, User, UserNameChangeHistory} from '../model'; import {Token, User, UserNameChangeHistory} from '../model';
import * as crypto from 'crypto'; import * as crypto from 'crypto';
import * as Bluebird from 'bluebird'; import * as Bluebird from 'bluebird';
import { getEntityManager } from 'typeorm'; import { getEntityManager } from './entity-manager';
import tp from '../mail'; import tp from '../mail';
import config from '../../config'; import config from '../../config';
import * as uuid from 'uuid'; import * as uuid from 'uuid';
...@@ -42,7 +42,7 @@ export const checkUserExists = async (ctx: Context) => { ...@@ -42,7 +42,7 @@ export const checkUserExists = async (ctx: Context) => {
if (user) { if (user) {
ctx.body = user; ctx.body = user.cleanSensitiveData();
} else { } else {
ctx.throw('i_not_found', 400); ctx.throw('i_not_found', 400);
} }
...@@ -56,10 +56,16 @@ export const UpdateProfiles = async (ctx: Context) => { ...@@ -56,10 +56,16 @@ export const UpdateProfiles = async (ctx: Context) => {
ctx.throw(400); ctx.throw(400);
} }
if (user.username && process.env.NO_CHANGE_USERNAME) { const targetUser = ctx.request.body;
if (targetUser.username && process.env.NO_CHANGE_USERNAME) {
ctx.throw("Changing username is currently not allowed.", 400); ctx.throw("Changing username is currently not allowed.", 400);
} }
if (user.id != targetUser.id || targetUser.admin != null || targetUser.active != null || targetUser.registration_ip_address || targetUser.ip_address) {
ctx.throw("Cannot change sensitive data.", 400);
}
const userRep = getEntityManager().getRepository(User); const userRep = getEntityManager().getRepository(User);
let _user: User | undefined = await userRep let _user: User | undefined = await userRep
...@@ -73,13 +79,13 @@ export const UpdateProfiles = async (ctx: Context) => { ...@@ -73,13 +79,13 @@ export const UpdateProfiles = async (ctx: Context) => {
} }
Object.assign(_user, ctx.request.body); Object.assign(_user, targetUser);
await getEntityManager().persist(_user); await getEntityManager().save(_user);
ctx.status = 200; ctx.status = 200;
ctx.body = _user; ctx.body = _user.cleanSensitiveData();
}; };
...@@ -137,7 +143,7 @@ export const UpdateAccount = async (ctx: Context) => { ...@@ -137,7 +143,7 @@ export const UpdateAccount = async (ctx: Context) => {
if (u.username != user.username) { if (u.username != user.username) {
const historyRep = getEntityManager().getRepository(UserNameChangeHistory); const historyRep = getEntityManager().getRepository(UserNameChangeHistory);
let changeHistory = new UserNameChangeHistory(user.username, u.username, u.user_id); let changeHistory = new UserNameChangeHistory(user.username, u.username, u.user_id);
historyRep.persist(changeHistory); historyRep.save(changeHistory);
} }
user.username = u.username; user.username = u.username;
...@@ -164,7 +170,7 @@ export const UpdateAccount = async (ctx: Context) => { ...@@ -164,7 +170,7 @@ export const UpdateAccount = async (ctx: Context) => {
type: 'activate', type: 'activate',
}); });
await getEntityManager().persist(token); await getEntityManager().save(token);
const url = new URL('https://accounts.moecube.com/activate'); const url = new URL('https://accounts.moecube.com/activate');
url.searchParams.set('key', key); url.searchParams.set('key', key);
...@@ -184,7 +190,7 @@ export const UpdateAccount = async (ctx: Context) => { ...@@ -184,7 +190,7 @@ export const UpdateAccount = async (ctx: Context) => {
} }
ctx.status = 200; ctx.status = 200;
ctx.body = await userRep.persist(user); ctx.body = (await userRep.save(user)).cleanSensitiveData();
}; };
...@@ -194,6 +200,7 @@ export const legacyYGOProAuth = async (ctx: Context) => { ...@@ -194,6 +200,7 @@ export const legacyYGOProAuth = async (ctx: Context) => {
if (!user) { if (!user) {
return ctx.throw(404); return ctx.throw(404);
} }
user.cleanSensitiveData();
ctx.body = { user }; ctx.body = { user };
}; };
...@@ -214,3 +221,11 @@ export const getUserAvatar = async (ctx: Context) => { ...@@ -214,3 +221,11 @@ export const getUserAvatar = async (ctx: Context) => {
} }
ctx.body = user.avatarURL() ctx.body = user.avatarURL()
}; };
export const allowChangeUsername = async (ctx: Context) => {
ctx.status = 200;
const envData = process.env.NO_CHANGE_USERNAME;
ctx.body = {
allow: !envData, reason: envData
}
};
import { Column, CreateDateColumn, Entity, PrimaryColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; import { Column, CreateDateColumn, Entity, PrimaryColumn, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm';
import { ColumnTypes } from 'typeorm/metadata/types/ColumnTypes';
import { URL } from 'url'; import { URL } from 'url';
@Entity('users') @Entity('users')
export class User { export class User {
@PrimaryGeneratedColumn(ColumnTypes.INTEGER) @PrimaryGeneratedColumn({type: 'int4'})
id: number; id: number;
@Column(ColumnTypes.STRING, { unique: true, nullable: false }) @Column('varchar', { length: 255, unique: true, nullable: false })
username: string; username: string;
@Column(ColumnTypes.STRING, { nullable: true }) @Column('varchar', { length: 255, nullable: true })
name?: string; name?: string;
@Column(ColumnTypes.STRING, { nullable: false }) @Column('varchar', { length: 255, nullable: false })
email: string; email: string;
@Column(ColumnTypes.STRING, { nullable: false }) @Column('varchar', { length: 255, nullable: false })
password_hash: string; password_hash: string;
@Column(ColumnTypes.STRING, { nullable: false }) @Column('varchar', { length: 255, nullable: false })
salt: string; salt: string;
@Column(ColumnTypes.BOOLEAN, { nullable: false }) @Column('boolean', { nullable: false })
active: boolean; active: boolean;
@Column(ColumnTypes.BOOLEAN, { nullable: false }) @Column('boolean', { nullable: false })
admin: boolean; admin: boolean;
@Column(ColumnTypes.STRING, { nullable: true }) @Column('varchar', { length: 255, nullable: true })
avatar?: string; avatar?: string;
@Column(ColumnTypes.STRING, { nullable: false }) @Column('varchar', { length: 255, nullable: false })
locale: string; locale: string;
@Column(ColumnTypes.STRING, { nullable: false }) @Column('varchar', { length: 255, nullable: false })
registration_ip_address: string; registration_ip_address: string;
@Column(ColumnTypes.STRING, { nullable: false }) @Column('varchar', { length: 255, nullable: false })
ip_address: string; ip_address: string;
@CreateDateColumn({ nullable: false }) @CreateDateColumn({ nullable: false })
created_at: Date; created_at: Date;
...@@ -57,6 +56,15 @@ export class User { ...@@ -57,6 +56,15 @@ export class User {
return 'https://cdn01.moecube.com/accounts/default_avatar.jpg'; return 'https://cdn01.moecube.com/accounts/default_avatar.jpg';
} }
} }
cleanSensitiveData() {
// this.email = '_masked';
this.password_hash = '_masked';
this.salt = '_masked';
this.registration_ip_address = '_masked';
this.ip_address = '_masked';
return this;
}
} }
interface UserCreate { interface UserCreate {
...@@ -74,13 +82,13 @@ interface UserCreate { ...@@ -74,13 +82,13 @@ interface UserCreate {
@Entity('tokens') @Entity('tokens')
export class Token { export class Token {
@PrimaryColumn(ColumnTypes.STRING) @PrimaryColumn('varchar', { length: 255 })
key: string; key: string;
@Column(ColumnTypes.STRING, { nullable: false }) @Column('varchar', { length: 255, nullable: false })
user_id: number; user_id: number;
@Column(ColumnTypes.STRING, { nullable: false }) @Column('varchar', { length: 255, nullable: false })
type: string; type: string;
@Column(ColumnTypes.STRING, { nullable: false }) @Column('varchar', { length: 255, nullable: false })
data: string; data: string;
constructor(props: Token) { constructor(props: Token) {
...@@ -112,13 +120,13 @@ export interface SignUp { ...@@ -112,13 +120,13 @@ export interface SignUp {
@Entity('username_change_history') @Entity('username_change_history')
export class UserNameChangeHistory { export class UserNameChangeHistory {
@PrimaryGeneratedColumn(ColumnTypes.INTEGER) @PrimaryGeneratedColumn({type: 'int4'})
id: number; id: number;
@Column(ColumnTypes.INTEGER) @Column('int4')
userid: number; userid: number;
@Column(ColumnTypes.STRING) @Column('varchar', { length: 255 })
old_username: string; old_username: string;
@Column(ColumnTypes.STRING) @Column('varchar', { length: 255 })
new_username: string; new_username: string;
@UpdateDateColumn() @UpdateDateColumn()
change_time: Date; change_time: Date;
...@@ -130,4 +138,4 @@ export class UserNameChangeHistory { ...@@ -130,4 +138,4 @@ export class UserNameChangeHistory {
this.change_time = new Date(); this.change_time = new Date();
} }
} }
\ No newline at end of file
...@@ -23,5 +23,7 @@ router.get('/users/:username.json', user.legacyYGOProAuth); ...@@ -23,5 +23,7 @@ router.get('/users/:username.json', user.legacyYGOProAuth);
router.get('/users/:username.png', user.legacyYGOProAvatar); router.get('/users/:username.png', user.legacyYGOProAvatar);
router.get('/users/:username.avatar', user.getUserAvatar); router.get('/users/:username.avatar', user.getUserAvatar);
router.get('/allowChangeUsername', user.allowChangeUsername)
export default router;
\ No newline at end of file export default router;
...@@ -5,7 +5,8 @@ ...@@ -5,7 +5,8 @@
"target": "es2017", "target": "es2017",
"emitDecoratorMetadata": true, "emitDecoratorMetadata": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"strictNullChecks": true "strictNullChecks": true,
"skipLibCheck": true
}, },
"exclude": [ "exclude": [
"node_modules" "node_modules"
......
This diff is collapsed.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment