chore: license说明

This commit is contained in:
xiaojunnuo
2024-08-14 21:24:12 +08:00
parent 746bb9d385
commit db9d27468e
25 changed files with 483 additions and 114 deletions
@@ -3,7 +3,6 @@ import { equal } from "assert";
describe("license", function () {
it("#license", async function () {
const req = {
appKey: "z4nXOeTeSnnpUpnmsV",
subjectId: "999",
license: "",
};
+34 -8
View File
@@ -2,8 +2,8 @@ import { createVerify } from "node:crypto";
import { logger } from "../utils/index.js";
const SecreteKey =
"LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1JSUJDZ0tDQVFFQXY3TGtMaUp1dGM0NzhTU3RaTExjajVGZXh1YjJwR2NLMGxwa0hwVnlZWjhMY29rRFhuUlAKUGQ5UlJSTVRTaGJsbFl2Mzd4QUhOV1ZIQ0ZsWHkrQklVU001bUlBU1NDQTV0azlJNmpZZ2F4bEFDQm1BY0lGMwozKzBjeGZIYVkrVW9YdVluMkZ6YUt2Ym5GdFZIZ0lkMDg4a3d4clZTZzlCT3BDRVZIR1pxR2I5TWN5MXVHVXhUClFTVENCbmpoTWZlZ0p6cXVPYWVOY0ZPSE5tbmtWRWpLTythbTBPeEhNS1lyS3ZnQnVEbzdoVnFENlBFMUd6V3AKZHdwZUV4QXZDSVJxL2pWTkdRK3FtMkRWOVNJZ3U5bmF4MktmSUtFeU50dUFFS1VpekdqL0VmRFhDM1cxMExhegpKaGNYNGw1SUFZU1o3L3JWVmpGbExWSVl0WDU1T054L1Z3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0K";
const appKey = "z4nXOeTeSnnpUpnmsV";
"LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUlJQkNnS0NBUUVBMjdoZDM0NjRYbyt3TkpmTTNCWjE5MXlQK2NLaTd3ck9CbXdjTWJPZUdsNlJOMUVtTGhyMgplOFdvOGpmMW9IVXc5RFV6L2I2ZHU3Q3ZXMXZNUDA1Q3dSS3lNd2U3Q1BYRGQ2U01mSkwxRFZyUkw5Ylh0cEYzCjJkQVA5UENrakFJcFMvRE5jVkhLRXk1QW8yMnFFenpTKzlUT0JVY2srREdZcmo4KzI5U3h2aEZDRE5ZbEE2d1EKbEkyRWc5TWNBV2xDU3p1S1JWa2ZWUWdYVlU3SmE5OXp1Um1oWWtYZjFxQzBLcVAwQkpDakdDNEV6ZHorMmwyaAo2T3RxVHVVLzRkemlYYnRMUS8vU0JqNEgxdi9PZ3dUZjJkSVBjUnRHOXlWVTB2ZlQzVzdUTkdlMjU3em5ESDBYCkd6Wm4zdWJxTXJuL084b2ltMHRrS3ZHZXZ1V2ZraWNwVVFJREFRQUIKLS0tLS1FTkQgUFVCTElDIEtFWS0tLS0tCg==";
export const appKey = "GGtrKRWRknFdIID0rW";
export type LicenseVerifyReq = {
subjectId: string;
license: string;
@@ -18,11 +18,15 @@ type License = {
duration: number;
version: number;
secret: string;
level: number;
signature: string;
};
class LicenseHolder {
isPlus = false;
expireTime = 0;
level = 1;
message?: string = undefined;
}
const holder = new LicenseHolder();
holder.isPlus = false;
@@ -35,9 +39,20 @@ class LicenseVerifier {
return await this.verify(req);
}
setPlus(value: boolean) {
holder.isPlus = value;
return value;
setPlus(value: boolean, info: any = {}) {
if (value && !info) {
holder.isPlus = true;
holder.expireTime = info.expireTime;
holder.level = info.level;
} else {
holder.isPlus = false;
holder.expireTime = 0;
holder.level = 1;
holder.message = info.message;
}
return {
...holder,
};
}
async verify(req: LicenseVerifyReq) {
this.licenseReq = req;
@@ -54,7 +69,7 @@ class LicenseVerifier {
const json: License = JSON.parse(licenseJson);
if (json.expireTime < Date.now()) {
logger.warn("授权已过期");
return this.setPlus(false);
return this.setPlus(false, { message: "授权已过期" });
}
const content = `${appKey},${this.licenseReq.subjectId},${json.code},${json.secret},${json.activeTime},${json.duration},${json.expireTime},${json.version}`;
const publicKey = Buffer.from(SecreteKey, "base64").toString();
@@ -62,9 +77,12 @@ class LicenseVerifier {
this.checked = true;
if (!res) {
logger.warn("授权校验失败");
return this.setPlus(false);
return this.setPlus(false, { message: "授权校验失败" });
}
return this.setPlus(true);
return this.setPlus(true, {
expireTime: json.expireTime,
level: json.level || 1,
});
}
verifySignature(content: string, signature: any, publicKey: string) {
@@ -80,6 +98,14 @@ export function isPlus() {
return holder.isPlus;
}
export function getPlusInfo() {
return {
isPlus: holder.isPlus,
level: holder.level,
expireTime: holder.expireTime,
};
}
export async function verify(req: LicenseVerifyReq) {
return await verifier.reVerify(req);
}
@@ -60,7 +60,14 @@ export async function mine(): Promise<UserInfoRes> {
});
}
return await request({
url: "/sys/authority/user/mine",
url: "/mine/info",
method: "post"
});
}
export async function getPlusInfo() {
return await request({
url: "/mine/plusInfo",
method: "post"
});
}
@@ -0,0 +1,9 @@
import { request } from "/@/api/service";
export async function doActive(form: any) {
return await request({
url: "/sys/plus/active",
method: "post",
data: form
});
}
@@ -0,0 +1,83 @@
<template>
<div class="layout-vip" :class="{ 'layout-plus': userStore.plusInfo?.isPlus }">
<contextHolder />
<fs-icon icon="mingcute:vip-1-line"></fs-icon>
<div class="text">
<span v-if="userStore.plusInfo?.isPlus">
<span>专业版</span>
<span>{{ expireTime }}</span>
</span>
<span v-else @click="openUpgrade"> 当前免费版 </span>
</div>
</div>
</template>
<script lang="tsx" setup>
import { ref, reactive } from "vue";
import { useUserStore } from "/@/store/modules/user";
import dayjs from "dayjs";
import { message, Modal } from "ant-design-vue";
import * as api from "./api";
const userStore = useUserStore();
const expireTime = ref("");
if (userStore.plusInfo?.isPlus) {
expireTime.value = dayjs(userStore.plusInfo.expireTime).format("YYYY-MM-DD");
}
const formState = reactive({
code: ""
});
async function doActive() {
if (!formState.code) {
message.error("请输入激活码");
throw new Error("请输入激活码");
}
const res = await api.doActive(formState);
if (res) {
await userStore.reInit();
Modal.success({
title: "激活成功",
content: `您已成功激活专业版,有效期至:${dayjs(userStore.plusInfo.expireTime).format("YYYY-MM-DD")}`
});
}
}
const [modal, contextHolder] = Modal.useModal();
function openUpgrade() {
const placeholder = "请输入激活码";
modal.confirm({
title: "升级专业版",
async onOk() {
await doActive();
},
content: () => {
return (
<div class="mt-10 mb-10">
<a-input v-model:value={formState.code} placeholder={placeholder} />
<div class="mt-10">
<a href="https://afdian.com/a/greper" target="_blank">
爱发电赞助获取激活码
</a>
</div>
</div>
);
}
});
}
</script>
<style lang="less">
.layout-vip {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
&.isPlus {
color: #c5913f;
}
.text {
margin-left: 5px;
}
}
</style>
@@ -12,14 +12,14 @@
<a-layout class="layout-body">
<a-layout-header class="header">
<div class="header-buttons">
<div class="header-left header-buttons">
<div class="menu-fold" @click="asideCollapsedToggle">
<MenuUnfoldOutlined v-if="asideCollapsed" />
<MenuFoldOutlined v-else />
</div>
<fs-menu class="header-menu" mode="horizontal" :expand-selected="false" :selectable="false" :menus="frameworkMenus" />
<vip-info class="flex-center header-btn"></vip-info>
</div>
<fs-menu class="header-menu" mode="horizontal" :expand-selected="false" :selectable="false" :menus="frameworkMenus" />
<div class="header-right header-buttons">
<!-- <button-->
<!-- w:bg="blue-400 hover:blue-500 dark:blue-500 dark:hover:blue-600"-->
@@ -83,10 +83,11 @@ import { MenuFoldOutlined, MenuUnfoldOutlined } from "@ant-design/icons-vue";
import FsThemeSet from "/@/layout/components/theme/index.vue";
import { env } from "../utils/util.env";
import FsThemeModeSet from "./components/theme/mode-set.vue";
import VipInfo from "./components/vip-info/index.vue";
export default {
name: "LayoutFramework",
// eslint-disable-next-line vue/no-unused-components
components: { FsThemeSet, MenuFoldOutlined, MenuUnfoldOutlined, FsMenu, FsLocale, FsSourceLink, FsUserInfo, FsTabs, FsThemeModeSet },
components: { FsThemeSet, MenuFoldOutlined, MenuUnfoldOutlined, FsMenu, FsLocale, FsSourceLink, FsUserInfo, FsTabs, FsThemeModeSet, VipInfo },
setup() {
const resourceStore = useResourceStore();
const frameworkMenus = computed(() => {
@@ -133,6 +134,7 @@ export default {
.fs-framework {
height: 100%;
overflow-x: hidden;
min-width: 1200px;
.menu-fold {
display: flex;
justify-content: center;
@@ -174,34 +176,41 @@ export default {
padding: 5px;
}
}
.header-buttons {
display: flex;
align-items: center;
& > * {
cursor: pointer;
padding: 0 10px;
}
height: 100%;
& > .header-btn {
display: inline-flex;
justify-content: center;
.ant-layout-header.header {
display: flex;
justify-content: space-between;
align-items: center;
.header-buttons {
display: flex;
align-items: center;
& > * {
cursor: pointer;
padding: 0 10px;
}
height: 100%;
//border-bottom: 1px solid rgba(255, 255, 255, 0);
&:hover {
background-color: #fff;
& > .header-btn {
display: inline-flex;
justify-content: center;
align-items: center;
height: 100%;
//border-bottom: 1px solid rgba(255, 255, 255, 0);
&:hover {
background-color: #fff;
}
}
}
.header-right {
justify-content: flex-end;
align-items: center;
display: flex;
}
.header-menu {
flex: 1;
}
}
.header-right {
justify-content: flex-end;
align-items: center;
display: flex;
}
.header-menu {
flex: 1;
}
.aside-menu {
flex: 1;
ui {
@@ -146,7 +146,6 @@ export default {
// position: absolute;
width: 100%;
bottom: 0;
padding: 0 16px;
margin: 48px 0 24px;
text-align: center;
@@ -33,6 +33,7 @@ router.beforeEach(async (to, from, next) => {
// 请根据自身业务需要修改
const token = userStore.getToken;
if (token) {
await userStore.init();
next();
} else {
// 没有登录的时候跳转到登录界面
@@ -1,21 +1,28 @@
import { defineStore } from "pinia";
import { store } from "../index";
import router from "../../router";
// @ts-ignore
import { LocalStorage } from "/src/utils/util.storage";
// @ts-ignore
import * as UserApi from "/src/api/modules/api.user";
import { RegisterReq } from "/src/api/modules/api.user";
// @ts-ignore
import { LoginReq, UserInfoRes } from "/@/api/modules/api.user";
import { Modal, notification } from "ant-design-vue";
import { useI18n } from "vue-i18n";
import { mitter } from "/src/utils/util.mitt";
import { RegisterReq } from "/src/api/modules/api.user";
interface UserState {
userInfo: Nullable<UserInfoRes>;
token?: string;
plusInfo?: PlusInfo;
inited: boolean;
}
interface PlusInfo {
level: number;
expireTime: number;
isPlus: boolean;
}
const USER_INFO_KEY = "USER_INFO";
@@ -26,7 +33,10 @@ export const useUserStore = defineStore({
// user info
userInfo: null,
// token
token: undefined
token: undefined,
// plus
plusInfo: null,
inited: false
}),
getters: {
getUserInfo(): UserInfoRes {
@@ -73,10 +83,7 @@ export const useUserStore = defineStore({
// save token
this.setToken(token, expire);
// get user info
const userInfo = await this.getUserInfoAction();
await router.replace("/");
mitter.emit("app.login", { userInfo, token: data });
return userInfo;
return await this.onLoginSuccess(data);
} catch (error) {
return null;
}
@@ -86,6 +93,19 @@ export const useUserStore = defineStore({
this.setUserInfo(userInfo);
return userInfo;
},
async onLoginSuccess(loginData: any) {
await this.getUserInfoAction();
await this.loadPlusInfo();
const userInfo = await this.getUserInfoAction();
mitter.emit("app.login", { userInfo, token: loginData, plusInfo: this.plusInfo });
await router.replace("/");
return userInfo;
},
async loadPlusInfo() {
this.plusInfo = await UserApi.getPlusInfo();
},
/**
* @description: logout
*/
@@ -108,6 +128,19 @@ export const useUserStore = defineStore({
await this.logout(true);
}
});
},
async init() {
if (this.inited) {
return;
}
if (this.getToken) {
await this.loadPlusInfo();
}
this.inited = true;
},
async reInit() {
this.inited = false;
await this.init();
}
}
});
@@ -14,9 +14,6 @@ html, body {
box-sizing: border-box;
}
body{
min-width: 1000px;
}
div#app {
height: 100%
}
@@ -48,6 +45,11 @@ h1, h2, h3, h4, h5, h6 {
vertical-align: 0 !important;
}
.flex-center{
display: flex;
justify-content: center;
align-items: center;
}
.flex-o{
display: flex !important;
align-items: center;
@@ -1 +1,5 @@
alter table cd_access alter column setting type text using setting::text;
alter table sys_settings alter column setting type text using setting::text;
alter table user_settings alter column setting type text using setting::text;
alter table pi_history_log alter column logs type text using logs::text;
alter table pi_history alter column pipeline type text using pipeline::text;
@@ -31,6 +31,10 @@ export const Constants = {
code: 10,
message: '参数错误',
},
needvip: {
code: 88,
message: '需要VIP',
},
auth: {
code: 401,
message: '您还未登录或token已过期',
@@ -0,0 +1,10 @@
import { Constants } from '../constants.js';
import { BaseException } from './base-exception.js';
/**
* 需要vip异常
*/
export class NeedVIPException extends BaseException {
constructor(message) {
super('NeedVIPException', Constants.res.needvip.code, message ? message : Constants.res.needvip.message);
}
}
@@ -8,7 +8,6 @@ import * as staticFile from '@midwayjs/static-file';
import * as cron from './modules/plugin/cron/index.js';
import * as flyway from '@certd/midway-flyway-js';
import cors from '@koa/cors';
import { ReportMiddleware } from './middleware/report.js';
import { GlobalExceptionMiddleware } from './middleware/global-exception.js';
import { PreviewMiddleware } from './middleware/preview.js';
import { AuthorityMiddleware } from './middleware/authority.js';
@@ -60,7 +59,6 @@ export class MainConfiguration {
//this.app.use(bodyParser());
//请求日志打印
this.app.useMiddleware([
ReportMiddleware,
//统一异常处理
GlobalExceptionMiddleware,
//预览模式限制修改id<1000的数据
@@ -12,7 +12,7 @@ export class GlobalExceptionMiddleware implements IWebMiddleware {
logger.info('请求开始:', url);
try {
await next();
logger.info('请求完成', url, Date.now() - startTime + 'ms');
logger.info('请求完成:', url, Date.now() - startTime + 'ms');
} catch (err) {
logger.error('请求异常:', url, Date.now() - startTime + 'ms', err);
ctx.status = 200;
@@ -1,27 +0,0 @@
import { Middleware, IMiddleware } from '@midwayjs/core';
import { NextFunction, Context } from '@midwayjs/koa';
@Middleware()
export class ReportMiddleware implements IMiddleware<Context, NextFunction> {
resolve() {
return async (ctx: Context, next: NextFunction) => {
// 控制器前执行的逻辑
const startTime = Date.now();
// 执行下一个 Web 中间件,最后执行到控制器
// 这里可以拿到下一个中间件或者控制器的返回值
const result = await next();
// 控制器之后执行的逻辑
ctx.logger.info(
`Report in "src/middleware/report.middleware.ts", rt = ${
Date.now() - startTime
}ms`
);
// 返回给上一个中间件的结果
return result;
};
}
static getName(): string {
return 'report';
}
}
@@ -1,24 +0,0 @@
import { Provide } from '@midwayjs/core';
import { IWebMiddleware, IMidwayKoaContext, NextFunction } from '@midwayjs/koa';
import { logger } from '../utils/logger.js';
@Provide()
export class ReportMiddleware implements IWebMiddleware {
resolve() {
return async (ctx: IMidwayKoaContext, next: NextFunction) => {
const { url } = ctx;
logger.info('请求开始:', url);
const startTime = Date.now();
await next();
if (ctx.status !== 200) {
logger.error(
'请求失败:',
url,
ctx.status,
Date.now() - startTime + 'ms'
);
}
logger.info('请求完成:', url, ctx.status, Date.now() - startTime + 'ms');
};
}
}
@@ -2,6 +2,7 @@ import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { BaseController } from '../../../basic/base-controller.js';
import { Constants } from '../../../basic/constants.js';
import { UserService } from '../../authority/service/user-service.js';
import { getPlusInfo } from '@certd/pipeline';
/**
*/
@@ -24,4 +25,12 @@ export class MineController extends BaseController {
await this.userService.changePassword(userId, body);
return this.ok({});
}
@Post('/plusInfo', { summary: Constants.per.authOnly })
async plusInfo(@Body(ALL) body) {
const info = getPlusInfo();
return this.ok({
...info,
});
}
}
@@ -4,7 +4,7 @@ import { In, Repository } from 'typeorm';
import { BaseService } from '../../../basic/base-service.js';
import { PipelineEntity } from '../entity/pipeline.js';
import { PipelineDetail } from '../entity/vo/pipeline-detail.js';
import { Executor, Pipeline, ResultType, RunHistory } from '@certd/pipeline';
import { Executor, isPlus, Pipeline, ResultType, RunHistory } from '@certd/pipeline';
import { AccessService } from './access-service.js';
import { DbStorage } from './db-storage.js';
import { StorageService } from './storage-service.js';
@@ -15,9 +15,10 @@ import { HistoryLogEntity } from '../entity/history-log.js';
import { HistoryLogService } from './history-log-service.js';
import { logger } from '../../../utils/logger.js';
import { EmailService } from '../../basic/service/email-service.js';
import { NeedVIPException } from '../../../basic/exception/vip-exception.js';
const runningTasks: Map<string | number, Executor> = new Map();
const freeCount = 10;
/**
* 证书申请
*/
@@ -47,6 +48,17 @@ export class PipelineService extends BaseService<PipelineEntity> {
return this.repository;
}
async add(bean: PipelineEntity) {
if (!isPlus()) {
const count = await this.repository.count();
if (count >= freeCount) {
throw new NeedVIPException('免费版最多只能创建10个pipeline');
}
}
await super.add(bean);
return bean;
}
async page(query: any, page: { offset: number; limit: number }, order: any, buildQuery: any) {
const result = await super.page(query, page, order, buildQuery);
const pipelineIds: number[] = [];
@@ -93,6 +105,12 @@ export class PipelineService extends BaseService<PipelineEntity> {
}
async save(bean: PipelineEntity) {
if (!isPlus()) {
const count = await this.repository.count();
if (count >= 10) {
throw new NeedVIPException('免费版最多只能创建10个pipeline');
}
}
await this.clearTriggers(bean.id);
if (bean.content) {
const pipeline = JSON.parse(bean.content);
@@ -0,0 +1,57 @@
import { ALL, Body, Controller, Inject, Post, Provide } from '@midwayjs/core';
import { SysSettingsService } from '../service/sys-settings-service.js';
import { BaseController } from '../../../basic/base-controller.js';
import { appKey, utils, verify } from '@certd/pipeline';
import { SysInstallInfo, SysLicenseInfo } from '../service/models.js';
import { logger } from '../../../utils/logger.js';
/**
*/
@Provide()
@Controller('/api/sys/plus')
export class SysPlusController extends BaseController {
@Inject()
sysSettingsService: SysSettingsService;
@Post('/active', { summary: 'sys:settings:edit' })
async active(@Body(ALL) body) {
const { code } = body;
const installInfo: SysInstallInfo = await this.sysSettingsService.getSetting(SysInstallInfo);
const formData = {
appKey: appKey,
code,
subjectId: installInfo.siteId,
};
const res: any = await utils.http({
url: 'https://api.ai.handsfree.work/activation/active',
method: 'post',
data: formData,
});
if (res.code > 0) {
logger.error('激活失败', res.message);
return this.fail(res.message, 1);
}
const license = res.data.license;
let licenseInfo: SysLicenseInfo = await this.sysSettingsService.getSetting(SysLicenseInfo);
if (!licenseInfo) {
licenseInfo = new SysLicenseInfo();
}
licenseInfo.license = license;
await this.sysSettingsService.saveSetting(licenseInfo);
const verifyRes = await verify({
subjectId: installInfo.siteId,
license,
});
if (!verifyRes.isPlus) {
const message = verifyRes.message || '授权码校验失败';
logger.error(message);
return this.fail(message, 1);
}
return this.ok(res.data);
}
}
@@ -0,0 +1 @@
export * from './plugins/index.js';
@@ -0,0 +1 @@
export * from './plugin-k8s.js';
@@ -0,0 +1,109 @@
import { AbstractTaskPlugin, IsTaskPlugin, pluginGroups, RunStrategy, TaskInput, utils } from '@certd/pipeline';
import { CertInfo } from '@certd/plugin-cert';
import { K8sClient } from '@certd/lib-k8s';
import { K8sAccess } from '../access/index.js';
import { appendTimeSuffix } from '../../plugin-aliyun/utils/index.js';
@IsTaskPlugin({
name: 'DeployToK8SIngress',
title: 'K8S Ingress证书部署',
group: pluginGroups.other.key,
default: {
strategy: {
runStrategy: RunStrategy.SkipWhenSucceed,
},
},
})
export class K8STestPlugin extends AbstractTaskPlugin {
@TaskInput({
title: '命名空间',
value: 'default',
component: {
placeholder: '命名空间',
},
required: true,
})
namespace!: string;
@TaskInput({
title: 'ingress名称',
value: '',
component: {
placeholder: 'ingress名称',
},
required: true,
helper: '可以传入一个数组',
})
ingressName!: string;
@TaskInput({
title: '保密字典Id',
component: {
placeholder: '保密字典Id',
},
required: true,
})
secretName!: string | string[];
@TaskInput({
title: 'k8s授权',
helper: 'kubeconfig',
component: {
name: 'pi-access-selector',
type: 'k8s',
},
required: true,
})
accessId!: string;
@TaskInput({
title: '域名证书',
helper: '请选择前置任务输出的域名证书',
component: {
name: 'pi-output-selector',
},
required: true,
})
cert!: CertInfo;
async onInstance() {}
async execute(): Promise<void> {
const access: K8sAccess = await this.accessService.getById(this.accessId);
const k8sClient = new K8sClient({
kubeConfigStr: access.kubeconfig,
logger: this.logger,
});
await this.patchNginxCertSecret({ cert: this.cert, k8sClient });
await utils.sleep(3000); // 停留2秒,等待secret部署完成
}
async patchNginxCertSecret(options: { cert: CertInfo; k8sClient: K8sClient }) {
const { cert, k8sClient } = options;
const crt = cert.crt;
const key = cert.key;
const crtBase64 = Buffer.from(crt).toString('base64');
const keyBase64 = Buffer.from(key).toString('base64');
const { namespace, secretName } = this;
const body: any = {
data: {
'tls.crt': crtBase64,
'tls.key': keyBase64,
},
metadata: {
labels: {
certd: appendTimeSuffix('certd'),
},
},
};
let secretNames: any = secretName;
if (typeof secretName === 'string') {
secretNames = [secretName];
}
for (const secret of secretNames) {
await k8sClient.patchSecret({ namespace, secretName: secret, body });
this.logger.info(`ingress cert Secret已更新:${secret}`);
}
}
}
new K8STestPlugin();