npmrun 4 years ago
parent
commit
0d541cc8cf
  1. 2
      .env
  2. 11
      package.json
  3. 64
      packages/hapi-router/dist/hapi-router.cjs.js
  4. 2
      packages/hapi-router/dist/hapi-router.cjs.js.map
  5. 9
      packages/hapi-router/dist/index.d.ts
  6. 29
      packages/hapi-router/package-lock.json
  7. 3
      packages/hapi-router/package.json
  8. 11
      packages/hapi-router/pnpm-lock.yaml
  9. 1
      packages/hapi-router/rollup.config.js
  10. 26
      packages/hapi-router/src/index.ts
  11. 6
      packages/hapi-router/src/util/index.ts
  12. 2
      packages/hapi-router/tsconfig.json
  13. 1253
      pnpm-lock.yaml
  14. 4
      route.txt
  15. BIN
      source/db/data.db
  16. 17
      source/db/index.ts
  17. 11
      source/db/init.ts
  18. 42
      source/model/User.ts
  19. 2
      source/plugins/index.ts
  20. 7
      source/route/api/index.ts
  21. 97
      source/route/api/user/index.ts
  22. 11
      source/route/index/index.ts
  23. 8
      source/route/route.txt
  24. 30
      source/run.ts
  25. 13
      source/validate.ts

2
.env

@ -1 +1,3 @@
NODE_ENV=development
KEY = dsRhw1Y5UZqB8SjfClbkrX9PF7yuDMV3JItcW0G4vgpaxONo6mzenHLQET2AiKyPUjjdDko10amjPaba

11
package.json

@ -5,6 +5,7 @@
"private": true,
"main": "index.js",
"scripts": {
"init": "npx ts-node --project ./tsconfig.json -r tsconfig-paths/register source/db/init.ts alter",
"start": "npx ts-node-dev --project ./tsconfig.json -r tsconfig-paths/register ./source/main.ts",
"temp": "nodemon --exec 'npx ts-node-dev -r tsconfig-paths/register ./source/main.ts'"
},
@ -14,8 +15,15 @@
"dependencies": {
"@hapi/hapi": "^20.1.2",
"@hapi/inert": "^6.0.3",
"bcrypt": "^5.0.1",
"hapi-auth-jwt2": "^10.2.0",
"hapi-swagger": "^14.2.0",
"joi": "^17.4.0",
"jsonwebtoken": "^8.5.1",
"multiparty": "^4.2.2",
"nodemon": "^2.0.7"
"nodemon": "^2.0.7",
"sequelize": "^6.6.2",
"sqlite3": "^5.0.2"
},
"devDependencies": {
"@hapi/vision": "^6.1.0",
@ -23,6 +31,7 @@
"dotenv": "^10.0.0",
"ejs": "^3.1.6",
"file-type": "^16.5.0",
"ts-node": "^10.0.0",
"ts-node-dev": "^1.1.6",
"tsconfig-paths": "^3.9.0",
"typescript": "^4.3.2"

64
packages/hapi-router/dist/hapi-router.cjs.js

@ -3,14 +3,6 @@
Object.defineProperty(exports, '__esModule', { value: true });
var fs = require('fs');
var path = require('path');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var fs__default = /*#__PURE__*/_interopDefaultLegacy(fs);
var path__default = /*#__PURE__*/_interopDefaultLegacy(path);
/*! *****************************************************************************
Copyright (c) Microsoft Corporation.
@ -38,6 +30,8 @@ function __values(o) {
throw new TypeError(s ? "Object is not iterable." : "Symbol.iterator is not defined.");
}
var path$1 = require("path");
var fs$1 = require("fs");
function removeIndex(ss) {
var remove = function (str) {
if (str.endsWith("/index")) {
@ -64,7 +58,7 @@ function isIndexEnd(str) {
return str.length == 1 && str.endsWith("/");
}
function walkDir(filePath, exclude) {
if (exclude === void 0) { exclude = ["node_modules", "^_", ".git", ".idea", ".gitignore", "client"]; }
if (exclude === void 0) { exclude = ["node_modules", "^_", ".git", ".idea", ".gitignore", "client", "\.txt$"]; }
var files = [];
function Data(opts) {
this.relativeDir = opts.relativeDir;
@ -77,11 +71,11 @@ function walkDir(filePath, exclude) {
}
function readDir(filePath, dirname) {
if (dirname === void 0) { dirname = "."; }
var res = fs__default['default'].readdirSync(filePath);
var res = fs$1.readdirSync(filePath);
res.forEach(function (filename) {
var filepath = path__default['default'].resolve(filePath, filename);
var stat = fs__default['default'].statSync(filepath);
var name = filepath.split(path__default['default'].sep).slice(-1)[0];
var filepath = path$1.resolve(filePath, filename);
var stat = fs$1.statSync(filepath);
var name = filepath.split(path$1.sep).slice(-1)[0];
if (typeof exclude === "string" && new RegExp(exclude).test(name)) {
return;
}
@ -94,17 +88,17 @@ function walkDir(filePath, exclude) {
}
}
if (!stat.isFile()) {
readDir(filepath, dirname + path__default['default'].sep + name);
readDir(filepath, dirname + path$1.sep + name);
}
else {
var data = new Data({
relativeDir: dirname,
relativeFile: dirname + path__default['default'].sep + path__default['default'].parse(filepath).base,
relativeFileNoExt: dirname + path__default['default'].sep + path__default['default'].parse(filepath).name,
file: path__default['default'].parse(filepath).base,
filename: path__default['default'].parse(filepath).name,
relativeFile: dirname + path$1.sep + path$1.parse(filepath).base,
relativeFileNoExt: dirname + path$1.sep + path$1.parse(filepath).name,
file: path$1.parse(filepath).base,
filename: path$1.parse(filepath).name,
absoluteFile: filepath,
absoluteDir: path__default['default'].parse(filepath).dir,
absoluteDir: path$1.parse(filepath).dir,
});
files.push(data);
}
@ -146,21 +140,23 @@ function swagger(desc, notes, tags) {
};
}
var path = require("path");
var fs = require("fs");
var routes = ["所有路由路径:"];
var index = new ((function () {
function hapiRouter() {
var routePlugin = (function () {
function routePlugin() {
this.name = "routePlugin";
this.version = "0.0.1";
}
hapiRouter.prototype.register = function (server, options) {
routePlugin.prototype.register = function (server, options) {
var sourceDir = options.sourceDir;
var files = walkDir(sourceDir);
files.forEach(function (file) {
var e_1, _a;
var filename = file.relativeFileNoExt;
var array = filename.split(path__default['default'].sep).slice(1);
var array = filename.split(path.sep).slice(1);
var fileNoExt = removeIndex("/" + array.join("/"));
var moduleName = path__default['default'].resolve(sourceDir, filename);
var moduleName = path.resolve(sourceDir, filename);
var obj = require(moduleName);
if (obj.default) {
var func = new (obj.default || obj)();
@ -217,7 +213,14 @@ var index = new ((function () {
options_1.notes = ff.$swagger[1];
options_1.tags = ff.$swagger[2];
}
routes.push(route);
var str = route;
if (options_1.auth) {
str += " 该路由需要权限";
}
else {
str += " 该路由不需要权限";
}
routes.push(str);
server.route({
method: method,
path: route,
@ -236,15 +239,18 @@ var index = new ((function () {
}
}
});
fs__default['default'].writeFileSync("route.txt", routes.join("\n"), { encoding: "utf-8" });
fs.writeFileSync(path.resolve(sourceDir, "route.txt"), routes.join("\n"), {
encoding: "utf-8",
});
};
return hapiRouter;
}()))();
return routePlugin;
}());
var plugin = new routePlugin();
exports.auth = auth;
exports.config = config;
exports.default = index;
exports.method = method;
exports.plugin = plugin;
exports.route = route;
exports.swagger = swagger;
exports.validate = validate;

2
packages/hapi-router/dist/hapi-router.cjs.js.map

File diff suppressed because one or more lines are too long

9
packages/hapi-router/dist/index.d.ts

@ -1,13 +1,14 @@
// Generated by dts-bundle v0.7.3
declare module '@noderun/hapi-router' {
export * from "@noderun/hapi-router/util/decorators";
const _default: {
class routePlugin {
name: string;
version: string;
register(server: any, options: any): void;
};
export default _default;
}
const plugin: routePlugin;
export { plugin };
export * from "@noderun/hapi-router/util/decorators";
}
declare module '@noderun/hapi-router/util/decorators' {

29
packages/hapi-router/package-lock.json

@ -8,6 +8,9 @@
"name": "@noderun/hapi-router",
"version": "0.0.1",
"license": "ISC",
"dependencies": {
"@types/node": "^15.12.2"
},
"devDependencies": {
"@rollup/plugin-alias": "^3.1.1",
"@rollup/plugin-commonjs": "^15.0.0",
@ -139,10 +142,9 @@
"dev": true
},
"node_modules/@types/node": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.0.tgz",
"integrity": "sha512-j2tekvJCO7j22cs+LO6i0kRPhmQ9MXaPZ55TzOc1lzkN5b6BWqq4AFjl04s1oRRQ1v5rSe+KEvnLUSTonuls/A==",
"dev": true
"version": "15.12.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
"integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww=="
},
"node_modules/ansi-styles": {
"version": "4.2.1",
@ -316,6 +318,12 @@
"node": ">= 0.10.0"
}
},
"node_modules/dts-bundle/node_modules/@types/node": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.0.tgz",
"integrity": "sha512-j2tekvJCO7j22cs+LO6i0kRPhmQ9MXaPZ55TzOc1lzkN5b6BWqq4AFjl04s1oRRQ1v5rSe+KEvnLUSTonuls/A==",
"dev": true
},
"node_modules/dts-bundle/node_modules/glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",
@ -1180,10 +1188,9 @@
"dev": true
},
"@types/node": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.0.tgz",
"integrity": "sha512-j2tekvJCO7j22cs+LO6i0kRPhmQ9MXaPZ55TzOc1lzkN5b6BWqq4AFjl04s1oRRQ1v5rSe+KEvnLUSTonuls/A==",
"dev": true
"version": "15.12.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-15.12.2.tgz",
"integrity": "sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww=="
},
"ansi-styles": {
"version": "4.2.1",
@ -1321,6 +1328,12 @@
"mkdirp": "^0.5.0"
},
"dependencies": {
"@types/node": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@types/node/-/node-8.0.0.tgz",
"integrity": "sha512-j2tekvJCO7j22cs+LO6i0kRPhmQ9MXaPZ55TzOc1lzkN5b6BWqq4AFjl04s1oRRQ1v5rSe+KEvnLUSTonuls/A==",
"dev": true
},
"glob": {
"version": "6.0.4",
"resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz",

3
packages/hapi-router/package.json

@ -6,7 +6,7 @@
"typings": "dist/index.d.ts",
"buildOptions": {
"filename": "hapi-router",
"var": "hapi-router",
"var": "hapiRouter",
"formats": [
"cjs"
]
@ -22,6 +22,7 @@
"@rollup/plugin-alias": "^3.1.1",
"@rollup/plugin-commonjs": "^15.0.0",
"@rollup/plugin-replace": "^2.3.3",
"@types/node": "^15.12.2",
"chalk": "^4.1.0",
"dts-bundle": "^0.7.3",
"execa": "^4.0.3",

11
packages/hapi-router/pnpm-lock.yaml

@ -4,6 +4,7 @@ specifiers:
'@rollup/plugin-alias': ^3.1.1
'@rollup/plugin-commonjs': ^15.0.0
'@rollup/plugin-replace': ^2.3.3
'@types/node': ^15.12.2
chalk: ^4.1.0
dts-bundle: ^0.7.3
execa: ^4.0.3
@ -20,6 +21,7 @@ devDependencies:
'@rollup/plugin-alias': 3.1.2_rollup@2.51.2
'@rollup/plugin-commonjs': 15.1.0_rollup@2.51.2
'@rollup/plugin-replace': 2.4.2_rollup@2.51.2
'@types/node': 15.12.2
chalk: 4.1.1
dts-bundle: 0.7.3
execa: 4.1.0
@ -27,7 +29,7 @@ devDependencies:
ftp-deploy: 2.4.1
lodash: 4.17.21
rollup: 2.51.2
rollup-plugin-sourcemaps: 0.6.3_rollup@2.51.2
rollup-plugin-sourcemaps: 0.6.3_0092beb8efba53e1329cf7064a1da378
rollup-plugin-typescript2: 0.27.3_rollup@2.51.2+typescript@3.9.9
tslib: 2.3.0
typescript: 3.9.9
@ -117,6 +119,10 @@ packages:
resolution: {integrity: sha1-fyrX7FX5FEgvybHsS7GuYCjUYGY=}
dev: true
/@types/node/15.12.2:
resolution: {integrity: sha512-zjQ69G564OCIWIOHSXyQEEDpdpGl+G348RAKY0XXy9Z5kU9Vzv1GMNnkar/ZJ8dzXB3COzD9Mo9NtRZ4xfgUww==}
dev: true
/@types/node/8.0.0:
resolution: {integrity: sha512-j2tekvJCO7j22cs+LO6i0kRPhmQ9MXaPZ55TzOc1lzkN5b6BWqq4AFjl04s1oRRQ1v5rSe+KEvnLUSTonuls/A==}
dev: true
@ -644,7 +650,7 @@ packages:
engines: {node: '>= 4'}
dev: true
/rollup-plugin-sourcemaps/0.6.3_rollup@2.51.2:
/rollup-plugin-sourcemaps/0.6.3_0092beb8efba53e1329cf7064a1da378:
resolution: {integrity: sha512-paFu+nT1xvuO1tPFYXGe+XnQvg4Hjqv/eIhG8i5EspfYYPBKL57X7iVbfv55aNVASg3dzWvES9dmWsL2KhfByw==}
engines: {node: '>=10.0.0'}
peerDependencies:
@ -655,6 +661,7 @@ packages:
optional: true
dependencies:
'@rollup/pluginutils': 3.1.0_rollup@2.51.2
'@types/node': 15.12.2
rollup: 2.51.2
source-map-resolve: 0.6.0
dev: true

1
packages/hapi-router/rollup.config.js

@ -43,6 +43,7 @@ function createPlugin() {
});
const replacePlugin = replace({
__DEV__: isProd,
preventAssignment: true
});
plugin = [sourcemaps(), commonjs(), tsPlugin, aliasPlugin, replacePlugin];
return plugin;

26
packages/hapi-router/src/index.ts

@ -1,16 +1,12 @@
// @ts-nocheck
import { walkDir, removeIndex, isIndexEnd } from "./util";
import path from "path";
import fs from "fs";
const path = require("path")
const fs = require("fs")
const routes = ["所有路由路径:"];
export * from "./util/decorators";
export default new (class hapiRouter {
constructor() {}
class routePlugin {
public name: string = "routePlugin";
public version: string = "0.0.1";
public register(server: any, options: any) {
@ -73,8 +69,13 @@ export default new (class hapiRouter {
options.notes = ff.$swagger[1];
options.tags = ff.$swagger[2];
}
routes.push(route);
let str = route;
if (options.auth) {
str += " 该路由需要权限";
} else {
str += " 该路由不需要权限";
}
routes.push(str);
server.route({
method: method,
path: route,
@ -89,4 +90,9 @@ export default new (class hapiRouter {
encoding: "utf-8",
});
}
})();
}
const plugin = new routePlugin();
export { plugin };
export * from "./util/decorators";

6
packages/hapi-router/src/util/index.ts

@ -1,7 +1,7 @@
// @ts-nocheck
import fs from "fs"
import path from "path"
const path = require("path")
const fs = require("fs")
export function removeIndex(ss:any) {
const remove = (str:any) => {
@ -32,7 +32,7 @@ export function isIndexEnd(str:any) {
export function walkDir(
filePath:any,
exclude = ["node_modules", "^_", ".git", ".idea", ".gitignore", "client"]
exclude = ["node_modules", "^_", ".git", ".idea", ".gitignore", "client","\.txt$"]
) {
let files:any[] = [];
function Data(opts:any) {

2
packages/hapi-router/tsconfig.json

@ -8,7 +8,7 @@
// js
"target": "ES5",
//
"module": "ESNext",
"module": "ES2015",
// any
"noImplicitAny": true,
// "__extends"

1253
pnpm-lock.yaml

File diff suppressed because it is too large

4
route.txt

@ -1,4 +0,0 @@
所有路由路径:
/
/about
/upload

BIN
data/data.db → source/db/data.db

Binary file not shown.

17
source/db/index.ts

@ -0,0 +1,17 @@
import { Sequelize, DataTypes } from "sequelize";
import path from "path";
export const sequelize = new Sequelize({
dialect: "sqlite",
storage: path.resolve(__dirname, "./data.db"),
});
export async function connect(){
try {
await sequelize.authenticate();
console.log('Connection has been established successfully.');
} catch (error) {
console.error('Unable to connect to the database:', error);
}
}

11
source/db/init.ts

@ -0,0 +1,11 @@
import {connect} from "@/db/index"
import User from "@/model/User"
const argv = require('minimist')(process.argv.slice(2));
const isForce = !!(argv._.indexOf("force")!=-1) || false
const isAlter = !!(argv._.indexOf("alter")!=-1) || false
connect().then(()=>{
User.sync({force: isForce,alter: isAlter})
})

42
source/model/User.ts

@ -0,0 +1,42 @@
import {sequelize, connect} from "@/db/index"
import {DataTypes} from "sequelize";
// 全自定义写法
// const User = sequelize.define('User', {
// id: {
// type: DataTypes.STRING(50),
// primaryKey: true
// },
// username: {
// type: DataTypes.STRING,
// allowNull: false
// },
// password: {
// type: DataTypes.STRING,
// allowNull: false
// },
// createdAt: DataTypes.BIGINT,
// updatedAt: DataTypes.BIGINT,
// version: DataTypes.BIGINT
// }, {
// timestamps: false
// });
/**
*
* ----
*/
const User = sequelize.define('User', {
username: {
type: DataTypes.STRING,
allowNull: false
},
password: {
type: DataTypes.STRING,
allowNull: false
},
email: {
type: DataTypes.STRING,
}
}, {
});
export default User;

2
source/plugins/index.ts

@ -1,7 +1,7 @@
import filePlugin from "./file-plugin";
import path from "path";
import { sourceDir } from "@/util";
import routePlugin from "@noderun/hapi-router";
import { plugin as routePlugin } from "@noderun/hapi-router";
export default [
{

7
source/route/api/index.ts

@ -0,0 +1,7 @@
export default class{
index(request, h) {
return "asdasd";
}
}

97
source/route/api/user/index.ts

@ -0,0 +1,97 @@
import { auth, method, route, swagger, validate } from "@noderun/hapi-router";
import { gSuccess, gFail } from "@/util";
import User from "@/model/User";
import * as bcrypt from "bcrypt";
import * as jwt from "jsonwebtoken";
import * as Joi from "joi";
export default class {
@validate({
payload: Joi.object({
username: Joi.string().alphanum().min(6).max(35).required(),
password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{3,30}$")),
email: Joi.string().email({
minDomainSegments: 2,
tlds: { allow: ["com", "net"] },
}),
}),
})
@method("POST")
@route("/register")
@swagger("用户注册", "返回注册用户的信息", ["api"])
@auth(false)
async register(request, h) {
let { username, password, email } = request.payload;
try {
const result = await User.findOne({ where: { username: username } });
if (result != null) {
return gFail(null, "已存在该用户");
}
let salt = bcrypt.genSaltSync(10);
let pwdLock = bcrypt.hashSync(password, salt);
await User.create({ username, password: pwdLock, email });
return gSuccess("success", "you have a good heart.");
} catch (e) {
return gFail(null, "新建用户失败");
}
}
@validate({
payload: Joi.object({
username: Joi.string().alphanum().min(6).max(35).required(),
password: Joi.string().pattern(new RegExp("^[a-zA-Z0-9]{3,30}$")),
}),
})
@auth(false)
@method("POST")
@route("/login")
@swagger("用户登录", "返回注册用户的信息", ["api"])
async login(request, h) {
let { username, password } = request.payload;
const result = <any>await User.findOne({ where: { username: username } });
if (result == null) {
return gFail(null, "不存在该用户");
}
const validUser = bcrypt.compareSync(password, result.password);
if (!validUser) {
return gFail(null, "密码不正确");
}
let token = jwt.sign({ id: result.id }, process.env.KEY);
return gSuccess({ token: token });
}
@method("DELETE")
@auth()
@route("/del")
@swagger("删除用户", "删除用户账号", ["api"])
async del(request, h) {
const { id } = request.auth.credentials;
let result = await User.findOne({ where: { id: id } });
if (result == null) {
return gFail(null, "不存在该用户");
}
await result.destroy();
return gSuccess(null, "删除成功");
}
@method("GET")
@route("/userinfo")
@validate({
headers: Joi.object({
Authorization: Joi.string(),
}).unknown(), // 注意加上这个
})
@swagger("获取用户信息", "返回注册用户的信息", ["api"])
async userinfo(request, h) {
console.log(request);
const { id } = request.auth.credentials;
let result = <any>await User.findOne({ where: { id: id } });
if (result == null) {
return gFail(null, "不存在该用户");
}
result = result.toJSON();
delete result.password;
return gSuccess(result);
}
}

11
source/route/index/index.ts

@ -1,11 +1,3 @@
/*
* @Author: your name
* @Date: 2021-06-16 06:38:54
* @LastEditTime: 2021-06-16 17:19:23
* @LastEditors: your name
* @Description: In User Settings Edit
* @FilePath: /hapi-demo/source/route/index/index.ts
*/
import { config, method } from "@noderun/hapi-router";
import UploadFunc from "./_upload";
@ -13,9 +5,6 @@ export default class {
index(request, h) {
return h.view("views/index.ejs");
}
about(request, h) {
return h.view("views/about.ejs");
}
@config({
payload: {

8
source/route/route.txt

@ -0,0 +1,8 @@
所有路由路径:
/api 该路由需要权限
/api/user/register 该路由不需要权限
/api/user/login 该路由不需要权限
/api/user/del 该路由需要权限
/api/user/userinfo 该路由需要权限
/ 该路由不需要权限
/upload 该路由不需要权限

30
source/run.ts

@ -1,7 +1,9 @@
"use strict";
import plugins from "@/plugins";
import { baseDir } from "@/util";
import validate from "./validate";
const Hapi = require("@hapi/hapi");
const HapiSwagger = require("hapi-swagger");
const run = async () => {
const server = Hapi.server({
@ -9,12 +11,40 @@ const run = async () => {
host: "localhost",
});
// http://localhost:3000/documentation
const swaggerOptions = {
info: {
title: "Dream 文档",
version: "1.0.0",
},
};
await server.register([
{
plugin: HapiSwagger,
options: swaggerOptions,
},
]);
/**
* jwt
*/
await server.register(require("hapi-auth-jwt2"));
server.auth.strategy("jwt", "jwt", {
key: process.env.KEY, // Never Share your secret key
validate, // validate function defined above
verifyOptions: { algorithms: ["HS256"] },
});
server.auth.default("jwt");
await server.register(plugins, {
routes: {
// prefix: "/api",
},
});
/**
*
*/
// https://hapi.dev/module/vision/api/?v=6.1.0
await server.register(require("@hapi/vision"));
server.views({

13
source/validate.ts

@ -0,0 +1,13 @@
import User from "@/model/User";
export default async function validate(decoded, request, h) {
if (decoded.id) {
const result = await User.findOne({ where: { id: decoded.id } });
if (result == null) {
return { isValid: false };
}
return { isValid: true };
} else {
return { isValid: false };
}
}
Loading…
Cancel
Save