feat: add link expiration
This commit is contained in:
parent
feb14faf27
commit
00fc1faed9
@ -8,6 +8,8 @@ import { ifProp } from "styled-tools";
|
||||
import getConfig from "next/config";
|
||||
import QRCode from "qrcode.react";
|
||||
import Link from "next/link";
|
||||
import differenceInMilliseconds from "date-fns/differenceInMilliseconds";
|
||||
import ms from "ms";
|
||||
|
||||
import { removeProtocol, withComma, errorMessage } from "../utils";
|
||||
import { useStoreActions, useStoreState } from "../store";
|
||||
@ -112,7 +114,8 @@ interface BanForm {
|
||||
interface EditForm {
|
||||
target: string;
|
||||
address: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
expire_in?: string;
|
||||
}
|
||||
|
||||
const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
|
||||
@ -124,7 +127,12 @@ const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
|
||||
{
|
||||
target: link.target,
|
||||
address: link.address,
|
||||
description: link.description
|
||||
description: link.description,
|
||||
expire_in: link.expire_in
|
||||
? ms(differenceInMilliseconds(new Date(link.expire_in), new Date()), {
|
||||
long: true
|
||||
})
|
||||
: ""
|
||||
},
|
||||
{ withIds: true }
|
||||
);
|
||||
@ -189,9 +197,20 @@ const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
|
||||
)}
|
||||
</Col>
|
||||
</Td>
|
||||
<Td {...createdFlex}>{`${formatDistanceToNow(
|
||||
new Date(link.created_at)
|
||||
)} ago`}</Td>
|
||||
<Td {...createdFlex} flexDirection="column" alignItems="flex-start">
|
||||
<Text>{formatDistanceToNow(new Date(link.created_at))} ago</Text>
|
||||
{link.expire_in && (
|
||||
<Text fontSize={[13, 14]} color="#888">
|
||||
Expires in{" "}
|
||||
{ms(
|
||||
differenceInMilliseconds(new Date(link.expire_in), new Date()),
|
||||
{
|
||||
long: true
|
||||
}
|
||||
)}
|
||||
</Text>
|
||||
)}
|
||||
</Td>
|
||||
<Td {...shortLinkFlex} withFade>
|
||||
{copied ? (
|
||||
<Animation
|
||||
@ -362,7 +381,7 @@ const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
|
||||
</Col>
|
||||
</Flex>
|
||||
<Flex alignItems="flex-start" width={1} mt={3}>
|
||||
<Col alignItems="flex-start">
|
||||
<Col alignItems="flex-start" mr={3}>
|
||||
<Text
|
||||
{...label("description")}
|
||||
as="label"
|
||||
@ -386,6 +405,30 @@ const Row: FC<RowProps> = ({ index, link, setDeleteModal }) => {
|
||||
/>
|
||||
</Flex>
|
||||
</Col>
|
||||
<Col alignItems="flex-start">
|
||||
<Text
|
||||
{...label("expire_in")}
|
||||
as="label"
|
||||
mb={2}
|
||||
fontSize={[14, 15]}
|
||||
bold
|
||||
>
|
||||
Expire in:
|
||||
</Text>
|
||||
<Flex as="form">
|
||||
<TextInput
|
||||
{...text("expire_in")}
|
||||
placeholder="2 minutes/hours/days"
|
||||
placeholderSize={[13, 14]}
|
||||
fontSize={[14, 15]}
|
||||
height={[40, 44]}
|
||||
width={[1, 210, 240]}
|
||||
pl={[3, 24]}
|
||||
pr={[3, 24]}
|
||||
required
|
||||
/>
|
||||
</Flex>
|
||||
</Col>
|
||||
</Flex>
|
||||
<Button
|
||||
color="blue"
|
||||
|
@ -55,6 +55,7 @@ interface Form {
|
||||
customurl?: string;
|
||||
password?: string;
|
||||
description?: string;
|
||||
expire_in?: string;
|
||||
showAdvanced?: boolean;
|
||||
}
|
||||
|
||||
@ -256,7 +257,7 @@ const Shortener = () => {
|
||||
mb={2}
|
||||
bold
|
||||
>
|
||||
Domain
|
||||
Domain:
|
||||
</Text>
|
||||
<Select
|
||||
{...select("domain")}
|
||||
@ -323,15 +324,38 @@ const Shortener = () => {
|
||||
</Col>
|
||||
</Flex>
|
||||
<Flex mt={[3]} flexDirection={["column", "row"]}>
|
||||
<Col width={1}>
|
||||
<Col>
|
||||
<Text
|
||||
as="description"
|
||||
as="label"
|
||||
{...label("expire_in")}
|
||||
fontSize={[14, 15]}
|
||||
mb={2}
|
||||
bold
|
||||
>
|
||||
Expire in:
|
||||
</Text>
|
||||
<TextInput
|
||||
{...text("expire_in")}
|
||||
placeholder="2 minutes/hours/days"
|
||||
data-lpignore
|
||||
pl={[3, 24]}
|
||||
pr={[3, 24]}
|
||||
placeholderSize={[13, 14]}
|
||||
fontSize={[14, 15]}
|
||||
height={[40, 44]}
|
||||
width={[1, 210, 240]}
|
||||
maxWidth="100%"
|
||||
/>
|
||||
</Col>
|
||||
<Col width={2 / 3} ml={[0, 26]}>
|
||||
<Text
|
||||
as="label"
|
||||
{...label("description")}
|
||||
fontSize={[14, 15]}
|
||||
mb={2}
|
||||
bold
|
||||
>
|
||||
Description
|
||||
Description:
|
||||
</Text>
|
||||
<TextInput
|
||||
{...text("description")}
|
||||
|
@ -47,6 +47,8 @@ export enum Colors {
|
||||
TableHeadBg = "hsl(200, 12%, 95%)",
|
||||
TableHeadBorder = "hsl(200, 14%, 94%)",
|
||||
TableRowHover = "hsl(200, 14%, 98%)",
|
||||
TableRowBanned = "hsl(0, 100%, 98%)",
|
||||
TableRowBannedHower = "hsl(0, 100%, 96%)",
|
||||
TableShadow = "hsla(200, 20%, 70%, 0.3)",
|
||||
Text = "hsl(200, 35%, 25%)",
|
||||
TrashIcon = "hsl(0, 100%, 69%)",
|
||||
|
@ -16,6 +16,7 @@ export interface Link {
|
||||
domain_id?: number;
|
||||
password?: string;
|
||||
description?: string;
|
||||
expire_in?: string;
|
||||
target: string;
|
||||
updated_at: string;
|
||||
user_id?: number;
|
||||
@ -43,7 +44,8 @@ export interface EditLink {
|
||||
id: string;
|
||||
target: string;
|
||||
address: string;
|
||||
description: string;
|
||||
description?: string;
|
||||
expire_in?: string;
|
||||
}
|
||||
|
||||
export interface LinksQuery {
|
||||
|
@ -524,6 +524,10 @@ export default {
|
||||
description: {
|
||||
type: "string"
|
||||
},
|
||||
expire_in: {
|
||||
type: "string",
|
||||
example: "2 minutes/hours/days"
|
||||
},
|
||||
password: {
|
||||
type: "string"
|
||||
},
|
||||
@ -547,12 +551,20 @@ export default {
|
||||
}
|
||||
},
|
||||
body_1: {
|
||||
required: ["target", "address"],
|
||||
properties: {
|
||||
target: {
|
||||
type: "string"
|
||||
},
|
||||
address: {
|
||||
type: "string"
|
||||
},
|
||||
description: {
|
||||
type: "string"
|
||||
},
|
||||
expire_in: {
|
||||
type: "string",
|
||||
example: "2 minutes/hours/days"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
9
global.d.ts
vendored
9
global.d.ts
vendored
@ -69,14 +69,15 @@ interface IP {
|
||||
}
|
||||
|
||||
interface Link {
|
||||
id: number;
|
||||
address: string;
|
||||
banned: boolean;
|
||||
banned_by_id?: number;
|
||||
banned: boolean;
|
||||
created_at: string;
|
||||
domain_id?: number;
|
||||
password?: string;
|
||||
description?: string;
|
||||
domain_id?: number;
|
||||
expire_in: string;
|
||||
id: number;
|
||||
password?: string;
|
||||
target: string;
|
||||
updated_at: string;
|
||||
user_id?: number;
|
||||
|
36
package-lock.json
generated
36
package-lock.json
generated
@ -5183,19 +5183,19 @@
|
||||
},
|
||||
"babel-plugin-syntax-async-functions": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "http://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-async-functions/-/babel-plugin-syntax-async-functions-6.13.0.tgz",
|
||||
"integrity": "sha1-ytnK0RkbWtY0vzCuCHI5HgZHvpU=",
|
||||
"dev": true
|
||||
},
|
||||
"babel-plugin-syntax-exponentiation-operator": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "http://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-exponentiation-operator/-/babel-plugin-syntax-exponentiation-operator-6.13.0.tgz",
|
||||
"integrity": "sha1-nufoM3KQ2pUoggGmpX9BcDF4MN4=",
|
||||
"dev": true
|
||||
},
|
||||
"babel-plugin-syntax-jsx": {
|
||||
"version": "6.18.0",
|
||||
"resolved": "http://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz",
|
||||
"integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY="
|
||||
},
|
||||
"babel-plugin-syntax-trailing-function-commas": {
|
||||
@ -5460,13 +5460,13 @@
|
||||
"dependencies": {
|
||||
"jsesc": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "http://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
|
||||
"integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=",
|
||||
"dev": true
|
||||
},
|
||||
"regexpu-core": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "http://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-2.0.0.tgz",
|
||||
"integrity": "sha1-SdA4g3uNz4v6W5pCE5k45uoq4kA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@ -5477,13 +5477,13 @@
|
||||
},
|
||||
"regjsgen": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "http://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.2.0.tgz",
|
||||
"integrity": "sha1-bAFq3qxVT3WCP+N6wFuS1aTtsfc=",
|
||||
"dev": true
|
||||
},
|
||||
"regjsparser": {
|
||||
"version": "0.1.5",
|
||||
"resolved": "http://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
|
||||
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.1.5.tgz",
|
||||
"integrity": "sha1-fuj4Tcb6eS0/0K4ijSS9lJ6tIFw=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@ -5995,7 +5995,7 @@
|
||||
},
|
||||
"browserify-aes": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
|
||||
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
|
||||
"requires": {
|
||||
"buffer-xor": "^1.0.3",
|
||||
@ -6029,7 +6029,7 @@
|
||||
},
|
||||
"browserify-rsa": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
|
||||
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
|
||||
"requires": {
|
||||
"bn.js": "^4.1.0",
|
||||
@ -6906,7 +6906,7 @@
|
||||
},
|
||||
"create-hash": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
|
||||
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
|
||||
"requires": {
|
||||
"cipher-base": "^1.0.1",
|
||||
@ -6918,7 +6918,7 @@
|
||||
},
|
||||
"create-hmac": {
|
||||
"version": "1.1.7",
|
||||
"resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
|
||||
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
|
||||
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
|
||||
"requires": {
|
||||
"cipher-base": "^1.0.3",
|
||||
@ -7615,7 +7615,7 @@
|
||||
},
|
||||
"diffie-hellman": {
|
||||
"version": "5.0.3",
|
||||
"resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
|
||||
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
|
||||
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
|
||||
"requires": {
|
||||
"bn.js": "^4.1.0",
|
||||
@ -9968,7 +9968,7 @@
|
||||
},
|
||||
"got": {
|
||||
"version": "6.7.1",
|
||||
"resolved": "http://registry.npmjs.org/got/-/got-6.7.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/got/-/got-6.7.1.tgz",
|
||||
"integrity": "sha1-JAzQV4WpoY5WHcG0S0HHY+8ejbA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
@ -14040,7 +14040,7 @@
|
||||
},
|
||||
"passport-jwt": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "http://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.0.tgz",
|
||||
"integrity": "sha512-BwC0n2GP/1hMVjR4QpnvqA61TxenUMlmfNjYNgK0ZAs0HK4SOQkHcSv4L328blNTLtHq7DbmvyNJiH+bn6C5Mg==",
|
||||
"requires": {
|
||||
"jsonwebtoken": "^8.2.0",
|
||||
@ -15479,7 +15479,7 @@
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "2.3.6",
|
||||
"resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz",
|
||||
"integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==",
|
||||
"requires": {
|
||||
"core-util-is": "~1.0.0",
|
||||
@ -16065,7 +16065,7 @@
|
||||
},
|
||||
"safe-regex": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "http://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
|
||||
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
|
||||
"integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=",
|
||||
"requires": {
|
||||
"ret": "~0.1.10"
|
||||
@ -16275,7 +16275,7 @@
|
||||
},
|
||||
"sha.js": {
|
||||
"version": "2.4.11",
|
||||
"resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
|
||||
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
|
||||
"requires": {
|
||||
"inherits": "^2.0.1",
|
||||
@ -16837,7 +16837,7 @@
|
||||
},
|
||||
"string_decoder": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
|
||||
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
|
||||
"requires": {
|
||||
"safe-buffer": "~5.1.0"
|
||||
|
@ -8,3 +8,9 @@ if (env.NON_USER_COOLDOWN) {
|
||||
query.ip.clear().catch();
|
||||
});
|
||||
}
|
||||
|
||||
cron.schedule("*/15 * * * * *", () => {
|
||||
query.link
|
||||
.batchRemove({ expire_in: ["<", new Date().toISOString()] })
|
||||
.catch();
|
||||
});
|
||||
|
@ -42,7 +42,15 @@ export const get: Handler = async (req, res) => {
|
||||
};
|
||||
|
||||
export const create: Handler = async (req: CreateLinkReq, res) => {
|
||||
const { reuse, password, customurl, description, target, domain } = req.body;
|
||||
const {
|
||||
reuse,
|
||||
password,
|
||||
customurl,
|
||||
description,
|
||||
target,
|
||||
domain,
|
||||
expire_in
|
||||
} = req.body;
|
||||
const domain_id = domain ? domain.id : null;
|
||||
|
||||
const targetDomain = URL.parse(target).hostname;
|
||||
@ -87,6 +95,7 @@ export const create: Handler = async (req: CreateLinkReq, res) => {
|
||||
domain_id,
|
||||
description,
|
||||
target,
|
||||
expire_in,
|
||||
user_id: req.user && req.user.id
|
||||
});
|
||||
|
||||
@ -100,7 +109,7 @@ export const create: Handler = async (req: CreateLinkReq, res) => {
|
||||
};
|
||||
|
||||
export const edit: Handler = async (req, res) => {
|
||||
const { address, target, description } = req.body;
|
||||
const { address, target, description, expire_in } = req.body;
|
||||
|
||||
if (!address && !target) {
|
||||
throw new CustomError("Should at least update one field.");
|
||||
@ -144,7 +153,8 @@ export const edit: Handler = async (req, res) => {
|
||||
{
|
||||
...(address && { address }),
|
||||
...(description && { description }),
|
||||
...(target && { target })
|
||||
...(target && { target }),
|
||||
...(expire_in && { expire_in })
|
||||
}
|
||||
);
|
||||
|
||||
|
1
server/handlers/types.d.ts
vendored
1
server/handlers/types.d.ts
vendored
@ -6,6 +6,7 @@ export interface CreateLinkReq extends Request {
|
||||
password?: string;
|
||||
customurl?: string;
|
||||
description?: string;
|
||||
expire_in?: string;
|
||||
domain?: Domain;
|
||||
target: string;
|
||||
};
|
||||
|
@ -1,11 +1,12 @@
|
||||
import { body, param } from "express-validator";
|
||||
import { isAfter, subDays, subHours } from "date-fns";
|
||||
import { isAfter, subDays, subHours, addMilliseconds } from "date-fns";
|
||||
import urlRegex from "url-regex";
|
||||
import { promisify } from "util";
|
||||
import bcrypt from "bcryptjs";
|
||||
import axios from "axios";
|
||||
import dns from "dns";
|
||||
import URL from "url";
|
||||
import ms from "ms";
|
||||
|
||||
import { CustomError, addProtocol } from "../utils";
|
||||
import query from "../queries";
|
||||
@ -87,6 +88,22 @@ export const createLink = [
|
||||
.trim()
|
||||
.isLength({ min: 0, max: 2040 })
|
||||
.withMessage("Description length must be between 0 and 2040."),
|
||||
body("expire_in")
|
||||
.optional({ nullable: true, checkFalsy: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.custom(value => {
|
||||
try {
|
||||
return !!ms(value);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.withMessage("Expire format is invalid. Valid examples: 1m, 8h, 42 days.")
|
||||
.customSanitizer(ms)
|
||||
.custom(value => value >= ms("1m"))
|
||||
.withMessage("Minimum expire time should be '1 minute'.")
|
||||
.customSanitizer(value => addMilliseconds(new Date(), value).toISOString()),
|
||||
body("domain")
|
||||
.optional({ nullable: true, checkFalsy: true })
|
||||
.custom(checkUser)
|
||||
@ -138,8 +155,24 @@ export const editLink = [
|
||||
.withMessage("Custom URL is not valid")
|
||||
.custom(value => !preservedUrls.some(url => url.toLowerCase() === value))
|
||||
.withMessage("You can't use this custom URL."),
|
||||
body("expire_in")
|
||||
.optional({ nullable: true, checkFalsy: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.custom(value => {
|
||||
try {
|
||||
return !!ms(value);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.withMessage("Expire format is invalid. Valid examples: 1m, 8h, 42 days.")
|
||||
.customSanitizer(ms)
|
||||
.custom(value => value >= ms("1m"))
|
||||
.withMessage("Minimum expire time should be '1 minute'.")
|
||||
.customSanitizer(value => addMilliseconds(new Date(), value).toISOString()),
|
||||
body("description")
|
||||
.optional()
|
||||
.optional({ nullable: true, checkFalsy: true })
|
||||
.isString()
|
||||
.trim()
|
||||
.isLength({ min: 0, max: 2040 })
|
||||
|
14
server/migrations/20200730203154_expire_in.ts
Normal file
14
server/migrations/20200730203154_expire_in.ts
Normal file
@ -0,0 +1,14 @@
|
||||
import * as Knex from "knex";
|
||||
|
||||
export async function up(knex: Knex): Promise<any> {
|
||||
const hasExpireIn = await knex.schema.hasColumn("links", "expire_in");
|
||||
if (!hasExpireIn) {
|
||||
await knex.schema.alterTable("links", table => {
|
||||
table.dateTime("expire_in");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export async function down(): Promise<any> {
|
||||
return null;
|
||||
}
|
@ -23,6 +23,7 @@ export async function createLinkTable(knex: Knex) {
|
||||
.references("id")
|
||||
.inTable("domains");
|
||||
table.string("password");
|
||||
table.dateTime("expire_in");
|
||||
table.string("target", 2040).notNullable();
|
||||
table
|
||||
.integer("user_id")
|
||||
|
@ -13,6 +13,7 @@ const selectable = [
|
||||
"links.updated_at",
|
||||
"links.password",
|
||||
"links.description",
|
||||
"links.expire_in",
|
||||
"links.target",
|
||||
"links.visit_count",
|
||||
"links.user_id",
|
||||
@ -135,6 +136,7 @@ export const create = async (params: Create) => {
|
||||
user_id: params.user_id || null,
|
||||
address: params.address,
|
||||
description: params.description || null,
|
||||
expire_in: params.expire_in || null,
|
||||
target: params.target
|
||||
},
|
||||
"*"
|
||||
@ -161,6 +163,22 @@ export const remove = async (match: Partial<Link>) => {
|
||||
return !!deletedLink;
|
||||
};
|
||||
|
||||
export const batchRemove = async (match: Match<Link>) => {
|
||||
const deleteQuery = knex<Link>("links");
|
||||
const findQuery = knex<Link>("links");
|
||||
|
||||
Object.entries(match).forEach(([key, value]) => {
|
||||
findQuery.andWhere(key, ...(Array.isArray(value) ? value : [value]));
|
||||
deleteQuery.andWhere(key, ...(Array.isArray(value) ? value : [value]));
|
||||
});
|
||||
|
||||
const links = await findQuery;
|
||||
|
||||
links.forEach(redis.remove.link);
|
||||
|
||||
await deleteQuery.delete();
|
||||
};
|
||||
|
||||
export const update = async (match: Partial<Link>, update: Partial<Link>) => {
|
||||
const links = await knex<Link>("links")
|
||||
.where(match)
|
||||
|
Loading…
x
Reference in New Issue
Block a user