Merge branch 'develop'
This commit is contained in:
commit
d078f78602
191 changed files with 4346 additions and 1410 deletions
803
src/@types/jsrsasign.d.ts
vendored
Normal file
803
src/@types/jsrsasign.d.ts
vendored
Normal file
|
@ -0,0 +1,803 @@
|
|||
// Attention: Partial Type Definition
|
||||
|
||||
declare module 'jsrsasign' {
|
||||
//// HELPER TYPES
|
||||
|
||||
/**
|
||||
* Attention: The value might be changed by the function.
|
||||
*/
|
||||
type Mutable<T> = T;
|
||||
|
||||
/**
|
||||
* Deprecated: The function might be deleted in future release.
|
||||
*/
|
||||
type Deprecated<T> = T;
|
||||
|
||||
//// COMMON TYPES
|
||||
|
||||
/**
|
||||
* byte number
|
||||
*/
|
||||
type ByteNumber = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255;
|
||||
|
||||
/**
|
||||
* hexadecimal string /[0-9A-F]/
|
||||
*/
|
||||
type HexString = string;
|
||||
|
||||
/**
|
||||
* binary string /[01]/
|
||||
*/
|
||||
type BinString = string;
|
||||
|
||||
/**
|
||||
* base64 string /[A-Za-z0-9+/]=+/
|
||||
*/
|
||||
type Base64String = string;
|
||||
|
||||
/**
|
||||
* base64 URL encoded string /[A-Za-z0-9_-]/
|
||||
*/
|
||||
type Base64URLString = string;
|
||||
|
||||
/**
|
||||
* time value (ex. "151231235959Z")
|
||||
*/
|
||||
type TimeValue = string;
|
||||
|
||||
/**
|
||||
* OID string (ex. '1.2.3.4.567')
|
||||
*/
|
||||
type OID = string;
|
||||
|
||||
/**
|
||||
* OID name
|
||||
*/
|
||||
type OIDName = string;
|
||||
|
||||
/**
|
||||
* PEM formatted string
|
||||
*/
|
||||
type PEM = string;
|
||||
|
||||
//// ASN1 TYPES
|
||||
|
||||
class ASN1Object {
|
||||
public isModified: boolean;
|
||||
|
||||
public hTLV: ASN1TLV;
|
||||
|
||||
public hT: ASN1T;
|
||||
|
||||
public hL: ASN1L;
|
||||
|
||||
public hV: ASN1V;
|
||||
|
||||
public getLengthHexFromValue(): HexString;
|
||||
|
||||
public getEncodedHex(): ASN1TLV;
|
||||
|
||||
public getValueHex(): ASN1V;
|
||||
|
||||
public getFreshValueHex(): ASN1V;
|
||||
}
|
||||
|
||||
class DERAbstractStructured extends ASN1Object {
|
||||
constructor(params?: Partial<Record<'array', ASN1Object[]>>);
|
||||
|
||||
public setByASN1ObjectArray(asn1ObjectArray: ASN1Object[]): void;
|
||||
|
||||
public appendASN1Object(asn1Object: ASN1Object): void;
|
||||
}
|
||||
|
||||
class DERSequence extends DERAbstractStructured {
|
||||
constructor(params?: Partial<Record<'array', ASN1Object[]>>);
|
||||
|
||||
public getFreshValueHex(): ASN1V;
|
||||
}
|
||||
|
||||
//// ASN1HEX TYPES
|
||||
|
||||
/**
|
||||
* ASN.1 DER encoded data (hexadecimal string)
|
||||
*/
|
||||
type ASN1S = HexString;
|
||||
|
||||
/**
|
||||
* index of something
|
||||
*/
|
||||
type Idx<T extends { [idx: string]: unknown } | { [idx: number]: unknown }> = ASN1S extends { [idx: string]: unknown } ? string : ASN1S extends { [idx: number]: unknown } ? number : never;
|
||||
|
||||
/**
|
||||
* byte length of something
|
||||
*/
|
||||
type ByteLength<T extends { length: unknown }> = T['length'];
|
||||
|
||||
/**
|
||||
* ASN.1 L(length) (hexadecimal string)
|
||||
*/
|
||||
type ASN1L = HexString;
|
||||
|
||||
/**
|
||||
* ASN.1 T(tag) (hexadecimal string)
|
||||
*/
|
||||
type ASN1T = HexString;
|
||||
|
||||
/**
|
||||
* ASN.1 V(value) (hexadecimal string)
|
||||
*/
|
||||
type ASN1V = HexString;
|
||||
|
||||
/**
|
||||
* ASN.1 TLV (hexadecimal string)
|
||||
*/
|
||||
type ASN1TLV = HexString;
|
||||
|
||||
/**
|
||||
* ASN.1 object string
|
||||
*/
|
||||
type ASN1ObjectString = string;
|
||||
|
||||
/**
|
||||
* nth
|
||||
*/
|
||||
type Nth = number;
|
||||
|
||||
/**
|
||||
* ASN.1 DER encoded OID value (hexadecimal string)
|
||||
*/
|
||||
type ASN1OIDV = HexString;
|
||||
|
||||
class ASN1HEX {
|
||||
public static getLblen(s: ASN1S, idx: Idx<ASN1S>): ByteLength<ASN1L>;
|
||||
|
||||
public static getL(s: ASN1S, idx: Idx<ASN1S>): ASN1L;
|
||||
|
||||
public static getVblen(s: ASN1S, idx: Idx<ASN1S>): ByteLength<ASN1V>;
|
||||
|
||||
public static getVidx(s: ASN1S, idx: Idx<ASN1S>): Idx<ASN1V>;
|
||||
|
||||
public static getV(s: ASN1S, idx: Idx<ASN1S>): ASN1V;
|
||||
|
||||
public static getTLV(s: ASN1S, idx: Idx<ASN1S>): ASN1TLV;
|
||||
|
||||
public static getNextSiblingIdx(s: ASN1S, idx: Idx<ASN1S>): Idx<ASN1ObjectString>;
|
||||
|
||||
public static getChildIdx(h: ASN1S, pos: Idx<ASN1S>): Idx<ASN1ObjectString>[];
|
||||
|
||||
public static getNthChildIdx(h: ASN1S, idx: Idx<ASN1S>, nth: Nth): Idx<ASN1ObjectString>;
|
||||
|
||||
public static getIdxbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string): Idx<Mutable<Nth[]>>;
|
||||
|
||||
public static getTLVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string): ASN1TLV;
|
||||
|
||||
public static getVbyList(h: ASN1S, currentIndex: Idx<ASN1ObjectString>, nthList: Mutable<Nth[]>, checkingTag?: string, removeUnusedbits?: boolean): ASN1V;
|
||||
|
||||
public static hextooidstr(hex: ASN1OIDV): OID;
|
||||
|
||||
public static dump(hexOrObj: ASN1S | ASN1Object, flags?: Record<string, unknown>, idx?: Idx<ASN1S>, indent?: string): string;
|
||||
|
||||
public static isASN1HEX(hex: string): hex is HexString;
|
||||
|
||||
public static oidname(oidDotOrHex: OID | ASN1OIDV): OIDName;
|
||||
}
|
||||
|
||||
//// BIG INTEGER TYPES (PARTIAL)
|
||||
|
||||
class BigInteger {
|
||||
constructor(a: null);
|
||||
|
||||
constructor(a: number, b: SecureRandom);
|
||||
|
||||
constructor(a: number, b: number, c: SecureRandom);
|
||||
|
||||
constructor(a: unknown);
|
||||
|
||||
constructor(a: string, b: number);
|
||||
|
||||
public am(i: number, x: number, w: number, j: number, c: number, n: number): number;
|
||||
|
||||
public DB: number;
|
||||
|
||||
public DM: number;
|
||||
|
||||
public DV: number;
|
||||
|
||||
public FV: number;
|
||||
|
||||
public F1: number;
|
||||
|
||||
public F2: number;
|
||||
|
||||
protected copyTo(r: Mutable<BigInteger>): void;
|
||||
|
||||
protected fromInt(x: number): void;
|
||||
|
||||
protected fromString(s: string, b: number): void;
|
||||
|
||||
protected clamp(): void;
|
||||
|
||||
public toString(b: number): string;
|
||||
|
||||
public negate(): BigInteger;
|
||||
|
||||
public abs(): BigInteger;
|
||||
|
||||
public compareTo(a: BigInteger): number;
|
||||
|
||||
public bitLength(): number;
|
||||
|
||||
protected dlShiftTo(n: number, r: Mutable<BigInteger>): void;
|
||||
|
||||
protected drShiftTo(n: number, r: Mutable<BigInteger>): void;
|
||||
|
||||
protected lShiftTo(n: number, r: Mutable<BigInteger>): void;
|
||||
|
||||
protected rShiftTo(n: number, r: Mutable<BigInteger>): void;
|
||||
|
||||
protected subTo(a: BigInteger, r: Mutable<BigInteger>): void;
|
||||
|
||||
protected multiplyTo(a: BigInteger, r: Mutable<BigInteger>): void;
|
||||
|
||||
protected squareTo(r: Mutable<BigInteger>): void;
|
||||
|
||||
protected divRemTo(m: BigInteger, q: Mutable<BigInteger>, r: Mutable<BigInteger>): void;
|
||||
|
||||
public mod(a: BigInteger): BigInteger;
|
||||
|
||||
protected invDigit(): number;
|
||||
|
||||
protected isEven(): boolean;
|
||||
|
||||
protected exp(e: number, z: Classic | Montgomery): BigInteger;
|
||||
|
||||
public modPowInt(e: number, m: BigInteger): BigInteger;
|
||||
|
||||
public static ZERO: BigInteger;
|
||||
|
||||
public static ONE: BigInteger;
|
||||
}
|
||||
|
||||
class Classic {
|
||||
constructor(m: BigInteger);
|
||||
|
||||
public convert(x: BigInteger): BigInteger;
|
||||
|
||||
public revert(x: BigInteger): BigInteger;
|
||||
|
||||
public reduce(x: Mutable<BigInteger>): void;
|
||||
|
||||
public mulTo(x: BigInteger, r: Mutable<BigInteger>): void;
|
||||
|
||||
public sqrTo(x: BigInteger, y: BigInteger, r: Mutable<BigInteger>): void;
|
||||
}
|
||||
|
||||
class Montgomery {
|
||||
constructor(m: BigInteger);
|
||||
|
||||
public convert(x: BigInteger): BigInteger;
|
||||
|
||||
public revert(x: BigInteger): BigInteger;
|
||||
|
||||
public reduce(x: Mutable<BigInteger>): void;
|
||||
|
||||
public mulTo(x: BigInteger, r: Mutable<BigInteger>): void;
|
||||
|
||||
public sqrTo(x: BigInteger, y: BigInteger, r: Mutable<BigInteger>): void;
|
||||
}
|
||||
|
||||
//// KEYUTIL TYPES
|
||||
|
||||
type DecryptAES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
|
||||
|
||||
type Decrypt3DES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
|
||||
|
||||
type DecryptDES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
|
||||
|
||||
type EncryptAES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
|
||||
|
||||
type Encrypt3DES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
|
||||
|
||||
type EncryptDES = (dataHex: HexString, keyHex: HexString, ivHex: HexString) => HexString;
|
||||
|
||||
type AlgList = {
|
||||
'AES-256-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 32; ivlen: 16; };
|
||||
'AES-192-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 24; ivlen: 16; };
|
||||
'AES-128-CBC': { 'proc': DecryptAES; 'eproc': EncryptAES; keylen: 16; ivlen: 16; };
|
||||
'DES-EDE3-CBC': { 'proc': Decrypt3DES; 'eproc': Encrypt3DES; keylen: 24; ivlen: 8; };
|
||||
'DES-CBC': { 'proc': DecryptDES; 'eproc': EncryptDES; keylen: 8; ivlen: 8; };
|
||||
};
|
||||
|
||||
type AlgName = keyof AlgList;
|
||||
|
||||
type PEMHeadAlgName = 'RSA' | 'EC' | 'DSA';
|
||||
|
||||
type GetKeyRSAParam = RSAKey | {
|
||||
n: BigInteger;
|
||||
e: number;
|
||||
} | Record<'n' | 'e', HexString> | Record<'n' | 'e', HexString> & Record<'d' | 'p' | 'q' | 'dp' | 'dq' | 'co', HexString | null> | {
|
||||
n: BigInteger;
|
||||
e: number;
|
||||
d: BigInteger;
|
||||
} | {
|
||||
kty: 'RSA';
|
||||
} & Record<'n' | 'e', Base64URLString> | {
|
||||
kty: 'RSA';
|
||||
} & Record<'n' | 'e' | 'd' | 'p' | 'q' | 'dp' | 'dq' | 'qi', Base64URLString> | {
|
||||
kty: 'RSA';
|
||||
} & Record<'n' | 'e' | 'd', Base64URLString>;
|
||||
|
||||
type GetKeyECDSAParam = KJUR.crypto.ECDSA | {
|
||||
curve: KJUR.crypto.CurveName;
|
||||
xy: HexString;
|
||||
} | {
|
||||
curve: KJUR.crypto.CurveName;
|
||||
d: HexString;
|
||||
} | {
|
||||
kty: 'EC';
|
||||
crv: KJUR.crypto.CurveName;
|
||||
x: Base64URLString;
|
||||
y: Base64URLString;
|
||||
} | {
|
||||
kty: 'EC';
|
||||
crv: KJUR.crypto.CurveName;
|
||||
x: Base64URLString;
|
||||
y: Base64URLString;
|
||||
d: Base64URLString;
|
||||
};
|
||||
|
||||
type GetKeyDSAParam = KJUR.crypto.DSA | Record<'p' | 'q' | 'g', BigInteger> & Record<'y', BigInteger | null> | Record<'p' | 'q' | 'g' | 'x', BigInteger> & Record<'y', BigInteger | null>;
|
||||
|
||||
type GetKeyParam = GetKeyRSAParam | GetKeyECDSAParam | GetKeyDSAParam | string;
|
||||
|
||||
class KEYUTIL {
|
||||
public version: '1.0.0';
|
||||
|
||||
public parsePKCS5PEM(sPKCS5PEM: PEM): Partial<Record<'type' | 's', string>> & (Record<'cipher' | 'ivsalt', string> | Record<'cipher' | 'ivsalt', undefined>);
|
||||
|
||||
public getKeyAndUnusedIvByPasscodeAndIvsalt(algName: AlgName, passcode: string, ivsaltHex: HexString): Record<'keyhex' | 'ivhex', HexString>;
|
||||
|
||||
public decryptKeyB64(privateKeyB64: Base64String, sharedKeyAlgName: AlgName, sharedKeyHex: HexString, ivsaltHex: HexString): Base64String;
|
||||
|
||||
public getDecryptedKeyHex(sEncryptedPEM: PEM, passcode: string): HexString;
|
||||
|
||||
public getEncryptedPKCS5PEMFromPrvKeyHex(pemHeadAlg: PEMHeadAlgName, hPrvKey: string, passcode: string, sharedKeyAlgName?: AlgName | null, ivsaltHex?: HexString | null): PEM;
|
||||
|
||||
public parseHexOfEncryptedPKCS8(sHEX: HexString): {
|
||||
ciphertext: ASN1V;
|
||||
encryptionSchemeAlg: 'TripleDES';
|
||||
encryptionSchemeIV: ASN1V;
|
||||
pbkdf2Salt: ASN1V;
|
||||
pbkdf2Iter: number;
|
||||
};
|
||||
|
||||
public getPBKDF2KeyHexFromParam(info: ReturnType<this['parseHexOfEncryptedPKCS8']>, passcode: string): HexString;
|
||||
|
||||
private _getPlainPKCS8HexFromEncryptedPKCS8PEM(pkcs8PEM: PEM, passcode: string): HexString;
|
||||
|
||||
public getKeyFromEncryptedPKCS8PEM(prvKeyHex: HexString): ReturnType<this['getKeyFromPlainPrivatePKCS8Hex']>;
|
||||
|
||||
public parsePlainPrivatePKCS8Hex(pkcs8PrvHex: HexString): {
|
||||
algparam: ASN1V | null;
|
||||
algoid: ASN1V;
|
||||
keyidx: Idx<ASN1V>;
|
||||
};
|
||||
|
||||
public getKeyFromPlainPrivatePKCS8PEM(prvKeyHex: HexString): ReturnType<this['getKeyFromPlainPrivatePKCS8Hex']>;
|
||||
|
||||
public getKeyFromPlainPrivatePKCS8Hex(prvKeyHex: HexString): RSAKey | KJUR.crypto.DSA | KJUR.crypto.ECDSA;
|
||||
|
||||
private _getKeyFromPublicPKCS8Hex(h: HexString): RSAKey | KJUR.crypto.DSA | KJUR.crypto.ECDSA;
|
||||
|
||||
public parsePublicRawRSAKeyHex(pubRawRSAHex: HexString): Record<'n' | 'e', ASN1V>;
|
||||
|
||||
public parsePublicPKCS8Hex(pkcs8PubHex: HexString): {
|
||||
algparam: ASN1V | Record<'p' | 'q' | 'g', ASN1V> | null;
|
||||
algoid: ASN1V;
|
||||
key: ASN1V;
|
||||
};
|
||||
|
||||
public static getKey(param: GetKeyRSAParam): RSAKey;
|
||||
|
||||
public static getKey(param: GetKeyECDSAParam): KJUR.crypto.ECDSA;
|
||||
|
||||
public static getKey(param: GetKeyDSAParam): KJUR.crypto.DSA;
|
||||
|
||||
public static getKey(param: string, passcode?: string, hextype?: string): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
|
||||
|
||||
public static generateKeypair(alg: 'RSA', keylen: number): Record<'prvKeyObj' | 'pubKeyObj', RSAKey>;
|
||||
|
||||
public static generateKeypair(alg: 'EC', curve: KJUR.crypto.CurveName): Record<'prvKeyObj' | 'pubKeyObj', KJUR.crypto.ECDSA>;
|
||||
|
||||
public static getPEM(keyObjOrHex: RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA, formatType?: 'PKCS1PRV' | 'PKCS5PRV' | 'PKCS8PRV', passwd?: string, encAlg?: 'DES-CBC' | 'DES-EDE3-CBC' | 'AES-128-CBC' | 'AES-192-CBC' | 'AES-256-CBC', hexType?: string, ivsaltHex?: HexString): object; // To Do
|
||||
|
||||
public static getKeyFromCSRPEM(csrPEM: PEM): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
|
||||
|
||||
public static getKeyFromCSRHex(csrHex: HexString): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
|
||||
|
||||
public static parseCSRHex(csrHex: HexString): Record<'p8pubkeyhex', ASN1TLV>;
|
||||
|
||||
public static getJWKFromKey(keyObj: RSAKey): {
|
||||
kty: 'RSA';
|
||||
} & Record<'n' | 'e' | 'd' | 'p' | 'q' | 'dp' | 'dq' | 'qi', Base64URLString> | {
|
||||
kty: 'RSA';
|
||||
} & Record<'n' | 'e', Base64URLString>;
|
||||
|
||||
public static getJWKFromKey(keyObj: KJUR.crypto.ECDSA): {
|
||||
kty: 'EC';
|
||||
crv: KJUR.crypto.CurveName;
|
||||
x: Base64URLString;
|
||||
y: Base64URLString;
|
||||
d: Base64URLString;
|
||||
} | {
|
||||
kty: 'EC';
|
||||
crv: KJUR.crypto.CurveName;
|
||||
x: Base64URLString;
|
||||
y: Base64URLString;
|
||||
};
|
||||
}
|
||||
|
||||
//// KJUR NAMESPACE (PARTIAL)
|
||||
|
||||
namespace KJUR {
|
||||
namespace crypto {
|
||||
type CurveName = 'secp128r1' | 'secp160k1' | 'secp160r1' | 'secp192k1' | 'secp192r1' | 'secp224r1' | 'secp256k1' | 'secp256r1' | 'secp384r1' | 'secp521r1';
|
||||
|
||||
class DSA {
|
||||
public p: BigInteger | null;
|
||||
|
||||
public q: BigInteger | null;
|
||||
|
||||
public g: BigInteger | null;
|
||||
|
||||
public y: BigInteger | null;
|
||||
|
||||
public x: BigInteger | null;
|
||||
|
||||
public type: 'DSA';
|
||||
|
||||
public isPrivate: boolean;
|
||||
|
||||
public isPublic: boolean;
|
||||
|
||||
public setPrivate(p: BigInteger, q: BigInteger, g: BigInteger, y: BigInteger | null, x: BigInteger): void;
|
||||
|
||||
public setPrivateHex(hP: HexString, hQ: HexString, hG: HexString, hY: HexString | null, hX: HexString): void;
|
||||
|
||||
public setPublic(p: BigInteger, q: BigInteger, g: BigInteger, y: BigInteger): void;
|
||||
|
||||
public setPublicHex(hP: HexString, hQ: HexString, hG: HexString, hY: HexString): void;
|
||||
|
||||
public signWithMessageHash(sHashHex: HexString): HexString;
|
||||
|
||||
public verifyWithMessageHash(sHashHex: HexString, hSigVal: HexString): boolean;
|
||||
|
||||
public parseASN1Signature(hSigVal: HexString): [BigInteger, BigInteger];
|
||||
|
||||
public readPKCS5PrvKeyHex(h: HexString): void;
|
||||
|
||||
public readPKCS8PrvKeyHex(h: HexString): void;
|
||||
|
||||
public readPKCS8PubKeyHex(h: HexString): void;
|
||||
|
||||
public readCertPubKeyHex(h: HexString, nthPKI: number): void;
|
||||
}
|
||||
|
||||
class ECDSA {
|
||||
constructor(params?: {
|
||||
curve?: CurveName;
|
||||
prv?: HexString;
|
||||
pub?: HexString;
|
||||
});
|
||||
|
||||
public p: BigInteger | null;
|
||||
|
||||
public q: BigInteger | null;
|
||||
|
||||
public g: BigInteger | null;
|
||||
|
||||
public y: BigInteger | null;
|
||||
|
||||
public x: BigInteger | null;
|
||||
|
||||
public type: 'EC';
|
||||
|
||||
public isPrivate: boolean;
|
||||
|
||||
public isPublic: boolean;
|
||||
|
||||
public getBigRandom(limit: BigInteger): BigInteger;
|
||||
|
||||
public setNamedCurve(curveName: CurveName): void;
|
||||
|
||||
public setPrivateKeyHex(prvKeyHex: HexString): void;
|
||||
|
||||
public setPublicKeyHex(pubKeyHex: HexString): void;
|
||||
|
||||
public getPublicKeyXYHex(): Record<'x' | 'y', HexString>;
|
||||
|
||||
public getShortNISTPCurveName(): 'P-256' | 'P-384' | null;
|
||||
|
||||
public generateKeyPairHex(): Record<'ecprvhex' | 'ecpubhex', HexString>;
|
||||
|
||||
public signWithMessageHash(hashHex: HexString): HexString;
|
||||
|
||||
public signHex(hashHex: HexString, privHex: HexString): HexString;
|
||||
|
||||
public verifyWithMessageHash(sHashHex: HexString, hSigVal: HexString): boolean;
|
||||
|
||||
public parseASN1Signature(hSigVal: HexString): [BigInteger, BigInteger];
|
||||
|
||||
public readPKCS5PrvKeyHex(h: HexString): void;
|
||||
|
||||
public readPKCS8PrvKeyHex(h: HexString): void;
|
||||
|
||||
public readPKCS8PubKeyHex(h: HexString): void;
|
||||
|
||||
public readCertPubKeyHex(h: HexString, nthPKI: number): void;
|
||||
|
||||
public static parseSigHex(sigHex: HexString): Record<'r' | 's', BigInteger>;
|
||||
|
||||
public static parseSigHexInHexRS(sigHex: HexString): Record<'r' | 's', ASN1V>;
|
||||
|
||||
public static asn1SigToConcatSig(asn1Sig: HexString): HexString;
|
||||
|
||||
public static concatSigToASN1Sig(concatSig: HexString): ASN1TLV;
|
||||
|
||||
public static hexRSSigToASN1Sig(hR: HexString, hS: HexString): ASN1TLV;
|
||||
|
||||
public static biRSSigToASN1Sig(biR: BigInteger, biS: BigInteger): ASN1TLV;
|
||||
|
||||
public static getName(s: CurveName | HexString): 'secp256r1' | 'secp256k1' | 'secp384r1' | null;
|
||||
}
|
||||
|
||||
class Signature {
|
||||
constructor(params?: ({
|
||||
alg: string;
|
||||
prov?: string;
|
||||
} | {}) & ({
|
||||
psssaltlen: number;
|
||||
} | {}) & ({
|
||||
prvkeypem: PEM;
|
||||
prvkeypas?: never;
|
||||
} | {}));
|
||||
|
||||
private _setAlgNames(): void;
|
||||
|
||||
private _zeroPaddingOfSignature(hex: HexString, bitLength: number): HexString;
|
||||
|
||||
public setAlgAndProvider(alg: string, prov: string): void;
|
||||
|
||||
public init(key: GetKeyParam, pass?: string): void;
|
||||
|
||||
public updateString(str: string): void;
|
||||
|
||||
public updateHex(hex: HexString): void;
|
||||
|
||||
public sign(): HexString;
|
||||
|
||||
public signString(str: string): HexString;
|
||||
|
||||
public signHex(hex: HexString): HexString;
|
||||
|
||||
public verify(hSigVal: string): boolean | 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//// RSAKEY TYPES
|
||||
|
||||
class RSAKey {
|
||||
public n: BigInteger | null;
|
||||
|
||||
public e: number;
|
||||
|
||||
public d: BigInteger | null;
|
||||
|
||||
public p: BigInteger | null;
|
||||
|
||||
public q: BigInteger | null;
|
||||
|
||||
public dmp1: BigInteger | null;
|
||||
|
||||
public dmq1: BigInteger | null;
|
||||
|
||||
public coeff: BigInteger | null;
|
||||
|
||||
public type: 'RSA';
|
||||
|
||||
public isPrivate?: boolean;
|
||||
|
||||
public isPublic?: boolean;
|
||||
|
||||
//// RSA PUBLIC
|
||||
|
||||
protected doPublic(x: BigInteger): BigInteger;
|
||||
|
||||
public setPublic(N: BigInteger, E: number): void;
|
||||
|
||||
public setPublic(N: HexString, E: HexString): void;
|
||||
|
||||
public encrypt(text: string): HexString | null;
|
||||
|
||||
public encryptOAEP(text: string, hash?: string, hashLen?: number): HexString | null;
|
||||
|
||||
public encryptOAEP(text: string, hash?: (s: string) => string, hashLen?: number): HexString | null;
|
||||
|
||||
//// RSA PRIVATE
|
||||
|
||||
protected doPrivate(x: BigInteger): BigInteger;
|
||||
|
||||
public setPrivate(N: BigInteger, E: number, D: BigInteger): void;
|
||||
|
||||
public setPrivate(N: HexString, E: HexString, D: HexString): void;
|
||||
|
||||
public setPrivateEx(N: HexString, E: HexString, D?: HexString | null, P?: HexString | null, Q?: HexString | null, DP?: HexString | null, DQ?: HexString | null, C?: HexString | null): void;
|
||||
|
||||
public generate(B: number, E: HexString): void;
|
||||
|
||||
public decrypt(ctext: HexString): string;
|
||||
|
||||
public decryptOAEP(ctext: HexString, hash?: string, hashLen?: number): string | null;
|
||||
|
||||
public encryptOAEP(ctext: HexString, hash?: (s: string) => string, hashLen?: number): string | null;
|
||||
|
||||
//// RSA PEM
|
||||
|
||||
public getPosArrayOfChildrenFromHex(hPrivateKey: PEM): Idx<ASN1ObjectString>[];
|
||||
|
||||
public getHexValueArrayOfChildrenFromHex(hPrivateKey: PEM): Idx<ASN1ObjectString>[];
|
||||
|
||||
public readPrivateKeyFromPEMString(keyPEM: PEM): void;
|
||||
|
||||
public readPKCS5PrvKeyHex(h: HexString): void;
|
||||
|
||||
public readPKCS8PrvKeyHex(h: HexString): void;
|
||||
|
||||
public readPKCS5PubKeyHex(h: HexString): void;
|
||||
|
||||
public readPKCS8PubKeyHex(h: HexString): void;
|
||||
|
||||
public readCertPubKeyHex(h: HexString, nthPKI: Nth): void;
|
||||
|
||||
//// RSA SIGN
|
||||
|
||||
public sign(s: string, hashAlg: string): HexString;
|
||||
|
||||
public signWithMessageHash(sHashHex: HexString, hashAlg: string): HexString;
|
||||
|
||||
public signPSS(s: string, hashAlg: string, sLen: number): HexString;
|
||||
|
||||
public signWithMessageHashPSS(hHash: HexString, hashAlg: string, sLen: number): HexString;
|
||||
|
||||
public verify(sMsg: string, hSig: HexString): boolean | 0;
|
||||
|
||||
public verifyWithMessageHash(sHashHex: HexString, hSig: HexString): boolean | 0;
|
||||
|
||||
public verifyPSS(sMsg: string, hSig: HexString, hashAlg: string, sLen: number): boolean;
|
||||
|
||||
public verifyWithMessageHashPSS(hHash: HexString, hSig: HexString, hashAlg: string, sLen: number): boolean;
|
||||
|
||||
public static SALT_LEN_HLEN: -1;
|
||||
|
||||
public static SALT_LEN_MAX: -2;
|
||||
|
||||
public static SALT_LEN_RECOVER: -2;
|
||||
}
|
||||
|
||||
/// RNG TYPES
|
||||
class SecureRandom {
|
||||
public nextBytes(ba: Mutable<ByteNumber[]>): void;
|
||||
}
|
||||
|
||||
//// X509 TYPES
|
||||
|
||||
type ExtInfo = {
|
||||
critical: boolean;
|
||||
oid: OID;
|
||||
vidx: Idx<ASN1V>;
|
||||
};
|
||||
|
||||
type ExtAIAInfo = Record<'ocsp' | 'caissuer', string>;
|
||||
|
||||
type ExtCertificatePolicy = {
|
||||
id: OIDName;
|
||||
} & Partial<{
|
||||
cps: string;
|
||||
} | {
|
||||
unotice: string;
|
||||
}>;
|
||||
|
||||
class X509 {
|
||||
public hex: HexString | null;
|
||||
|
||||
public version: number;
|
||||
|
||||
public foffset: number;
|
||||
|
||||
public aExtInfo: null;
|
||||
|
||||
public getVersion(): number;
|
||||
|
||||
public getSerialNumberHex(): ASN1V;
|
||||
|
||||
public getSignatureAlgorithmField(): OIDName;
|
||||
|
||||
public getIssuerHex(): ASN1TLV;
|
||||
|
||||
public getIssuerString(): HexString;
|
||||
|
||||
public getSubjectHex(): ASN1TLV;
|
||||
|
||||
public getSubjectString(): HexString;
|
||||
|
||||
public getNotBefore(): TimeValue;
|
||||
|
||||
public getNotAfter(): TimeValue;
|
||||
|
||||
public getPublicKeyHex(): ASN1TLV;
|
||||
|
||||
public getPublicKeyIdx(): Idx<Mutable<Nth[]>>;
|
||||
|
||||
public getPublicKeyContentIdx(): Idx<Mutable<Nth[]>>;
|
||||
|
||||
public getPublicKey(): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
|
||||
|
||||
public getSignatureAlgorithmName(): OIDName;
|
||||
|
||||
public getSignatureValueHex(): ASN1V;
|
||||
|
||||
public verifySignature(pubKey: GetKeyParam): boolean | 0;
|
||||
|
||||
public parseExt(): void;
|
||||
|
||||
public getExtInfo(oidOrName: OID | string): ExtInfo | undefined;
|
||||
|
||||
public getExtBasicConstraints(): ExtInfo | {} | {
|
||||
cA: true;
|
||||
pathLen?: number;
|
||||
};
|
||||
|
||||
public getExtKeyUsageBin(): BinString;
|
||||
|
||||
public getExtKeyUsageString(): string;
|
||||
|
||||
public getExtSubjectKeyIdentifier(): ASN1V | undefined;
|
||||
|
||||
public getExtAuthorityKeyIdentifier(): {
|
||||
kid: ASN1V;
|
||||
} | undefined;
|
||||
|
||||
public getExtExtKeyUsageName(): OIDName[] | undefined;
|
||||
|
||||
public getExtSubjectAltName(): Deprecated<string[]>;
|
||||
|
||||
public getExtSubjectAltName2(): ['MAIL' | 'DNS' | 'DN' | 'URI' | 'IP', string][] | undefined;
|
||||
|
||||
public getExtCRLDistributionPointsURI(): string[] | undefined;
|
||||
|
||||
public getExtAIAInfo(): ExtAIAInfo | undefined;
|
||||
|
||||
public getExtCertificatePolicies(): ExtCertificatePolicy[] | undefined;
|
||||
|
||||
public readCertPEM(sCertPEM: PEM): void;
|
||||
|
||||
public readCertHex(sCertHex: HexString): void;
|
||||
|
||||
public getInfo(): string;
|
||||
|
||||
public static hex2dn(hex: HexString, idx?: Idx<HexString>): string;
|
||||
|
||||
public static hex2rdn(hex: HexString, idx?: Idx<HexString>): string;
|
||||
|
||||
public static hex2attrTypeValue(hex: HexString, idx?: Idx<HexString>): string;
|
||||
|
||||
public static getPublicKeyFromCertPEM(sCertPEM: PEM): RSAKey | KJUR.crypto.ECDSA | KJUR.crypto.DSA;
|
||||
|
||||
public static getPublicKeyInfoPropOfCertPEM(sCertPEM: PEM): {
|
||||
algparam: ASN1V | null;
|
||||
leyhex: ASN1V;
|
||||
algoid: ASN1V;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -79,6 +79,7 @@ export async function masterMain() {
|
|||
require('../daemons/server-stats').default();
|
||||
require('../daemons/notes-stats').default();
|
||||
require('../daemons/queue-stats').default();
|
||||
require('../daemons/janitor').default();
|
||||
}
|
||||
|
||||
bootLogger.succ(`Now listening on port ${config.port} on ${config.url}`, null, true);
|
||||
|
|
|
@ -5,6 +5,14 @@
|
|||
<section v-if="tables">
|
||||
<div v-for="table in Object.keys(tables)"><b>{{ table }}</b> {{ tables[table].count | number }} {{ tables[table].size | bytes }}</div>
|
||||
</section>
|
||||
<section>
|
||||
<header><fa :icon="faBroom"/> {{ $t('vacuum') }}</header>
|
||||
<ui-info>{{ $t('vacuum-info') }}</ui-info>
|
||||
<ui-switch v-model="fullVacuum">FULL</ui-switch>
|
||||
<ui-switch v-model="analyzeVacuum">ANALYZE</ui-switch>
|
||||
<ui-button @click="vacuum()"><fa :icon="faBroom"/> {{ $t('vacuum') }}</ui-button>
|
||||
<ui-info warn>{{ $t('vacuum-exclamation') }}</ui-info>
|
||||
</section>
|
||||
</ui-card>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -12,7 +20,7 @@
|
|||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../i18n';
|
||||
import { faDatabase } from '@fortawesome/free-solid-svg-icons';
|
||||
import { faDatabase, faBroom } from '@fortawesome/free-solid-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('admin/views/db.vue'),
|
||||
|
@ -20,7 +28,9 @@ export default Vue.extend({
|
|||
data() {
|
||||
return {
|
||||
tables: null,
|
||||
faDatabase
|
||||
fullVacuum: true,
|
||||
analyzeVacuum: true,
|
||||
faDatabase, faBroom
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -34,6 +44,18 @@ export default Vue.extend({
|
|||
this.tables = tables;
|
||||
});
|
||||
},
|
||||
|
||||
vacuum() {
|
||||
this.$root.api('admin/vacuum', {
|
||||
full: this.fullVacuum,
|
||||
analyze: this.analyzeVacuum,
|
||||
}).then(() => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
splash: true
|
||||
});
|
||||
});
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -129,6 +129,7 @@
|
|||
<ui-input v-model="smtpPass" type="password" :with-password-toggle="true" :disabled="!enableEmail || !smtpAuth">{{ $t('smtp-pass') }}</ui-input>
|
||||
</ui-horizon-group>
|
||||
<ui-switch v-model="smtpSecure" :disabled="!enableEmail">{{ $t('smtp-secure') }}<template #desc>{{ $t('smtp-secure-info') }}</template></ui-switch>
|
||||
<ui-button @click="testEmail()">{{ $t('test-email') }}</ui-button>
|
||||
</template>
|
||||
</section>
|
||||
<section>
|
||||
|
@ -424,6 +425,24 @@ export default Vue.extend({
|
|||
});
|
||||
},
|
||||
|
||||
async testEmail() {
|
||||
this.$root.api('admin/send-email', {
|
||||
to: this.maintainerEmail,
|
||||
subject: 'Test email',
|
||||
text: 'Yo'
|
||||
}).then(x => {
|
||||
this.$root.dialog({
|
||||
type: 'success',
|
||||
splash: true
|
||||
});
|
||||
}).catch(e => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: e
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
updateMeta() {
|
||||
this.$root.api('admin/update-meta', {
|
||||
maintainerName: this.maintainerName,
|
||||
|
|
|
@ -41,7 +41,6 @@
|
|||
if (`${url.pathname}/`.startsWith('/dev/')) app = 'dev';
|
||||
if (`${url.pathname}/`.startsWith('/auth/')) app = 'auth';
|
||||
if (`${url.pathname}/`.startsWith('/admin/')) app = 'admin';
|
||||
if (`${url.pathname}/`.startsWith('/test/')) app = 'test';
|
||||
//#endregion
|
||||
|
||||
// Script version
|
||||
|
|
5
src/client/app/common/scripts/2fa.ts
Normal file
5
src/client/app/common/scripts/2fa.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export function hexifyAB(buffer) {
|
||||
return Array.from(new Uint8Array(buffer))
|
||||
.map(item => item.toString(16).padStart(2, 0))
|
||||
.join('');
|
||||
}
|
|
@ -33,7 +33,7 @@
|
|||
</template>
|
||||
</ui-select>
|
||||
<ui-horizon-group no-grow class="buttons fit-bottom" v-if="!splash && (showOkButton || showCancelButton)">
|
||||
<ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
|
||||
<ui-button @click="ok" v-if="showOkButton" primary :autofocus="!input && !select && !user" :disabled="!canOk">{{ (showCancelButton || input || select || user) ? $t('@.ok') : $t('@.got-it') }}</ui-button>
|
||||
<ui-button @click="cancel" v-if="showCancelButton || input || select || user">{{ $t('@.cancel') }}</ui-button>
|
||||
</ui-horizon-group>
|
||||
</template>
|
||||
|
@ -99,11 +99,26 @@ export default Vue.extend({
|
|||
inputValue: this.input && this.input.default ? this.input.default : null,
|
||||
userInputValue: null,
|
||||
selectedValue: this.select ? this.select.items ? this.select.items[0].value : this.select.groupedItems[0].items[0].value : null,
|
||||
canOk: true,
|
||||
faTimesCircle, faQuestionCircle
|
||||
};
|
||||
},
|
||||
|
||||
watch: {
|
||||
userInputValue() {
|
||||
if (this.user) {
|
||||
this.$root.api('users/show', parseAcct(this.userInputValue)).then(u => {
|
||||
this.canOk = u != null;
|
||||
}).catch(() => {
|
||||
this.canOk = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
if (this.user) this.canOk = false;
|
||||
|
||||
this.$nextTick(() => {
|
||||
(this.$refs.bg as any).style.pointerEvents = 'auto';
|
||||
anime({
|
||||
|
@ -131,6 +146,7 @@ export default Vue.extend({
|
|||
|
||||
methods: {
|
||||
async ok() {
|
||||
if (!this.canOk) return;
|
||||
if (!this.showOkButton) return;
|
||||
|
||||
if (this.user) {
|
||||
|
|
|
@ -92,6 +92,14 @@ export default Vue.extend({
|
|||
|
||||
try {
|
||||
if (this.isFollowing) {
|
||||
const { canceled } = await this.$root.dialog({
|
||||
type: 'warning',
|
||||
text: this.$t('@.unfollow-confirm', { name: this.user.name || this.user.username }),
|
||||
showCancelButton: true
|
||||
});
|
||||
|
||||
if (canceled) return;
|
||||
|
||||
await this.$root.api('following/delete', {
|
||||
userId: this.user.id
|
||||
});
|
||||
|
|
|
@ -15,6 +15,8 @@ export default Vue.extend({
|
|||
|
||||
<style lang="stylus" scoped>
|
||||
.havbbuyv
|
||||
white-space pre-wrap
|
||||
|
||||
>>> .title
|
||||
display block
|
||||
margin-bottom 4px
|
||||
|
|
|
@ -9,7 +9,6 @@ import Vue from 'vue';
|
|||
import i18n from '../../../i18n';
|
||||
import { url } from '../../../config';
|
||||
import copyToClipboard from '../../../common/scripts/copy-to-clipboard';
|
||||
import { concat, intersperse } from '../../../../../prelude/array';
|
||||
import { faCopy, faEye, faEyeSlash } from '@fortawesome/free-regular-svg-icons';
|
||||
|
||||
export default Vue.extend({
|
||||
|
@ -129,6 +128,13 @@ export default Vue.extend({
|
|||
splash: true
|
||||
});
|
||||
this.destroyDom();
|
||||
}).catch(e => {
|
||||
if (e.id === '72dab508-c64d-498f-8740-a8eec1ba385a') {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('pin-limit-exceeded')
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
|
|
@ -1,11 +1,54 @@
|
|||
<template>
|
||||
<div class="2fa">
|
||||
<div class="2fa totp-section">
|
||||
<p style="margin-top:0;">{{ $t('intro') }}<a :href="$t('url')" target="_blank">{{ $t('detail') }}</a></p>
|
||||
<ui-info warn>{{ $t('caution') }}</ui-info>
|
||||
<p v-if="!data && !$store.state.i.twoFactorEnabled"><ui-button @click="register">{{ $t('register') }}</ui-button></p>
|
||||
<template v-if="$store.state.i.twoFactorEnabled">
|
||||
<h2 class="heading">{{ $t('totp-header') }}</h2>
|
||||
<p>{{ $t('already-registered') }}</p>
|
||||
<ui-button @click="unregister">{{ $t('unregister') }}</ui-button>
|
||||
|
||||
<template v-if="supportsCredentials">
|
||||
<hr class="totp-method-sep">
|
||||
|
||||
<h2 class="heading">{{ $t('security-key-header') }}</h2>
|
||||
<p>{{ $t('security-key') }}</p>
|
||||
<div class="key-list">
|
||||
<div class="key" v-for="key in $store.state.i.securityKeysList">
|
||||
<h3>
|
||||
{{ key.name }}
|
||||
</h3>
|
||||
<div class="last-used">
|
||||
{{ $t('last-used') }}
|
||||
<mk-time :time="key.lastUsed"/>
|
||||
</div>
|
||||
<ui-button @click="unregisterKey(key)">
|
||||
{{ $t('unregister') }}
|
||||
</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ui-info warn v-if="registration && registration.error">{{ $t('something-went-wrong') }} {{ registration.error }}</ui-info>
|
||||
<ui-button v-if="!registration || registration.error" @click="addSecurityKey">{{ $t('register') }}</ui-button>
|
||||
|
||||
<ol v-if="registration && !registration.error">
|
||||
<li v-if="registration.stage >= 0">
|
||||
{{ $t('activate-key') }}
|
||||
<fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 0" />
|
||||
</li>
|
||||
<li v-if="registration.stage >= 1">
|
||||
<ui-form :disabled="registration.stage != 1 || registration.saving">
|
||||
<ui-input v-model="keyName" :max="30">
|
||||
<span>{{ $t('security-key-name') }}</span>
|
||||
</ui-input>
|
||||
<ui-button @click="registerKey" :disabled="this.keyName.length == 0">
|
||||
{{ $t('register-security-key') }}
|
||||
</ui-button>
|
||||
<fa icon="spinner" pulse fixed-width v-if="registration.saving && registration.stage == 1" />
|
||||
</ui-form>
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
</template>
|
||||
<div v-if="data && !$store.state.i.twoFactorEnabled">
|
||||
<ol>
|
||||
|
@ -24,12 +67,21 @@
|
|||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import i18n from '../../../../i18n';
|
||||
import { hostname } from '../../../../config';
|
||||
import { hexifyAB } from '../../../scripts/2fa';
|
||||
|
||||
function stringifyAB(buffer) {
|
||||
return String.fromCharCode.apply(null, new Uint8Array(buffer));
|
||||
}
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('desktop/views/components/settings.2fa.vue'),
|
||||
data() {
|
||||
return {
|
||||
data: null,
|
||||
supportsCredentials: !!navigator.credentials,
|
||||
registration: null,
|
||||
keyName: '',
|
||||
token: null
|
||||
};
|
||||
},
|
||||
|
@ -76,7 +128,116 @@ export default Vue.extend({
|
|||
}).catch(() => {
|
||||
this.$notify(this.$t('failed'));
|
||||
});
|
||||
},
|
||||
|
||||
registerKey() {
|
||||
this.registration.saving = true;
|
||||
this.$root.api('i/2fa/key-done', {
|
||||
password: this.registration.password,
|
||||
name: this.keyName,
|
||||
challengeId: this.registration.challengeId,
|
||||
// we convert each 16 bits to a string to serialise
|
||||
clientDataJSON: stringifyAB(this.registration.credential.response.clientDataJSON),
|
||||
attestationObject: hexifyAB(this.registration.credential.response.attestationObject)
|
||||
}).then(key => {
|
||||
this.registration = null;
|
||||
key.lastUsed = new Date();
|
||||
this.$notify(this.$t('success'));
|
||||
})
|
||||
},
|
||||
|
||||
unregisterKey(key) {
|
||||
this.$root.dialog({
|
||||
title: this.$t('enter-password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
return this.$root.api('i/2fa/remove-key', {
|
||||
password,
|
||||
credentialId: key.id
|
||||
}).then(() => {
|
||||
this.$notify(this.$t('key-unregistered'));
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
addSecurityKey() {
|
||||
this.$root.dialog({
|
||||
title: this.$t('enter-password'),
|
||||
input: {
|
||||
type: 'password'
|
||||
}
|
||||
}).then(({ canceled, result: password }) => {
|
||||
if (canceled) return;
|
||||
this.$root.api('i/2fa/register-key', {
|
||||
password
|
||||
}).then(registration => {
|
||||
this.registration = {
|
||||
password,
|
||||
challengeId: registration.challengeId,
|
||||
stage: 0,
|
||||
publicKeyOptions: {
|
||||
challenge: Buffer.from(
|
||||
registration.challenge
|
||||
.replace(/\-/g, "+")
|
||||
.replace(/_/g, "/"),
|
||||
'base64'
|
||||
),
|
||||
rp: {
|
||||
id: hostname,
|
||||
name: 'Misskey'
|
||||
},
|
||||
user: {
|
||||
id: Uint8Array.from(this.$store.state.i.id, c => c.charCodeAt(0)),
|
||||
name: this.$store.state.i.username,
|
||||
displayName: this.$store.state.i.name,
|
||||
},
|
||||
pubKeyCredParams: [{alg: -7, type: 'public-key'}],
|
||||
timeout: 60000,
|
||||
attestation: 'direct'
|
||||
},
|
||||
saving: true
|
||||
};
|
||||
return navigator.credentials.create({
|
||||
publicKey: this.registration.publicKeyOptions
|
||||
});
|
||||
}).then(credential => {
|
||||
this.registration.credential = credential;
|
||||
this.registration.saving = false;
|
||||
this.registration.stage = 1;
|
||||
}).catch(err => {
|
||||
console.warn('Error while registering?', err);
|
||||
this.registration.error = err.message;
|
||||
this.registration.stage = -1;
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
.totp-section
|
||||
.totp-method-sep
|
||||
margin 1.5em 0 1em
|
||||
border none
|
||||
border-top solid var(--lineWidth) var(--faceDivider)
|
||||
|
||||
h2.heading
|
||||
margin 0
|
||||
|
||||
.key
|
||||
padding 1em
|
||||
margin 0.5em 0
|
||||
background #161616
|
||||
border-radius 6px
|
||||
|
||||
h3
|
||||
margin-top 0
|
||||
margin-bottom .3em
|
||||
|
||||
.last-used
|
||||
margin-bottom .5em
|
||||
</style>
|
||||
|
|
|
@ -1,23 +1,40 @@
|
|||
<template>
|
||||
<form class="mk-signin" :class="{ signing }" @submit.prevent="onSubmit">
|
||||
<form class="mk-signin" :class="{ signing, totpLogin }" @submit.prevent="onSubmit">
|
||||
<div class="avatar" :style="{ backgroundImage: user ? `url('${ user.avatarUrl }')` : null }" v-show="withAvatar"></div>
|
||||
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange">
|
||||
<span>{{ $t('username') }}</span>
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
</ui-input>
|
||||
<ui-input v-model="password" type="password" :with-password-toggle="true" required>
|
||||
<span>{{ $t('password') }}</span>
|
||||
<template #prefix><fa icon="lock"/></template>
|
||||
</ui-input>
|
||||
<ui-input v-if="user && user.twoFactorEnabled" v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
|
||||
<span>{{ $t('@.2fa') }}</span>
|
||||
<template #prefix><fa icon="gavel"/></template>
|
||||
</ui-input>
|
||||
<ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('@.signin') }}</ui-button>
|
||||
<p v-if="meta && meta.enableTwitterIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/twitter`"><fa :icon="['fab', 'twitter']"/> {{ $t('signin-with-twitter') }}</a></p>
|
||||
<p v-if="meta && meta.enableGithubIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/github`"><fa :icon="['fab', 'github']"/> {{ $t('signin-with-github') }}</a></p>
|
||||
<p v-if="meta && meta.enableDiscordIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/discord`"><fa :icon="['fab', 'discord']"/> {{ $t('signin-with-discord') /* TODO: Make these layouts better */ }}</a></p>
|
||||
<div class="normal-signin" v-if="!totpLogin">
|
||||
<ui-input v-model="username" type="text" pattern="^[a-zA-Z0-9_]+$" spellcheck="false" autofocus required @input="onUsernameChange">
|
||||
<span>{{ $t('username') }}</span>
|
||||
<template #prefix>@</template>
|
||||
<template #suffix>@{{ host }}</template>
|
||||
</ui-input>
|
||||
<ui-input v-model="password" type="password" :with-password-toggle="true" required>
|
||||
<span>{{ $t('password') }}</span>
|
||||
<template #prefix><fa icon="lock"/></template>
|
||||
</ui-input>
|
||||
<ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('@.signin') }}</ui-button>
|
||||
<p v-if="meta && meta.enableTwitterIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/twitter`"><fa :icon="['fab', 'twitter']"/> {{ $t('signin-with-twitter') }}</a></p>
|
||||
<p v-if="meta && meta.enableGithubIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/github`"><fa :icon="['fab', 'github']"/> {{ $t('signin-with-github') }}</a></p>
|
||||
<p v-if="meta && meta.enableDiscordIntegration" style="margin: 8px 0;"><a :href="`${apiUrl}/signin/discord`"><fa :icon="['fab', 'discord']"/> {{ $t('signin-with-discord') /* TODO: Make these layouts better */ }}</a></p>
|
||||
</div>
|
||||
<div class="2fa-signin" v-if="totpLogin" :class="{ securityKeys: user && user.securityKeys }">
|
||||
<div v-if="user && user.securityKeys" class="twofa-group tap-group">
|
||||
<p>{{ $t('tap-key') }}</p>
|
||||
<ui-button @click="queryKey" v-if="!queryingKey">
|
||||
{{ $t('@.error.retry') }}
|
||||
</ui-button>
|
||||
</div>
|
||||
<div class="or-hr" v-if="user && user.securityKeys">
|
||||
<p class="or-msg">{{ $t('or') }}</p>
|
||||
</div>
|
||||
<div class="twofa-group totp-group">
|
||||
<p style="margin-bottom:0;">{{ $t('enter-2fa-code') }}</p>
|
||||
<ui-input v-model="token" type="text" pattern="^[0-9]{6}$" autocomplete="off" spellcheck="false" required>
|
||||
<span>{{ $t('@.2fa') }}</span>
|
||||
<template #prefix><fa icon="gavel"/></template>
|
||||
</ui-input>
|
||||
<ui-button type="submit" :disabled="signing">{{ signing ? $t('signing-in') : $t('@.signin') }}</ui-button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
|
@ -26,6 +43,7 @@ import Vue from 'vue';
|
|||
import i18n from '../../../i18n';
|
||||
import { apiUrl, host } from '../../../config';
|
||||
import { toUnicode } from 'punycode';
|
||||
import { hexifyAB } from '../../scripts/2fa';
|
||||
|
||||
export default Vue.extend({
|
||||
i18n: i18n('common/views/components/signin.vue'),
|
||||
|
@ -47,7 +65,11 @@ export default Vue.extend({
|
|||
token: '',
|
||||
apiUrl,
|
||||
host: toUnicode(host),
|
||||
meta: null
|
||||
meta: null,
|
||||
totpLogin: false,
|
||||
credential: null,
|
||||
challengeData: null,
|
||||
queryingKey: false,
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -68,23 +90,87 @@ export default Vue.extend({
|
|||
});
|
||||
},
|
||||
|
||||
onSubmit() {
|
||||
this.signing = true;
|
||||
|
||||
this.$root.api('signin', {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
token: this.user && this.user.twoFactorEnabled ? this.token : undefined
|
||||
queryKey() {
|
||||
this.queryingKey = true;
|
||||
return navigator.credentials.get({
|
||||
publicKey: {
|
||||
challenge: Buffer.from(
|
||||
this.challengeData.challenge
|
||||
.replace(/\-/g, '+')
|
||||
.replace(/_/g, '/'),
|
||||
'base64'
|
||||
),
|
||||
allowCredentials: this.challengeData.securityKeys.map(key => ({
|
||||
id: Buffer.from(key.id, 'hex'),
|
||||
type: 'public-key',
|
||||
transports: ['usb', 'ble', 'nfc']
|
||||
})),
|
||||
timeout: 60 * 1000
|
||||
}
|
||||
}).catch(err => {
|
||||
this.queryingKey = false;
|
||||
console.warn(err);
|
||||
return Promise.reject(null);
|
||||
}).then(credential => {
|
||||
this.queryingKey = false;
|
||||
this.signing = true;
|
||||
return this.$root.api('signin', {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
signature: hexifyAB(credential.response.signature),
|
||||
authenticatorData: hexifyAB(credential.response.authenticatorData),
|
||||
clientDataJSON: hexifyAB(credential.response.clientDataJSON),
|
||||
credentialId: credential.id,
|
||||
challengeId: this.challengeData.challengeId
|
||||
});
|
||||
}).then(res => {
|
||||
localStorage.setItem('i', res.i);
|
||||
location.reload();
|
||||
}).catch(() => {
|
||||
}).catch(err => {
|
||||
if(err === null) return;
|
||||
console.error(err);
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('login-failed')
|
||||
});
|
||||
this.signing = false;
|
||||
});
|
||||
},
|
||||
|
||||
onSubmit() {
|
||||
this.signing = true;
|
||||
|
||||
if (!this.totpLogin && this.user && this.user.twoFactorEnabled) {
|
||||
if (window.PublicKeyCredential && this.user.securityKeys) {
|
||||
this.$root.api('i/2fa/getkeys', {
|
||||
username: this.username,
|
||||
password: this.password
|
||||
}).then(res => {
|
||||
this.totpLogin = true;
|
||||
this.signing = false;
|
||||
this.challengeData = res;
|
||||
return this.queryKey();
|
||||
});
|
||||
} else {
|
||||
this.totpLogin = true;
|
||||
this.signing = false;
|
||||
}
|
||||
} else {
|
||||
this.$root.api('signin', {
|
||||
username: this.username,
|
||||
password: this.password,
|
||||
token: this.user && this.user.twoFactorEnabled ? this.token : undefined
|
||||
}).then(res => {
|
||||
localStorage.setItem('i', res.i);
|
||||
location.reload();
|
||||
}).catch(() => {
|
||||
this.$root.dialog({
|
||||
type: 'error',
|
||||
text: this.$t('login-failed')
|
||||
});
|
||||
this.signing = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -94,6 +180,48 @@ export default Vue.extend({
|
|||
.mk-signin
|
||||
color #555
|
||||
|
||||
.or-hr,
|
||||
.or-hr .or-msg,
|
||||
.twofa-group,
|
||||
.twofa-group p
|
||||
color var(--text)
|
||||
|
||||
.tap-group > button
|
||||
margin-bottom 1em
|
||||
|
||||
.securityKeys .or-hr
|
||||
&
|
||||
position relative
|
||||
|
||||
.or-msg
|
||||
&:before
|
||||
right 100%
|
||||
margin-right 0.125em
|
||||
|
||||
&:after
|
||||
left 100%
|
||||
margin-left 0.125em
|
||||
|
||||
&:before, &:after
|
||||
content ""
|
||||
position absolute
|
||||
top 50%
|
||||
width 100%
|
||||
height 2px
|
||||
background #555
|
||||
|
||||
&
|
||||
position relative
|
||||
margin auto
|
||||
left 0
|
||||
right 0
|
||||
top 0
|
||||
bottom 0
|
||||
font-size 1.5em
|
||||
height 1.5em
|
||||
width 3em
|
||||
text-align center
|
||||
|
||||
&.signing
|
||||
&, *
|
||||
cursor wait !important
|
||||
|
|
|
@ -5,6 +5,9 @@
|
|||
<span class="hostname">{{ hostname }}</span>
|
||||
<span class="port" v-if="port != ''">:{{ port }}</span>
|
||||
</template>
|
||||
<template v-if="pathname === '/' && self">
|
||||
<span class="self">{{ hostname }}</span>
|
||||
</template>
|
||||
<span class="pathname" v-if="pathname != ''">{{ self ? pathname.substr(1) : pathname }}</span>
|
||||
<span class="query">{{ query }}</span>
|
||||
<span class="hash">{{ hash }}</span>
|
||||
|
@ -22,6 +25,7 @@ export default Vue.extend({
|
|||
data() {
|
||||
const isSelf = this.url.startsWith(local);
|
||||
const hasRoute = isSelf && (
|
||||
(this.url.substr(local.length) === '/') ||
|
||||
this.url.substr(local.length).startsWith('/@') ||
|
||||
this.url.substr(local.length).startsWith('/notes/') ||
|
||||
this.url.substr(local.length).startsWith('/pages/'));
|
||||
|
@ -54,19 +58,28 @@ export default Vue.extend({
|
|||
<style lang="stylus" scoped>
|
||||
.mk-url
|
||||
word-break break-all
|
||||
|
||||
> [data-icon]
|
||||
padding-left 2px
|
||||
font-size .9em
|
||||
font-weight 400
|
||||
font-style normal
|
||||
|
||||
> .self
|
||||
font-weight bold
|
||||
|
||||
> .schema
|
||||
opacity 0.5
|
||||
|
||||
> .hostname
|
||||
font-weight bold
|
||||
|
||||
> .pathname
|
||||
opacity 0.8
|
||||
|
||||
> .query
|
||||
opacity 0.5
|
||||
|
||||
> .hash
|
||||
font-style italic
|
||||
</style>
|
||||
|
|
|
@ -97,7 +97,9 @@ export default Vue.extend({
|
|||
const image = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif'
|
||||
'image/gif',
|
||||
'image/apng',
|
||||
'image/vnd.mozilla.apng',
|
||||
];
|
||||
|
||||
this.$root.api('users/notes', {
|
||||
|
|
|
@ -91,6 +91,7 @@ export default ($root: any) => {
|
|||
? Promise.resolve(file)
|
||||
: $root.$chooseDriveFile({
|
||||
multiple: false,
|
||||
type: 'image/*',
|
||||
title: locale['desktop']['choose-avatar']
|
||||
});
|
||||
|
||||
|
|
|
@ -91,6 +91,7 @@ export default ($root: any) => {
|
|||
? Promise.resolve(file)
|
||||
: $root.$chooseDriveFile({
|
||||
multiple: false,
|
||||
type: 'image/*',
|
||||
title: locale['desktop']['choose-banner']
|
||||
});
|
||||
|
||||
|
|
|
@ -77,6 +77,7 @@ init(async (launch, os) => {
|
|||
if (document.body.clientWidth > 800) {
|
||||
const w = this.$root.new(MkChooseFileFromDriveWindow, {
|
||||
title: o.title,
|
||||
type: o.type,
|
||||
multiple: o.multiple,
|
||||
initFolder: o.currentFolder
|
||||
});
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<x-drive
|
||||
ref="browser"
|
||||
class="browser"
|
||||
:type="type"
|
||||
:multiple="multiple"
|
||||
@selected="onSelected"
|
||||
@change-selection="onChangeSelection"
|
||||
|
@ -33,6 +34,11 @@ export default Vue.extend({
|
|||
XDrive: () => import('./drive.vue').then(m => m.default),
|
||||
},
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
multiple: {
|
||||
default: false
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
import Vue from 'vue';
|
||||
import i18n from '../../../i18n';
|
||||
import VueCropper from 'vue-cropperjs';
|
||||
import 'cropperjs/dist/cropper.css';
|
||||
import * as url from '../../../../../prelude/url';
|
||||
|
||||
export default Vue.extend({
|
||||
|
|
|
@ -80,6 +80,11 @@ export default Vue.extend({
|
|||
type: Object,
|
||||
required: false
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: undefined
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false
|
||||
|
@ -540,6 +545,7 @@ export default Vue.extend({
|
|||
// ファイル一覧取得
|
||||
this.$root.api('drive/files', {
|
||||
folderId: this.folder ? this.folder.id : null,
|
||||
type: this.type,
|
||||
limit: filesMax + 1
|
||||
}).then(files => {
|
||||
if (files.length == filesMax + 1) {
|
||||
|
@ -570,6 +576,7 @@ export default Vue.extend({
|
|||
// ファイル一覧取得
|
||||
this.$root.api('drive/files', {
|
||||
folderId: this.folder ? this.folder.id : null,
|
||||
type: this.type,
|
||||
untilId: this.files[this.files.length - 1].id,
|
||||
limit: max + 1
|
||||
}).then(files => {
|
||||
|
|
|
@ -38,7 +38,9 @@ export default Vue.extend({
|
|||
const image = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif'
|
||||
'image/gif',
|
||||
'image/apng',
|
||||
'image/vnd.mozilla.apng',
|
||||
];
|
||||
|
||||
this.$root.api('users/notes', {
|
||||
|
|
|
@ -186,7 +186,9 @@ export default Vue.extend({
|
|||
const image = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif'
|
||||
'image/gif',
|
||||
'image/apng',
|
||||
'image/vnd.mozilla.apng',
|
||||
];
|
||||
|
||||
this.$root.api('notes/local-timeline', {
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
</header>
|
||||
<x-drive class="drive" ref="browser"
|
||||
:select-file="true"
|
||||
:type="type"
|
||||
:multiple="multiple"
|
||||
@change-selection="onChangeSelection"
|
||||
@selected="onSelected"
|
||||
|
@ -25,7 +26,7 @@ export default Vue.extend({
|
|||
components: {
|
||||
XDrive: () => import('./drive.vue').then(m => m.default),
|
||||
},
|
||||
props: ['multiple'],
|
||||
props: ['type', 'multiple'],
|
||||
data() {
|
||||
return {
|
||||
files: []
|
||||
|
|
|
@ -30,7 +30,9 @@ export default Vue.extend({
|
|||
const image = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif'
|
||||
'image/gif',
|
||||
'image/apng',
|
||||
'image/vnd.mozilla.apng',
|
||||
];
|
||||
this.$root.api('users/notes', {
|
||||
userId: this.user.id,
|
||||
|
|
|
@ -110,7 +110,9 @@ export default Vue.extend({
|
|||
const image = [
|
||||
'image/jpeg',
|
||||
'image/png',
|
||||
'image/gif'
|
||||
'image/gif',
|
||||
'image/apng',
|
||||
'image/vnd.mozilla.apng',
|
||||
];
|
||||
|
||||
this.$root.api('notes/local-timeline', {
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
import VueRouter from 'vue-router';
|
||||
|
||||
// Style
|
||||
import './style.styl';
|
||||
|
||||
import init from '../init';
|
||||
import Index from './views/index.vue';
|
||||
import NotFound from '../common/views/pages/not-found.vue';
|
||||
|
||||
init(launch => {
|
||||
document.title = 'Misskey';
|
||||
|
||||
// Init router
|
||||
const router = new VueRouter({
|
||||
mode: 'history',
|
||||
base: '/test/',
|
||||
routes: [
|
||||
{ path: '/', component: Index },
|
||||
{ path: '*', component: NotFound }
|
||||
]
|
||||
});
|
||||
|
||||
// Launch the app
|
||||
launch(router);
|
||||
});
|
|
@ -1,6 +0,0 @@
|
|||
@import "../app"
|
||||
@import "../reset"
|
||||
|
||||
html
|
||||
height 100%
|
||||
background var(--bg)
|
|
@ -1,82 +0,0 @@
|
|||
<template>
|
||||
<main>
|
||||
<ui-card>
|
||||
<template #title>MFM Playground</template>
|
||||
<section class="fit-top">
|
||||
<ui-textarea v-model="mfm">
|
||||
<span>MFM</span>
|
||||
</ui-textarea>
|
||||
</section>
|
||||
<section>
|
||||
<header>Preview</header>
|
||||
<mfm :text="mfm" :i="$store.state.i"/>
|
||||
</section>
|
||||
<section>
|
||||
<header style="margin-bottom:0;">AST</header>
|
||||
<ui-textarea v-model="mfmAst" readonly tall style="margin-top:16px;"></ui-textarea>
|
||||
</section>
|
||||
</ui-card>
|
||||
|
||||
<ui-card>
|
||||
<template #title>Dialog Generator</template>
|
||||
<section class="fit-top">
|
||||
<ui-select v-model="dialogType" placeholder="">
|
||||
<option value="info">Information</option>
|
||||
<option value="success">Success</option>
|
||||
<option value="warning">Warning</option>
|
||||
<option value="error">Error</option>
|
||||
</ui-select>
|
||||
<ui-input v-model="dialogTitle">
|
||||
<span>Title</span>
|
||||
</ui-input>
|
||||
<ui-input v-model="dialogText">
|
||||
<span>Text</span>
|
||||
</ui-input>
|
||||
<ui-switch v-model="dialogShowCancelButton">With cancel button</ui-switch>
|
||||
<ui-button @click="showDialog">Show</ui-button>
|
||||
</section>
|
||||
</ui-card>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import { parse } from '../../../../mfm/parse';
|
||||
import * as JSON5 from 'json5';
|
||||
|
||||
export default Vue.extend({
|
||||
data() {
|
||||
return {
|
||||
mfm: '',
|
||||
dialogType: 'success',
|
||||
dialogTitle: '',
|
||||
dialogText: 'Hello World!',
|
||||
dialogShowCancelButton: false
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
mfmAst(): any {
|
||||
return JSON5.stringify(parse(this.mfm), null, 2);
|
||||
}
|
||||
},
|
||||
|
||||
methods: {
|
||||
showDialog() {
|
||||
this.$root.dialog({
|
||||
type: this.dialogType,
|
||||
title: this.dialogTitle,
|
||||
text: this.dialogText,
|
||||
showCancelButton: this.dialogShowCancelButton
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="stylus" scoped>
|
||||
main
|
||||
max-width 700px
|
||||
margin 0 auto
|
||||
|
||||
</style>
|
18
src/daemons/janitor.ts
Normal file
18
src/daemons/janitor.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
const interval = 30 * 60 * 1000;
|
||||
import { AttestationChallenges } from '../models';
|
||||
import { LessThan } from 'typeorm';
|
||||
|
||||
/**
|
||||
* Clean up database occasionally
|
||||
*/
|
||||
export default function() {
|
||||
async function tick() {
|
||||
await AttestationChallenges.delete({
|
||||
createdAt: LessThan(new Date(new Date().getTime() - 5 * 60 * 1000))
|
||||
});
|
||||
}
|
||||
|
||||
tick();
|
||||
|
||||
setInterval(tick, interval);
|
||||
}
|
|
@ -43,6 +43,8 @@ import { Poll } from '../models/entities/poll';
|
|||
import { UserKeypair } from '../models/entities/user-keypair';
|
||||
import { UserPublickey } from '../models/entities/user-publickey';
|
||||
import { UserProfile } from '../models/entities/user-profile';
|
||||
import { UserSecurityKey } from '../models/entities/user-security-key';
|
||||
import { AttestationChallenge } from '../models/entities/attestation-challenge';
|
||||
import { Page } from '../models/entities/page';
|
||||
import { PageLike } from '../models/entities/page-like';
|
||||
|
||||
|
@ -80,6 +82,53 @@ class MyCustomLogger implements Logger {
|
|||
}
|
||||
}
|
||||
|
||||
export const entities = [
|
||||
Meta,
|
||||
Instance,
|
||||
App,
|
||||
AuthSession,
|
||||
AccessToken,
|
||||
User,
|
||||
UserProfile,
|
||||
UserKeypair,
|
||||
UserPublickey,
|
||||
UserList,
|
||||
UserListJoining,
|
||||
UserGroup,
|
||||
UserGroupJoining,
|
||||
UserGroupInvite,
|
||||
UserNotePining,
|
||||
UserSecurityKey,
|
||||
AttestationChallenge,
|
||||
Following,
|
||||
FollowRequest,
|
||||
Muting,
|
||||
Blocking,
|
||||
Note,
|
||||
NoteFavorite,
|
||||
NoteReaction,
|
||||
NoteWatching,
|
||||
NoteUnread,
|
||||
Page,
|
||||
PageLike,
|
||||
Log,
|
||||
DriveFile,
|
||||
DriveFolder,
|
||||
Poll,
|
||||
PollVote,
|
||||
Notification,
|
||||
Emoji,
|
||||
Hashtag,
|
||||
SwSubscription,
|
||||
AbuseUserReport,
|
||||
RegistrationTicket,
|
||||
MessagingMessage,
|
||||
Signin,
|
||||
ReversiGame,
|
||||
ReversiMatching,
|
||||
...charts as any
|
||||
];
|
||||
|
||||
export function initDb(justBorrow = false, sync = false, log = false) {
|
||||
try {
|
||||
const conn = getConnection();
|
||||
|
@ -101,7 +150,7 @@ export function initDb(justBorrow = false, sync = false, log = false) {
|
|||
options: {
|
||||
host: config.redis.host,
|
||||
port: config.redis.port,
|
||||
options:{
|
||||
options: {
|
||||
password: config.redis.pass,
|
||||
prefix: config.redis.prefix,
|
||||
db: config.redis.db || 0
|
||||
|
@ -110,49 +159,6 @@ export function initDb(justBorrow = false, sync = false, log = false) {
|
|||
} : false,
|
||||
logging: log,
|
||||
logger: log ? new MyCustomLogger() : undefined,
|
||||
entities: [
|
||||
Meta,
|
||||
Instance,
|
||||
App,
|
||||
AuthSession,
|
||||
AccessToken,
|
||||
User,
|
||||
UserProfile,
|
||||
UserKeypair,
|
||||
UserPublickey,
|
||||
UserList,
|
||||
UserListJoining,
|
||||
UserGroup,
|
||||
UserGroupJoining,
|
||||
UserGroupInvite,
|
||||
UserNotePining,
|
||||
Following,
|
||||
FollowRequest,
|
||||
Muting,
|
||||
Blocking,
|
||||
Note,
|
||||
NoteFavorite,
|
||||
NoteReaction,
|
||||
NoteWatching,
|
||||
NoteUnread,
|
||||
Page,
|
||||
PageLike,
|
||||
Log,
|
||||
DriveFile,
|
||||
DriveFolder,
|
||||
Poll,
|
||||
PollVote,
|
||||
Notification,
|
||||
Emoji,
|
||||
Hashtag,
|
||||
SwSubscription,
|
||||
AbuseUserReport,
|
||||
RegistrationTicket,
|
||||
MessagingMessage,
|
||||
Signin,
|
||||
ReversiGame,
|
||||
ReversiMatching,
|
||||
...charts as any
|
||||
]
|
||||
entities: entities
|
||||
});
|
||||
}
|
||||
|
|
|
@ -6,8 +6,8 @@ import { toUnicode } from 'punycode';
|
|||
import { emojiRegex } from '../misc/emoji-regex';
|
||||
|
||||
export function removeOrphanedBrackets(s: string): string {
|
||||
const openBrackets = ['(', '「'];
|
||||
const closeBrackets = [')', '」'];
|
||||
const openBrackets = ['(', '「', '['];
|
||||
const closeBrackets = [')', '」', ']'];
|
||||
const xs = cumulativeSum(s.split('').map(c => {
|
||||
if (openBrackets.includes(c)) return 1;
|
||||
if (closeBrackets.includes(c)) return -1;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import * as fs from 'fs';
|
||||
import fileType from 'file-type';
|
||||
import fileType = require('file-type');
|
||||
import checkSvg from '../misc/check-svg';
|
||||
|
||||
export async function detectMine(path: string) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import chalk from 'chalk';
|
|||
import Logger from '../services/logger';
|
||||
|
||||
export async function downloadUrl(url: string, path: string) {
|
||||
const logger = new Logger('download-url');
|
||||
const logger = new Logger('download');
|
||||
|
||||
await new Promise((res, rej) => {
|
||||
logger.info(`Downloading ${chalk.cyan(url)} ...`);
|
||||
|
|
|
@ -1,17 +1,3 @@
|
|||
export const types = {
|
||||
boolean: 'boolean' as 'boolean',
|
||||
string: 'string' as 'string',
|
||||
number: 'number' as 'number',
|
||||
array: 'array' as 'array',
|
||||
object: 'object' as 'object',
|
||||
any: 'any' as 'any',
|
||||
};
|
||||
|
||||
export const bool = {
|
||||
true: true as true,
|
||||
false: false as false,
|
||||
};
|
||||
|
||||
export type Schema = {
|
||||
type: 'boolean' | 'number' | 'string' | 'array' | 'object' | 'any';
|
||||
nullable: boolean;
|
||||
|
|
46
src/models/entities/attestation-challenge.ts
Normal file
46
src/models/entities/attestation-challenge.ts
Normal file
|
@ -0,0 +1,46 @@
|
|||
import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm';
|
||||
import { User } from './user';
|
||||
import { id } from '../id';
|
||||
|
||||
@Entity()
|
||||
export class AttestationChallenge {
|
||||
@PrimaryColumn(id())
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@PrimaryColumn(id())
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
length: 64,
|
||||
comment: 'Hex-encoded sha256 hash of the challenge.'
|
||||
})
|
||||
public challenge: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
comment: 'The date challenge was created for expiry purposes.'
|
||||
})
|
||||
public createdAt: Date;
|
||||
|
||||
@Column('boolean', {
|
||||
comment:
|
||||
'Indicates that the challenge is only for registration purposes if true to prevent the challenge for being used as authentication.',
|
||||
default: false
|
||||
})
|
||||
public registrationChallenge: boolean;
|
||||
|
||||
constructor(data: Partial<AttestationChallenge>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -76,6 +76,11 @@ export class UserProfile {
|
|||
})
|
||||
public twoFactorEnabled: boolean;
|
||||
|
||||
@Column('boolean', {
|
||||
default: false,
|
||||
})
|
||||
public securityKeysAvailable: boolean;
|
||||
|
||||
@Column('varchar', {
|
||||
length: 128, nullable: true,
|
||||
comment: 'The password hash of the User. It will be null if the origin of the user is local.'
|
||||
|
|
48
src/models/entities/user-security-key.ts
Normal file
48
src/models/entities/user-security-key.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne, Index } from 'typeorm';
|
||||
import { User } from './user';
|
||||
import { id } from '../id';
|
||||
|
||||
@Entity()
|
||||
export class UserSecurityKey {
|
||||
@PrimaryColumn('varchar', {
|
||||
comment: 'Variable-length id given to navigator.credentials.get()'
|
||||
})
|
||||
public id: string;
|
||||
|
||||
@Index()
|
||||
@Column(id())
|
||||
public userId: User['id'];
|
||||
|
||||
@ManyToOne(type => User, {
|
||||
onDelete: 'CASCADE'
|
||||
})
|
||||
@JoinColumn()
|
||||
public user: User | null;
|
||||
|
||||
@Index()
|
||||
@Column('varchar', {
|
||||
comment:
|
||||
'Variable-length public key used to verify attestations (hex-encoded).'
|
||||
})
|
||||
public publicKey: string;
|
||||
|
||||
@Column('timestamp with time zone', {
|
||||
comment:
|
||||
'The date of the last time the UserSecurityKey was successfully validated.'
|
||||
})
|
||||
public lastUsed: Date;
|
||||
|
||||
@Column('varchar', {
|
||||
comment: 'User-defined name for this key',
|
||||
length: 30
|
||||
})
|
||||
public name: string;
|
||||
|
||||
constructor(data: Partial<UserSecurityKey>) {
|
||||
if (data == null) return;
|
||||
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
(this as any)[k] = v;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
export const id = () => ({
|
||||
type: 'varchar' as 'varchar',
|
||||
type: 'varchar' as const,
|
||||
length: 32
|
||||
});
|
||||
|
|
|
@ -37,6 +37,8 @@ import { FollowingRepository } from './repositories/following';
|
|||
import { AbuseUserReportRepository } from './repositories/abuse-user-report';
|
||||
import { AuthSessionRepository } from './repositories/auth-session';
|
||||
import { UserProfile } from './entities/user-profile';
|
||||
import { AttestationChallenge } from './entities/attestation-challenge';
|
||||
import { UserSecurityKey } from './entities/user-security-key';
|
||||
import { HashtagRepository } from './repositories/hashtag';
|
||||
import { PageRepository } from './repositories/page';
|
||||
import { PageLikeRepository } from './repositories/page-like';
|
||||
|
@ -52,6 +54,8 @@ export const PollVotes = getRepository(PollVote);
|
|||
export const Users = getCustomRepository(UserRepository);
|
||||
export const UserProfiles = getRepository(UserProfile);
|
||||
export const UserKeypairs = getRepository(UserKeypair);
|
||||
export const AttestationChallenges = getRepository(AttestationChallenge);
|
||||
export const UserSecurityKeys = getRepository(UserSecurityKey);
|
||||
export const UserPublickeys = getRepository(UserPublickey);
|
||||
export const UserLists = getCustomRepository(UserListRepository);
|
||||
export const UserListJoinings = getRepository(UserListJoining);
|
||||
|
|
|
@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
|
|||
import { App } from '../entities/app';
|
||||
import { AccessTokens } from '..';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { types, bool, SchemaType } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
|
||||
export type PackedApp = SchemaType<typeof packedAppSchema>;
|
||||
|
||||
|
@ -42,37 +42,37 @@ export class AppRepository extends Repository<App> {
|
|||
}
|
||||
|
||||
export const packedAppSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this Note.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
name: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'アプリケーションの名前'
|
||||
},
|
||||
callbackUrl: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.true,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
description: 'コールバックするURL'
|
||||
},
|
||||
permission: {
|
||||
type: types.array,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
}
|
||||
},
|
||||
secret: {
|
||||
type: types.string,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
description: 'アプリケーションのシークレットキー'
|
||||
}
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Users } from '..';
|
|||
import { Blocking } from '../entities/blocking';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
import { SchemaType, types, bool } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
|
||||
export type PackedBlocking = SchemaType<typeof packedBlockingSchema>;
|
||||
|
||||
|
@ -34,30 +34,30 @@ export class BlockingRepository extends Repository<Blocking> {
|
|||
}
|
||||
|
||||
export const packedBlockingSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this blocking.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the blocking was created.'
|
||||
},
|
||||
blockeeId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
},
|
||||
blockee: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'User',
|
||||
description: 'The blockee.'
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@ import { User } from '../entities/user';
|
|||
import { toPuny } from '../../misc/convert-host';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
import { types, bool, SchemaType } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
|
||||
export type PackedDriveFile = SchemaType<typeof packedDriveFileSchema>;
|
||||
|
||||
|
@ -114,63 +114,63 @@ export class DriveFileRepository extends Repository<DriveFile> {
|
|||
}
|
||||
|
||||
export const packedDriveFileSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this Drive file.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the Drive file was created on Misskey.'
|
||||
},
|
||||
name: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'The file name with extension.',
|
||||
example: 'lenna.jpg'
|
||||
},
|
||||
type: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'The MIME type of this Drive file.',
|
||||
example: 'image/jpeg'
|
||||
},
|
||||
md5: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'md5',
|
||||
description: 'The MD5 hash of this Drive file.',
|
||||
example: '15eca7fba0480996e2245f5185bf39f2'
|
||||
},
|
||||
size: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'number' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'The size of this Drive file. (bytes)',
|
||||
example: 51469
|
||||
},
|
||||
url: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.true,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
format: 'url',
|
||||
description: 'The URL of this Drive file.',
|
||||
},
|
||||
folderId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.true,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
format: 'id',
|
||||
description: 'The parent folder ID of this Drive file.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
isSensitive: {
|
||||
type: types.boolean,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'Whether this Drive file is sensitive.',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@ import { DriveFolders, DriveFiles } from '..';
|
|||
import { DriveFolder } from '../entities/drive-folder';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
import { SchemaType, types, bool } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
|
||||
export type PackedDriveFolder = SchemaType<typeof packedDriveFolderSchema>;
|
||||
|
||||
|
@ -53,47 +53,47 @@ export class DriveFolderRepository extends Repository<DriveFolder> {
|
|||
}
|
||||
|
||||
export const packedDriveFolderSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this Drive folder.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the Drive folder was created.'
|
||||
},
|
||||
name: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'The folder name.',
|
||||
},
|
||||
foldersCount: {
|
||||
type: types.number,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'number' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
description: 'The count of child folders.',
|
||||
},
|
||||
filesCount: {
|
||||
type: types.number,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'number' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
description: 'The count of child files.',
|
||||
},
|
||||
parentId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.true,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
format: 'id',
|
||||
description: 'The parent folder ID of this folder.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
parent: {
|
||||
type: types.object,
|
||||
optional: bool.true, nullable: bool.true,
|
||||
type: 'object' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
ref: 'DriveFolder'
|
||||
},
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Users } from '..';
|
|||
import { Following } from '../entities/following';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
import { SchemaType, types, bool } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
|
||||
type LocalFollowerFollowing = Following & {
|
||||
followerHost: null;
|
||||
|
@ -88,41 +88,41 @@ export class FollowingRepository extends Repository<Following> {
|
|||
}
|
||||
|
||||
export const packedFollowingSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this following.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the following was created.'
|
||||
},
|
||||
followeeId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
},
|
||||
followee: {
|
||||
type: types.object,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
ref: 'User',
|
||||
description: 'The followee.'
|
||||
},
|
||||
followerId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
},
|
||||
follower: {
|
||||
type: types.object,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
ref: 'User',
|
||||
description: 'The follower.'
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { Hashtag } from '../entities/hashtag';
|
||||
import { SchemaType, types, bool } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
|
||||
export type PackedHashtag = SchemaType<typeof packedHashtagSchema>;
|
||||
|
||||
|
@ -28,43 +28,43 @@ export class HashtagRepository extends Repository<Hashtag> {
|
|||
}
|
||||
|
||||
export const packedHashtagSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
tag: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'The hashtag name. No # prefixed.',
|
||||
example: 'misskey',
|
||||
},
|
||||
mentionedUsersCount: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'number' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'Number of all users using this hashtag.'
|
||||
},
|
||||
mentionedLocalUsersCount: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'number' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'Number of local users using this hashtag.'
|
||||
},
|
||||
mentionedRemoteUsersCount: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'number' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'Number of remote users using this hashtag.'
|
||||
},
|
||||
attachedUsersCount: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'number' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'Number of all users who attached this hashtag to profile.'
|
||||
},
|
||||
attachedLocalUsersCount: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'number' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'Number of local users who attached this hashtag to profile.'
|
||||
},
|
||||
attachedRemoteUsersCount: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'number' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'Number of remote users who attached this hashtag to profile.'
|
||||
},
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
|
|||
import { MessagingMessage } from '../entities/messaging-message';
|
||||
import { Users, DriveFiles, UserGroups } from '..';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { types, bool, SchemaType } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
|
||||
export type PackedMessagingMessage = SchemaType<typeof packedMessagingMessageSchema>;
|
||||
|
||||
|
@ -46,76 +46,76 @@ export class MessagingMessageRepository extends Repository<MessagingMessage> {
|
|||
}
|
||||
|
||||
export const packedMessagingMessageSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this MessagingMessage.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the MessagingMessage was created.'
|
||||
},
|
||||
userId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
},
|
||||
user: {
|
||||
type: types.object,
|
||||
type: 'object' as const,
|
||||
ref: 'User',
|
||||
optional: bool.true, nullable: bool.false,
|
||||
optional: true as const, nullable: false as const,
|
||||
},
|
||||
text: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.true,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
},
|
||||
fileId: {
|
||||
type: types.string,
|
||||
optional: bool.true, nullable: bool.true,
|
||||
type: 'string' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
format: 'id',
|
||||
},
|
||||
file: {
|
||||
type: types.object,
|
||||
optional: bool.true, nullable: bool.true,
|
||||
type: 'object' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
ref: 'DriveFile',
|
||||
},
|
||||
recipientId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.true,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
format: 'id',
|
||||
},
|
||||
recipient: {
|
||||
type: types.object,
|
||||
optional: bool.true, nullable: bool.true,
|
||||
type: 'object' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
ref: 'User'
|
||||
},
|
||||
groupId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.true,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
format: 'id',
|
||||
},
|
||||
group: {
|
||||
type: types.object,
|
||||
optional: bool.true, nullable: bool.true,
|
||||
type: 'object' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
ref: 'UserGroup'
|
||||
},
|
||||
isRead: {
|
||||
type: types.boolean,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'boolean' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
},
|
||||
reads: {
|
||||
type: types.array,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id'
|
||||
}
|
||||
},
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Users } from '..';
|
|||
import { Muting } from '../entities/muting';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
import { types, bool, SchemaType } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
|
||||
export type PackedMuting = SchemaType<typeof packedMutingSchema>;
|
||||
|
||||
|
@ -34,30 +34,30 @@ export class MutingRepository extends Repository<Muting> {
|
|||
}
|
||||
|
||||
export const packedMutingSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this muting.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the muting was created.'
|
||||
},
|
||||
muteeId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
},
|
||||
mutee: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'User',
|
||||
description: 'The mutee.'
|
||||
},
|
||||
|
|
|
@ -2,7 +2,6 @@ import { EntityRepository, Repository } from 'typeorm';
|
|||
import { NoteFavorite } from '../entities/note-favorite';
|
||||
import { Notes } from '..';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { types, bool } from '../../misc/schema';
|
||||
|
||||
@EntityRepository(NoteFavorite)
|
||||
export class NoteFavoriteRepository extends Repository<NoteFavorite> {
|
||||
|
@ -29,30 +28,30 @@ export class NoteFavoriteRepository extends Repository<NoteFavorite> {
|
|||
}
|
||||
|
||||
export const packedNoteFavoriteSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this favorite.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the favorite was created.'
|
||||
},
|
||||
note: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
},
|
||||
noteId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
},
|
||||
},
|
||||
|
|
|
@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
|
|||
import { NoteReaction } from '../entities/note-reaction';
|
||||
import { Users } from '..';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { types, bool, SchemaType } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
|
||||
export type PackedNoteReaction = SchemaType<typeof packedNoteReactionSchema>;
|
||||
|
||||
|
@ -24,31 +24,31 @@ export class NoteReactionRepository extends Repository<NoteReaction> {
|
|||
}
|
||||
|
||||
export const packedNoteReactionSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this reaction.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the reaction was created.'
|
||||
},
|
||||
user: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'User',
|
||||
description: 'User who performed this reaction.'
|
||||
},
|
||||
type: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'The reaction type.'
|
||||
},
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@ import { unique, concat } from '../../prelude/array';
|
|||
import { nyaize } from '../../misc/nyaize';
|
||||
import { Emojis, Users, Apps, PollVotes, DriveFiles, NoteReactions, Followings, Polls } from '..';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { SchemaType, types, bool } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
|
||||
export type PackedNote = SchemaType<typeof packedNoteSchema>;
|
||||
|
@ -144,8 +144,8 @@ export class NoteRepository extends Repository<Note> {
|
|||
|
||||
let text = note.text;
|
||||
|
||||
if (note.name) {
|
||||
text = `【${note.name}】\n${note.text}`;
|
||||
if (note.name && note.uri) {
|
||||
text = `【${note.name}】\n${(note.text || '').trim()}\n${note.uri}`;
|
||||
}
|
||||
|
||||
const reactionEmojis = unique(concat([note.emojis, Object.keys(note.reactions)]));
|
||||
|
@ -218,125 +218,125 @@ export class NoteRepository extends Repository<Note> {
|
|||
}
|
||||
|
||||
export const packedNoteSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this Note.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the Note was created on Misskey.'
|
||||
},
|
||||
text: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.true,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
},
|
||||
cw: {
|
||||
type: types.string,
|
||||
optional: bool.true, nullable: bool.true,
|
||||
type: 'string' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
},
|
||||
userId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
},
|
||||
user: {
|
||||
type: types.object,
|
||||
type: 'object' as const,
|
||||
ref: 'User',
|
||||
optional: bool.false, nullable: bool.false,
|
||||
optional: false as const, nullable: false as const,
|
||||
},
|
||||
replyId: {
|
||||
type: types.string,
|
||||
optional: bool.true, nullable: bool.true,
|
||||
type: 'string' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
format: 'id',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
renoteId: {
|
||||
type: types.string,
|
||||
optional: bool.true, nullable: bool.true,
|
||||
type: 'string' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
format: 'id',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
reply: {
|
||||
type: types.object,
|
||||
optional: bool.true, nullable: bool.true,
|
||||
type: 'object' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
ref: 'Note'
|
||||
},
|
||||
renote: {
|
||||
type: types.object,
|
||||
optional: bool.true, nullable: bool.true,
|
||||
type: 'object' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
ref: 'Note'
|
||||
},
|
||||
viaMobile: {
|
||||
type: types.boolean,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'boolean' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
},
|
||||
isHidden: {
|
||||
type: types.boolean,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'boolean' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
},
|
||||
visibility: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
},
|
||||
mentions: {
|
||||
type: types.array,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id'
|
||||
}
|
||||
},
|
||||
visibleUserIds: {
|
||||
type: types.array,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id'
|
||||
}
|
||||
},
|
||||
fileIds: {
|
||||
type: types.array,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id'
|
||||
}
|
||||
},
|
||||
files: {
|
||||
type: types.array,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'DriveFile'
|
||||
}
|
||||
},
|
||||
tags: {
|
||||
type: types.array,
|
||||
optional: bool.true, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: true as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
}
|
||||
},
|
||||
poll: {
|
||||
type: types.object,
|
||||
optional: bool.true, nullable: bool.true,
|
||||
type: 'object' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
},
|
||||
geo: {
|
||||
type: types.object,
|
||||
optional: bool.true, nullable: bool.true,
|
||||
type: 'object' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -3,7 +3,7 @@ import { Users, Notes } from '..';
|
|||
import { Notification } from '../entities/notification';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
import { types, bool, SchemaType } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
|
||||
export type PackedNotification = SchemaType<typeof packedNotificationSchema>;
|
||||
|
||||
|
@ -51,37 +51,37 @@ export class NotificationRepository extends Repository<Notification> {
|
|||
}
|
||||
|
||||
export const packedNotificationSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this notification.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the notification was created.'
|
||||
},
|
||||
type: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
enum: ['follow', 'receiveFollowRequest', 'mention', 'reply', 'renote', 'quote', 'reaction', 'pollVote'],
|
||||
description: 'The type of the notification.'
|
||||
},
|
||||
userId: {
|
||||
type: types.string,
|
||||
optional: bool.true, nullable: bool.true,
|
||||
type: 'string' as const,
|
||||
optional: true as const, nullable: true as const,
|
||||
format: 'id',
|
||||
},
|
||||
user: {
|
||||
type: types.object,
|
||||
type: 'object' as const,
|
||||
ref: 'User',
|
||||
optional: bool.true, nullable: bool.true,
|
||||
optional: true as const, nullable: true as const,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { EntityRepository, Repository } from 'typeorm';
|
||||
import { Page } from '../entities/page';
|
||||
import { SchemaType, types, bool } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
import { Users, DriveFiles, PageLikes } from '..';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
import { DriveFile } from '../entities/drive-file';
|
||||
|
@ -89,54 +89,54 @@ export class PageRepository extends Repository<Page> {
|
|||
}
|
||||
|
||||
export const packedPageSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
},
|
||||
updatedAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
},
|
||||
title: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
},
|
||||
name: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
},
|
||||
summary: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.true,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: true as const,
|
||||
},
|
||||
content: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
},
|
||||
variables: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
},
|
||||
userId: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
},
|
||||
user: {
|
||||
type: types.object,
|
||||
type: 'object' as const,
|
||||
ref: 'User',
|
||||
optional: bool.false, nullable: bool.false,
|
||||
optional: false as const, nullable: false as const,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
|
|||
import { UserGroup } from '../entities/user-group';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { UserGroupJoinings } from '..';
|
||||
import { bool, types, SchemaType } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
|
||||
export type PackedUserGroup = SchemaType<typeof packedUserGroupSchema>;
|
||||
|
||||
|
@ -28,38 +28,38 @@ export class UserGroupRepository extends Repository<UserGroup> {
|
|||
}
|
||||
|
||||
export const packedUserGroupSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this UserGroup.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the UserGroup was created.'
|
||||
},
|
||||
name: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'The name of the UserGroup.'
|
||||
},
|
||||
ownerId: {
|
||||
type: types.string,
|
||||
nullable: bool.false, optional: bool.false,
|
||||
type: 'string' as const,
|
||||
nullable: false as const, optional: false as const,
|
||||
format: 'id',
|
||||
},
|
||||
userIds: {
|
||||
type: types.array,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'array' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
items: {
|
||||
type: types.string,
|
||||
nullable: bool.false, optional: bool.false,
|
||||
type: 'string' as const,
|
||||
nullable: false as const, optional: false as const,
|
||||
format: 'id',
|
||||
}
|
||||
},
|
||||
|
|
|
@ -2,7 +2,7 @@ import { EntityRepository, Repository } from 'typeorm';
|
|||
import { UserList } from '../entities/user-list';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import { UserListJoinings } from '..';
|
||||
import { bool, types, SchemaType } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
|
||||
export type PackedUserList = SchemaType<typeof packedUserListSchema>;
|
||||
|
||||
|
@ -27,33 +27,33 @@ export class UserListRepository extends Repository<UserList> {
|
|||
}
|
||||
|
||||
export const packedUserListSchema = {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this UserList.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the UserList was created.'
|
||||
},
|
||||
name: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'The name of the UserList.'
|
||||
},
|
||||
userIds: {
|
||||
type: types.array,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'array' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
items: {
|
||||
type: types.string,
|
||||
nullable: bool.false, optional: bool.false,
|
||||
type: 'string' as const,
|
||||
nullable: false as const, optional: false as const,
|
||||
format: 'id',
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import $ from 'cafy';
|
||||
import { EntityRepository, Repository, In } from 'typeorm';
|
||||
import { User, ILocalUser, IRemoteUser } from '../entities/user';
|
||||
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserGroupJoinings } from '..';
|
||||
import { Emojis, Notes, NoteUnreads, FollowRequests, Notifications, MessagingMessages, UserNotePinings, Followings, Blockings, Mutings, UserProfiles, UserSecurityKeys, UserGroupJoinings } from '..';
|
||||
import { ensure } from '../../prelude/ensure';
|
||||
import config from '../../config';
|
||||
import { SchemaType, bool, types } from '../../misc/schema';
|
||||
import { SchemaType } from '../../misc/schema';
|
||||
import { awaitAll } from '../../prelude/await-all';
|
||||
|
||||
export type PackedUser = SchemaType<typeof packedUserSchema>;
|
||||
|
@ -156,6 +156,11 @@ export class UserRepository extends Repository<User> {
|
|||
detail: true
|
||||
}),
|
||||
twoFactorEnabled: profile!.twoFactorEnabled,
|
||||
securityKeys: profile!.twoFactorEnabled
|
||||
? UserSecurityKeys.count({
|
||||
userId: user.id
|
||||
}).then(result => result >= 1)
|
||||
: false,
|
||||
twitter: profile!.twitter ? {
|
||||
id: profile!.twitterUserId,
|
||||
screenName: profile!.twitterScreenName
|
||||
|
@ -195,6 +200,15 @@ export class UserRepository extends Repository<User> {
|
|||
clientData: profile!.clientData,
|
||||
email: profile!.email,
|
||||
emailVerified: profile!.emailVerified,
|
||||
securityKeysList: profile!.twoFactorEnabled
|
||||
? UserSecurityKeys.find({
|
||||
where: {
|
||||
userId: user.id
|
||||
},
|
||||
select: ['id', 'name', 'lastUsed']
|
||||
})
|
||||
: []
|
||||
|
||||
} : {}),
|
||||
|
||||
...(relation ? {
|
||||
|
@ -243,150 +257,150 @@ export class UserRepository extends Repository<User> {
|
|||
}
|
||||
|
||||
export const packedUserSchema = {
|
||||
type: types.object,
|
||||
nullable: bool.false, optional: bool.false,
|
||||
type: 'object' as const,
|
||||
nullable: false as const, optional: false as const,
|
||||
properties: {
|
||||
id: {
|
||||
type: types.string,
|
||||
nullable: bool.false, optional: bool.false,
|
||||
type: 'string' as const,
|
||||
nullable: false as const, optional: false as const,
|
||||
format: 'id',
|
||||
description: 'The unique identifier for this User.',
|
||||
example: 'xxxxxxxxxx',
|
||||
},
|
||||
username: {
|
||||
type: types.string,
|
||||
nullable: bool.false, optional: bool.false,
|
||||
type: 'string' as const,
|
||||
nullable: false as const, optional: false as const,
|
||||
description: 'The screen name, handle, or alias that this user identifies themselves with.',
|
||||
example: 'ai'
|
||||
},
|
||||
name: {
|
||||
type: types.string,
|
||||
nullable: bool.true, optional: bool.false,
|
||||
type: 'string' as const,
|
||||
nullable: true as const, optional: false as const,
|
||||
description: 'The name of the user, as they’ve defined it.',
|
||||
example: '藍'
|
||||
},
|
||||
url: {
|
||||
type: types.string,
|
||||
type: 'string' as const,
|
||||
format: 'url',
|
||||
nullable: bool.true, optional: bool.true,
|
||||
nullable: true as const, optional: true as const,
|
||||
},
|
||||
avatarUrl: {
|
||||
type: types.string,
|
||||
type: 'string' as const,
|
||||
format: 'url',
|
||||
nullable: bool.true, optional: bool.false,
|
||||
nullable: true as const, optional: false as const,
|
||||
},
|
||||
avatarColor: {
|
||||
type: types.any,
|
||||
nullable: bool.true, optional: bool.false,
|
||||
type: 'any' as const,
|
||||
nullable: true as const, optional: false as const,
|
||||
},
|
||||
bannerUrl: {
|
||||
type: types.string,
|
||||
type: 'string' as const,
|
||||
format: 'url',
|
||||
nullable: bool.true, optional: bool.true,
|
||||
nullable: true as const, optional: true as const,
|
||||
},
|
||||
bannerColor: {
|
||||
type: types.any,
|
||||
nullable: bool.true, optional: bool.true,
|
||||
type: 'any' as const,
|
||||
nullable: true as const, optional: true as const,
|
||||
},
|
||||
emojis: {
|
||||
type: types.any,
|
||||
nullable: bool.true, optional: bool.false,
|
||||
type: 'any' as const,
|
||||
nullable: true as const, optional: false as const,
|
||||
},
|
||||
host: {
|
||||
type: types.string,
|
||||
nullable: bool.true, optional: bool.false,
|
||||
type: 'string' as const,
|
||||
nullable: true as const, optional: false as const,
|
||||
example: 'misskey.example.com'
|
||||
},
|
||||
description: {
|
||||
type: types.string,
|
||||
nullable: bool.true, optional: bool.true,
|
||||
type: 'string' as const,
|
||||
nullable: true as const, optional: true as const,
|
||||
description: 'The user-defined UTF-8 string describing their account.',
|
||||
example: 'Hi masters, I am Ai!'
|
||||
},
|
||||
birthday: {
|
||||
type: types.string,
|
||||
nullable: bool.true, optional: bool.true,
|
||||
type: 'string' as const,
|
||||
nullable: true as const, optional: true as const,
|
||||
example: '2018-03-12'
|
||||
},
|
||||
createdAt: {
|
||||
type: types.string,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'string' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
format: 'date-time',
|
||||
description: 'The date that the user account was created on Misskey.'
|
||||
},
|
||||
updatedAt: {
|
||||
type: types.string,
|
||||
nullable: bool.true, optional: bool.true,
|
||||
type: 'string' as const,
|
||||
nullable: true as const, optional: true as const,
|
||||
format: 'date-time',
|
||||
},
|
||||
location: {
|
||||
type: types.string,
|
||||
nullable: bool.true, optional: bool.true,
|
||||
type: 'string' as const,
|
||||
nullable: true as const, optional: true as const,
|
||||
},
|
||||
followersCount: {
|
||||
type: types.number,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'number' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
description: 'The number of followers this account currently has.'
|
||||
},
|
||||
followingCount: {
|
||||
type: types.number,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'number' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
description: 'The number of users this account is following.'
|
||||
},
|
||||
notesCount: {
|
||||
type: types.number,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'number' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
description: 'The number of Notes (including renotes) issued by the user.'
|
||||
},
|
||||
isBot: {
|
||||
type: types.boolean,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'boolean' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
description: 'Whether this account is a bot.'
|
||||
},
|
||||
pinnedNoteIds: {
|
||||
type: types.array,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'array' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
items: {
|
||||
type: types.string,
|
||||
nullable: bool.false, optional: bool.false,
|
||||
type: 'string' as const,
|
||||
nullable: false as const, optional: false as const,
|
||||
format: 'id',
|
||||
}
|
||||
},
|
||||
pinnedNotes: {
|
||||
type: types.array,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'array' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
items: {
|
||||
type: types.object,
|
||||
nullable: bool.false, optional: bool.false,
|
||||
type: 'object' as const,
|
||||
nullable: false as const, optional: false as const,
|
||||
ref: 'Note'
|
||||
}
|
||||
},
|
||||
isCat: {
|
||||
type: types.boolean,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'boolean' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
description: 'Whether this account is a cat.'
|
||||
},
|
||||
isAdmin: {
|
||||
type: types.boolean,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'boolean' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
description: 'Whether this account is the admin.'
|
||||
},
|
||||
isModerator: {
|
||||
type: types.boolean,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'boolean' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
description: 'Whether this account is a moderator.'
|
||||
},
|
||||
isLocked: {
|
||||
type: types.boolean,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'boolean' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
},
|
||||
hasUnreadSpecifiedNotes: {
|
||||
type: types.boolean,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'boolean' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
},
|
||||
hasUnreadMentions: {
|
||||
type: types.boolean,
|
||||
nullable: bool.false, optional: bool.true,
|
||||
type: 'boolean' as const,
|
||||
nullable: false as const, optional: true as const,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,19 +0,0 @@
|
|||
import * as fs from 'fs';
|
||||
import config from './config';
|
||||
|
||||
const json = {
|
||||
type: 'postgres',
|
||||
host: config.db.host,
|
||||
port: config.db.port,
|
||||
username: config.db.user,
|
||||
password: config.db.pass,
|
||||
database: config.db.db,
|
||||
extra: config.db.extra,
|
||||
entities: ['src/models/entities/*.ts'],
|
||||
migrations: ['migration/*.ts'],
|
||||
cli: {
|
||||
migrationsDir: 'migration'
|
||||
}
|
||||
};
|
||||
|
||||
fs.writeFileSync('ormconfig.json', JSON.stringify(json));
|
|
@ -194,6 +194,13 @@ export function createDeleteObjectStorageFileJob(key: string) {
|
|||
});
|
||||
}
|
||||
|
||||
export function createCleanRemoteFilesJob() {
|
||||
return objectStorageQueue.add('cleanRemoteFiles', {}, {
|
||||
removeOnComplete: true,
|
||||
removeOnFail: true
|
||||
});
|
||||
}
|
||||
|
||||
export default function() {
|
||||
if (!program.onlyServer) {
|
||||
deliverQueue.process(128, processDeliver);
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import * as Bull from 'bull';
|
||||
|
||||
import { queueLogger } from '../../logger';
|
||||
import { deleteFile } from '../../../services/drive/delete-file';
|
||||
import { deleteFileSync } from '../../../services/drive/delete-file';
|
||||
import { Users, DriveFiles } from '../../../models';
|
||||
import { MoreThan } from 'typeorm';
|
||||
|
||||
|
@ -39,7 +39,7 @@ export async function deleteDriveFiles(job: Bull.Job, done: any): Promise<void>
|
|||
cursor = files[files.length - 1].id;
|
||||
|
||||
for (const file of files) {
|
||||
await deleteFile(file);
|
||||
await deleteFileSync(file);
|
||||
deletedCount++;
|
||||
}
|
||||
|
||||
|
|
50
src/queue/processors/object-storage/clean-remote-files.ts
Normal file
50
src/queue/processors/object-storage/clean-remote-files.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
import * as Bull from 'bull';
|
||||
|
||||
import { queueLogger } from '../../logger';
|
||||
import { deleteFileSync } from '../../../services/drive/delete-file';
|
||||
import { DriveFiles } from '../../../models';
|
||||
import { MoreThan, Not, IsNull } from 'typeorm';
|
||||
|
||||
const logger = queueLogger.createSubLogger('clean-remote-files');
|
||||
|
||||
export default async function cleanRemoteFiles(job: Bull.Job, done: any): Promise<void> {
|
||||
logger.info(`Deleting cached remote files...`);
|
||||
|
||||
let deletedCount = 0;
|
||||
let cursor: any = null;
|
||||
|
||||
while (true) {
|
||||
const files = await DriveFiles.find({
|
||||
where: {
|
||||
userHost: Not(IsNull()),
|
||||
isLink: false,
|
||||
...(cursor ? { id: MoreThan(cursor) } : {})
|
||||
},
|
||||
take: 8,
|
||||
order: {
|
||||
id: 1
|
||||
}
|
||||
});
|
||||
|
||||
if (files.length === 0) {
|
||||
job.progress(100);
|
||||
break;
|
||||
}
|
||||
|
||||
cursor = files[files.length - 1].id;
|
||||
|
||||
await Promise.all(files.map(file => deleteFileSync(file, true)));
|
||||
|
||||
deletedCount += 8;
|
||||
|
||||
const total = await DriveFiles.count({
|
||||
userHost: Not(IsNull()),
|
||||
isLink: false,
|
||||
});
|
||||
|
||||
job.progress(deletedCount / total);
|
||||
}
|
||||
|
||||
logger.succ(`All cahced remote files has been deleted.`);
|
||||
done();
|
||||
}
|
|
@ -1,22 +1,10 @@
|
|||
import * as Bull from 'bull';
|
||||
import * as Minio from 'minio';
|
||||
import { fetchMeta } from '../../../misc/fetch-meta';
|
||||
import { deleteObjectStorageFile } from '../../../services/drive/delete-file';
|
||||
|
||||
export default async (job: Bull.Job) => {
|
||||
const meta = await fetchMeta();
|
||||
|
||||
const minio = new Minio.Client({
|
||||
endPoint: meta.objectStorageEndpoint!,
|
||||
region: meta.objectStorageRegion ? meta.objectStorageRegion : undefined,
|
||||
port: meta.objectStoragePort ? meta.objectStoragePort : undefined,
|
||||
useSSL: meta.objectStorageUseSSL,
|
||||
accessKey: meta.objectStorageAccessKey!,
|
||||
secretKey: meta.objectStorageSecretKey!,
|
||||
});
|
||||
|
||||
const key: string = job.data.key;
|
||||
|
||||
await minio.removeObject(meta.objectStorageBucket!, key);
|
||||
await deleteObjectStorageFile(key);
|
||||
|
||||
return 'Success';
|
||||
};
|
||||
|
|
|
@ -1,12 +1,14 @@
|
|||
import * as Bull from 'bull';
|
||||
import deleteFile from './delete-file';
|
||||
import cleanRemoteFiles from './clean-remote-files';
|
||||
|
||||
const jobs = {
|
||||
deleteFile,
|
||||
cleanRemoteFiles,
|
||||
} as any;
|
||||
|
||||
export default function(q: Bull.Queue) {
|
||||
for (const [k, v] of Object.entries(jobs)) {
|
||||
q.process(k, v as any);
|
||||
q.process(k, 16, v as any);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
import Resolver from '../../resolver';
|
||||
import { IRemoteUser } from '../../../../models/entities/user';
|
||||
import announceNote from './note';
|
||||
import { IAnnounce, INote } from '../../type';
|
||||
import { IAnnounce, INote, validPost, getApId } from '../../type';
|
||||
import { apLogger } from '../../logger';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> => {
|
||||
const uri = activity.id || activity;
|
||||
const uri = getApId(activity);
|
||||
|
||||
logger.info(`Announce: ${uri}`);
|
||||
|
||||
|
@ -22,15 +22,9 @@ export default async (actor: IRemoteUser, activity: IAnnounce): Promise<void> =>
|
|||
throw e;
|
||||
}
|
||||
|
||||
switch (object.type) {
|
||||
case 'Note':
|
||||
case 'Question':
|
||||
case 'Article':
|
||||
if (validPost.includes(object.type)) {
|
||||
announceNote(resolver, actor, activity, object as INote);
|
||||
break;
|
||||
|
||||
default:
|
||||
} else {
|
||||
logger.warn(`Unknown announce type: ${object.type}`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Resolver from '../../resolver';
|
||||
import post from '../../../../services/note/create';
|
||||
import { IRemoteUser, User } from '../../../../models/entities/user';
|
||||
import { IAnnounce, INote } from '../../type';
|
||||
import { IAnnounce, INote, getApId, getApIds } from '../../type';
|
||||
import { fetchNote, resolveNote } from '../../models/note';
|
||||
import { resolvePerson } from '../../models/person';
|
||||
import { apLogger } from '../../logger';
|
||||
|
@ -14,17 +14,13 @@ const logger = apLogger;
|
|||
* アナウンスアクティビティを捌きます
|
||||
*/
|
||||
export default async function(resolver: Resolver, actor: IRemoteUser, activity: IAnnounce, note: INote): Promise<void> {
|
||||
const uri = activity.id || activity;
|
||||
const uri = getApId(activity);
|
||||
|
||||
// アナウンサーが凍結されていたらスキップ
|
||||
if (actor.isSuspended) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof uri !== 'string') {
|
||||
throw new Error('invalid announce');
|
||||
}
|
||||
|
||||
// アナウンス先をブロックしてたら中断
|
||||
const meta = await fetchMeta();
|
||||
if (meta.blockedHosts.includes(extractDbHost(uri))) return;
|
||||
|
@ -52,11 +48,14 @@ export default async function(resolver: Resolver, actor: IRemoteUser, activity:
|
|||
logger.info(`Creating the (Re)Note: ${uri}`);
|
||||
|
||||
//#region Visibility
|
||||
const visibility = getVisibility(activity.to || [], activity.cc || [], actor);
|
||||
const to = getApIds(activity.to);
|
||||
const cc = getApIds(activity.cc);
|
||||
|
||||
const visibility = getVisibility(to, cc, actor);
|
||||
|
||||
let visibleUsers: User[] = [];
|
||||
if (visibility == 'specified') {
|
||||
visibleUsers = await Promise.all((note.to || []).map(uri => resolvePerson(uri)));
|
||||
visibleUsers = await Promise.all(to.map(uri => resolvePerson(uri)));
|
||||
}
|
||||
//#endergion
|
||||
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
import { IRemoteUser } from '../../../../models/entities/user';
|
||||
import { createImage } from '../../models/image';
|
||||
|
||||
export default async function(actor: IRemoteUser, image: any): Promise<void> {
|
||||
await createImage(image.url, actor);
|
||||
}
|
|
@ -1,14 +1,13 @@
|
|||
import Resolver from '../../resolver';
|
||||
import { IRemoteUser } from '../../../../models/entities/user';
|
||||
import createImage from './image';
|
||||
import createNote from './note';
|
||||
import { ICreate } from '../../type';
|
||||
import { ICreate, getApId, validPost } from '../../type';
|
||||
import { apLogger } from '../../logger';
|
||||
|
||||
const logger = apLogger;
|
||||
|
||||
export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
|
||||
const uri = activity.id || activity;
|
||||
const uri = getApId(activity);
|
||||
|
||||
logger.info(`Create: ${uri}`);
|
||||
|
||||
|
@ -23,19 +22,9 @@ export default async (actor: IRemoteUser, activity: ICreate): Promise<void> => {
|
|||
throw e;
|
||||
}
|
||||
|
||||
switch (object.type) {
|
||||
case 'Image':
|
||||
createImage(actor, object);
|
||||
break;
|
||||
|
||||
case 'Note':
|
||||
case 'Question':
|
||||
case 'Article':
|
||||
if (validPost.includes(object.type)) {
|
||||
createNote(resolver, actor, object);
|
||||
break;
|
||||
|
||||
default:
|
||||
} else {
|
||||
logger.warn(`Unknown type: ${object.type}`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
import Resolver from '../../resolver';
|
||||
import deleteNote from './note';
|
||||
import { IRemoteUser } from '../../../../models/entities/user';
|
||||
import { IDelete } from '../../type';
|
||||
import { IDelete, getApId, validPost } from '../../type';
|
||||
import { apLogger } from '../../logger';
|
||||
import { Notes } from '../../../../models';
|
||||
|
||||
/**
|
||||
* 削除アクティビティを捌きます
|
||||
|
@ -17,24 +16,11 @@ export default async (actor: IRemoteUser, activity: IDelete): Promise<void> => {
|
|||
|
||||
const object = await resolver.resolve(activity.object);
|
||||
|
||||
const uri = (object as any).id;
|
||||
const uri = getApId(object);
|
||||
|
||||
switch (object.type) {
|
||||
case 'Note':
|
||||
case 'Question':
|
||||
case 'Article':
|
||||
deleteNote(actor, uri);
|
||||
break;
|
||||
|
||||
case 'Tombstone':
|
||||
const note = await Notes.findOne({ uri });
|
||||
if (note != null) {
|
||||
deleteNote(actor, uri);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
apLogger.warn(`Unknown type: ${object.type}`);
|
||||
break;
|
||||
if (validPost.includes(object.type) || object.type === 'Tombstone') {
|
||||
deleteNote(actor, uri);
|
||||
} else {
|
||||
apLogger.warn(`Unknown type: ${object.type}`);
|
||||
}
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ import { deliverQuestionUpdate } from '../../../services/note/polls/update';
|
|||
import { extractDbHost, toPuny } from '../../../misc/convert-host';
|
||||
import { Notes, Emojis, Polls } from '../../../models';
|
||||
import { Note } from '../../../models/entities/note';
|
||||
import { IObject, INote } from '../type';
|
||||
import { IObject, INote, getApIds, getOneApId, getApId, validPost } from '../type';
|
||||
import { Emoji } from '../../../models/entities/emoji';
|
||||
import { genId } from '../../../misc/gen-id';
|
||||
import { fetchMeta } from '../../../misc/fetch-meta';
|
||||
|
@ -32,7 +32,7 @@ export function validateNote(object: any, uri: string) {
|
|||
return new Error('invalid Note: object is null');
|
||||
}
|
||||
|
||||
if (!['Note', 'Question', 'Article'].includes(object.type)) {
|
||||
if (!validPost.includes(object.type)) {
|
||||
return new Error(`invalid Note: invalied object type ${object.type}`);
|
||||
}
|
||||
|
||||
|
@ -40,7 +40,7 @@ export function validateNote(object: any, uri: string) {
|
|||
return new Error(`invalid Note: id has different host. expected: ${expectHost}, actual: ${extractDbHost(object.id)}`);
|
||||
}
|
||||
|
||||
if (object.attributedTo && extractDbHost(object.attributedTo) !== expectHost) {
|
||||
if (object.attributedTo && extractDbHost(getOneApId(object.attributedTo)) !== expectHost) {
|
||||
return new Error(`invalid Note: attributedTo has different host. expected: ${expectHost}, actual: ${extractDbHost(object.attributedTo)}`);
|
||||
}
|
||||
|
||||
|
@ -53,8 +53,7 @@ export function validateNote(object: any, uri: string) {
|
|||
* Misskeyに対象のNoteが登録されていればそれを返します。
|
||||
*/
|
||||
export async function fetchNote(value: string | IObject, resolver?: Resolver): Promise<Note | null> {
|
||||
const uri = typeof value == 'string' ? value : value.id;
|
||||
if (uri == null) throw new Error('missing uri');
|
||||
const uri = getApId(value);
|
||||
|
||||
// URIがこのサーバーを指しているならデータベースからフェッチ
|
||||
if (uri.startsWith(config.url + '/')) {
|
||||
|
@ -76,12 +75,12 @@ export async function fetchNote(value: string | IObject, resolver?: Resolver): P
|
|||
/**
|
||||
* Noteを作成します。
|
||||
*/
|
||||
export async function createNote(value: any, resolver?: Resolver, silent = false): Promise<Note | null> {
|
||||
export async function createNote(value: string | IObject, resolver?: Resolver, silent = false): Promise<Note | null> {
|
||||
if (resolver == null) resolver = new Resolver();
|
||||
|
||||
const object: any = await resolver.resolve(value);
|
||||
|
||||
const entryUri = value.id || value;
|
||||
const entryUri = getApId(value);
|
||||
const err = validateNote(object, entryUri);
|
||||
if (err) {
|
||||
logger.error(`${err.message}`, {
|
||||
|
@ -101,7 +100,7 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
|||
logger.info(`Creating the Note: ${note.id}`);
|
||||
|
||||
// 投稿者をフェッチ
|
||||
const actor = await resolvePerson(note.attributedTo, resolver) as IRemoteUser;
|
||||
const actor = await resolvePerson(getOneApId(note.attributedTo), resolver) as IRemoteUser;
|
||||
|
||||
// 投稿者が凍結されていたらスキップ
|
||||
if (actor.isSuspended) {
|
||||
|
@ -109,24 +108,24 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
|||
}
|
||||
|
||||
//#region Visibility
|
||||
note.to = note.to == null ? [] : typeof note.to == 'string' ? [note.to] : note.to;
|
||||
note.cc = note.cc == null ? [] : typeof note.cc == 'string' ? [note.cc] : note.cc;
|
||||
const to = getApIds(note.to);
|
||||
const cc = getApIds(note.cc);
|
||||
|
||||
let visibility = 'public';
|
||||
let visibleUsers: User[] = [];
|
||||
if (!note.to.includes('https://www.w3.org/ns/activitystreams#Public')) {
|
||||
if (note.cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
|
||||
if (!to.includes('https://www.w3.org/ns/activitystreams#Public')) {
|
||||
if (cc.includes('https://www.w3.org/ns/activitystreams#Public')) {
|
||||
visibility = 'home';
|
||||
} else if (note.to.includes(`${actor.uri}/followers`)) { // TODO: person.followerと照合するべき?
|
||||
} else if (to.includes(`${actor.uri}/followers`)) { // TODO: person.followerと照合するべき?
|
||||
visibility = 'followers';
|
||||
} else {
|
||||
visibility = 'specified';
|
||||
visibleUsers = await Promise.all(note.to.map(uri => resolvePerson(uri, resolver)));
|
||||
visibleUsers = await Promise.all(to.map(uri => resolvePerson(uri, resolver)));
|
||||
}
|
||||
}
|
||||
//#endergion
|
||||
|
||||
const apMentions = await extractMentionedUsers(actor, note.to, note.cc, resolver);
|
||||
const apMentions = await extractMentionedUsers(actor, to, cc, resolver);
|
||||
|
||||
const apHashtags = await extractHashtags(note.tag);
|
||||
|
||||
|
@ -217,11 +216,11 @@ export async function createNote(value: any, resolver?: Resolver, silent = false
|
|||
const apEmojis = emojis.map(emoji => emoji.name);
|
||||
|
||||
const questionUri = note._misskey_question;
|
||||
const poll = await extractPollFromQuestion(note._misskey_question || note).catch(() => undefined);
|
||||
const poll = await extractPollFromQuestion(note._misskey_question || note, resolver).catch(() => undefined);
|
||||
|
||||
// ユーザーの情報が古かったらついでに更新しておく
|
||||
if (actor.lastFetchedAt == null || Date.now() - actor.lastFetchedAt.getTime() > 1000 * 60 * 60 * 24) {
|
||||
updatePerson(note.attributedTo);
|
||||
if (actor.uri) updatePerson(actor.uri);
|
||||
}
|
||||
|
||||
return await post(actor, {
|
||||
|
|
|
@ -1,12 +1,19 @@
|
|||
import config from '../../../config';
|
||||
import Resolver from '../resolver';
|
||||
import { IQuestion } from '../type';
|
||||
import { IObject, IQuestion, isQuestion, } from '../type';
|
||||
import { apLogger } from '../logger';
|
||||
import { Notes, Polls } from '../../../models';
|
||||
import { IPoll } from '../../../models/entities/poll';
|
||||
|
||||
export async function extractPollFromQuestion(source: string | IQuestion): Promise<IPoll> {
|
||||
const question = typeof source === 'string' ? await new Resolver().resolve(source) as IQuestion : source;
|
||||
export async function extractPollFromQuestion(source: string | IObject, resolver?: Resolver): Promise<IPoll> {
|
||||
if (resolver == null) resolver = new Resolver();
|
||||
|
||||
const question = await resolver.resolve(source);
|
||||
|
||||
if (!isQuestion(question)) {
|
||||
throw new Error('invalid type');
|
||||
}
|
||||
|
||||
const multiple = !question.oneOf;
|
||||
const expiresAt = question.endTime ? new Date(question.endTime) : null;
|
||||
|
||||
|
|
|
@ -6,9 +6,9 @@ export interface IObject {
|
|||
id?: string;
|
||||
summary?: string;
|
||||
published?: string;
|
||||
cc?: string[];
|
||||
to?: string[];
|
||||
attributedTo: string;
|
||||
cc?: IObject | string | (IObject | string)[];
|
||||
to?: IObject | string | (IObject | string)[];
|
||||
attributedTo: IObject | string | (IObject | string)[];
|
||||
attachment?: any[];
|
||||
inReplyTo?: any;
|
||||
replies?: ICollection;
|
||||
|
@ -23,6 +23,32 @@ export interface IObject {
|
|||
sensitive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get array of ActivityStreams Objects id
|
||||
*/
|
||||
export function getApIds(value: IObject | string | (IObject | string)[] | undefined): string[] {
|
||||
if (value == null) return [];
|
||||
const array = Array.isArray(value) ? value : [value];
|
||||
return array.map(x => getApId(x));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get first ActivityStreams Object id
|
||||
*/
|
||||
export function getOneApId(value: IObject | string | (IObject | string)[]): string {
|
||||
const firstOne = Array.isArray(value) ? value[0] : value;
|
||||
return getApId(firstOne);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ActivityStreams Object id
|
||||
*/
|
||||
export function getApId(value: string | IObject): string {
|
||||
if (typeof value === 'string') return value;
|
||||
if (typeof value.id === 'string') return value.id;
|
||||
throw new Error(`cannot detemine id`);
|
||||
}
|
||||
|
||||
export interface IActivity extends IObject {
|
||||
//type: 'Activity';
|
||||
actor: IObject | string;
|
||||
|
@ -42,8 +68,10 @@ export interface IOrderedCollection extends IObject {
|
|||
orderedItems: IObject | string | IObject[] | string[];
|
||||
}
|
||||
|
||||
export const validPost = ['Note', 'Question', 'Article', 'Audio', 'Document', 'Image', 'Page', 'Video'];
|
||||
|
||||
export interface INote extends IObject {
|
||||
type: 'Note' | 'Question';
|
||||
type: 'Note' | 'Question' | 'Article' | 'Audio' | 'Document' | 'Image' | 'Page' | 'Video';
|
||||
_misskey_content?: string;
|
||||
_misskey_quote?: string;
|
||||
_misskey_question?: string;
|
||||
|
@ -59,6 +87,9 @@ export interface IQuestion extends IObject {
|
|||
endTime?: Date;
|
||||
}
|
||||
|
||||
export const isQuestion = (object: IObject): object is IQuestion =>
|
||||
object.type === 'Note' || object.type === 'Question';
|
||||
|
||||
interface IQuestionChoice {
|
||||
name?: string;
|
||||
replies?: ICollection;
|
||||
|
|
422
src/server/api/2fa.ts
Normal file
422
src/server/api/2fa.ts
Normal file
|
@ -0,0 +1,422 @@
|
|||
import * as crypto from 'crypto';
|
||||
import config from '../../config';
|
||||
import * as jsrsasign from 'jsrsasign';
|
||||
|
||||
const ECC_PRELUDE = Buffer.from([0x04]);
|
||||
const NULL_BYTE = Buffer.from([0]);
|
||||
const PEM_PRELUDE = Buffer.from(
|
||||
'3059301306072a8648ce3d020106082a8648ce3d030107034200',
|
||||
'hex'
|
||||
);
|
||||
|
||||
// Android Safetynet attestations are signed with this cert:
|
||||
const GSR2 = `-----BEGIN CERTIFICATE-----
|
||||
MIIDujCCAqKgAwIBAgILBAAAAAABD4Ym5g0wDQYJKoZIhvcNAQEFBQAwTDEgMB4G
|
||||
A1UECxMXR2xvYmFsU2lnbiBSb290IENBIC0gUjIxEzARBgNVBAoTCkdsb2JhbFNp
|
||||
Z24xEzARBgNVBAMTCkdsb2JhbFNpZ24wHhcNMDYxMjE1MDgwMDAwWhcNMjExMjE1
|
||||
MDgwMDAwWjBMMSAwHgYDVQQLExdHbG9iYWxTaWduIFJvb3QgQ0EgLSBSMjETMBEG
|
||||
A1UEChMKR2xvYmFsU2lnbjETMBEGA1UEAxMKR2xvYmFsU2lnbjCCASIwDQYJKoZI
|
||||
hvcNAQEBBQADggEPADCCAQoCggEBAKbPJA6+Lm8omUVCxKs+IVSbC9N/hHD6ErPL
|
||||
v4dfxn+G07IwXNb9rfF73OX4YJYJkhD10FPe+3t+c4isUoh7SqbKSaZeqKeMWhG8
|
||||
eoLrvozps6yWJQeXSpkqBy+0Hne/ig+1AnwblrjFuTosvNYSuetZfeLQBoZfXklq
|
||||
tTleiDTsvHgMCJiEbKjNS7SgfQx5TfC4LcshytVsW33hoCmEofnTlEnLJGKRILzd
|
||||
C9XZzPnqJworc5HGnRusyMvo4KD0L5CLTfuwNhv2GXqF4G3yYROIXJ/gkwpRl4pa
|
||||
zq+r1feqCapgvdzZX99yqWATXgAByUr6P6TqBwMhAo6CygPCm48CAwEAAaOBnDCB
|
||||
mTAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUm+IH
|
||||
V2ccHsBqBt5ZtJot39wZhi4wNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2NybC5n
|
||||
bG9iYWxzaWduLm5ldC9yb290LXIyLmNybDAfBgNVHSMEGDAWgBSb4gdXZxwewGoG
|
||||
3lm0mi3f3BmGLjANBgkqhkiG9w0BAQUFAAOCAQEAmYFThxxol4aR7OBKuEQLq4Gs
|
||||
J0/WwbgcQ3izDJr86iw8bmEbTUsp9Z8FHSbBuOmDAGJFtqkIk7mpM0sYmsL4h4hO
|
||||
291xNBrBVNpGP+DTKqttVCL1OmLNIG+6KYnX3ZHu01yiPqFbQfXf5WRDLenVOavS
|
||||
ot+3i9DAgBkcRcAtjOj4LaR0VknFBbVPFd5uRHg5h6h+u/N5GJG79G+dwfCMNYxd
|
||||
AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7
|
||||
TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg==
|
||||
-----END CERTIFICATE-----\n`;
|
||||
|
||||
function base64URLDecode(source: string) {
|
||||
return Buffer.from(source.replace(/\-/g, '+').replace(/_/g, '/'), 'base64');
|
||||
}
|
||||
|
||||
function getCertSubject(certificate: string) {
|
||||
const subjectCert = new jsrsasign.X509();
|
||||
subjectCert.readCertPEM(certificate);
|
||||
|
||||
const subjectString = subjectCert.getSubjectString();
|
||||
const subjectFields = subjectString.slice(1).split('/');
|
||||
|
||||
const fields = {} as Record<string, string>;
|
||||
for (const field of subjectFields) {
|
||||
const eqIndex = field.indexOf('=');
|
||||
fields[field.substring(0, eqIndex)] = field.substring(eqIndex + 1);
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
function verifyCertificateChain(certificates: string[]) {
|
||||
let valid = true;
|
||||
|
||||
for (let i = 0; i < certificates.length; i++) {
|
||||
const Cert = certificates[i];
|
||||
const certificate = new jsrsasign.X509();
|
||||
certificate.readCertPEM(Cert);
|
||||
|
||||
const CACert = i + 1 >= certificates.length ? Cert : certificates[i + 1];
|
||||
|
||||
const certStruct = jsrsasign.ASN1HEX.getTLVbyList(certificate.hex!, 0, [0]);
|
||||
const algorithm = certificate.getSignatureAlgorithmField();
|
||||
const signatureHex = certificate.getSignatureValueHex();
|
||||
|
||||
// Verify against CA
|
||||
const Signature = new jsrsasign.KJUR.crypto.Signature({alg: algorithm});
|
||||
Signature.init(CACert);
|
||||
Signature.updateHex(certStruct);
|
||||
valid = valid && !!Signature.verify(signatureHex); // true if CA signed the certificate
|
||||
}
|
||||
|
||||
return valid;
|
||||
}
|
||||
|
||||
function PEMString(pemBuffer: Buffer, type = 'CERTIFICATE') {
|
||||
if (pemBuffer.length == 65 && pemBuffer[0] == 0x04) {
|
||||
pemBuffer = Buffer.concat([PEM_PRELUDE, pemBuffer], 91);
|
||||
type = 'PUBLIC KEY';
|
||||
}
|
||||
const cert = pemBuffer.toString('base64');
|
||||
|
||||
const keyParts = [];
|
||||
const max = Math.ceil(cert.length / 64);
|
||||
let start = 0;
|
||||
for (let i = 0; i < max; i++) {
|
||||
keyParts.push(cert.substring(start, start + 64));
|
||||
start += 64;
|
||||
}
|
||||
|
||||
return (
|
||||
`-----BEGIN ${type}-----\n` +
|
||||
keyParts.join('\n') +
|
||||
`\n-----END ${type}-----\n`
|
||||
);
|
||||
}
|
||||
|
||||
export function hash(data: Buffer) {
|
||||
return crypto
|
||||
.createHash('sha256')
|
||||
.update(data)
|
||||
.digest();
|
||||
}
|
||||
|
||||
export function verifyLogin({
|
||||
publicKey,
|
||||
authenticatorData,
|
||||
clientDataJSON,
|
||||
clientData,
|
||||
signature,
|
||||
challenge
|
||||
}: {
|
||||
publicKey: Buffer,
|
||||
authenticatorData: Buffer,
|
||||
clientDataJSON: Buffer,
|
||||
clientData: any,
|
||||
signature: Buffer,
|
||||
challenge: string
|
||||
}) {
|
||||
if (clientData.type != 'webauthn.get') {
|
||||
throw new Error('type is not webauthn.get');
|
||||
}
|
||||
|
||||
if (hash(clientData.challenge).toString('hex') != challenge) {
|
||||
throw new Error('challenge mismatch');
|
||||
}
|
||||
if (clientData.origin != config.scheme + '://' + config.host) {
|
||||
throw new Error('origin mismatch');
|
||||
}
|
||||
|
||||
const verificationData = Buffer.concat(
|
||||
[authenticatorData, hash(clientDataJSON)],
|
||||
32 + authenticatorData.length
|
||||
);
|
||||
|
||||
return crypto
|
||||
.createVerify('SHA256')
|
||||
.update(verificationData)
|
||||
.verify(PEMString(publicKey), signature);
|
||||
}
|
||||
|
||||
export const procedures = {
|
||||
none: {
|
||||
verify({publicKey}: {publicKey: Map<number, Buffer>}) {
|
||||
const negTwo = publicKey.get(-2);
|
||||
|
||||
if (!negTwo || negTwo.length != 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length != 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
const publicKeyU2F = Buffer.concat(
|
||||
[ECC_PRELUDE, negTwo, negThree],
|
||||
1 + 32 + 32
|
||||
);
|
||||
|
||||
return {
|
||||
publicKey: publicKeyU2F,
|
||||
valid: true
|
||||
};
|
||||
}
|
||||
},
|
||||
'android-key': {
|
||||
verify({
|
||||
attStmt,
|
||||
authenticatorData,
|
||||
clientDataHash,
|
||||
publicKey,
|
||||
rpIdHash,
|
||||
credentialId
|
||||
}: {
|
||||
attStmt: any,
|
||||
authenticatorData: Buffer,
|
||||
clientDataHash: Buffer,
|
||||
publicKey: Map<number, any>;
|
||||
rpIdHash: Buffer,
|
||||
credentialId: Buffer,
|
||||
}) {
|
||||
if (attStmt.alg != -7) {
|
||||
throw new Error('alg mismatch');
|
||||
}
|
||||
|
||||
const verificationData = Buffer.concat([
|
||||
authenticatorData,
|
||||
clientDataHash
|
||||
]);
|
||||
|
||||
const attCert: Buffer = attStmt.x5c[0];
|
||||
|
||||
const negTwo = publicKey.get(-2);
|
||||
|
||||
if (!negTwo || negTwo.length != 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length != 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
const publicKeyData = Buffer.concat(
|
||||
[ECC_PRELUDE, negTwo, negThree],
|
||||
1 + 32 + 32
|
||||
);
|
||||
|
||||
if (!attCert.equals(publicKeyData)) {
|
||||
throw new Error('public key mismatch');
|
||||
}
|
||||
|
||||
const isValid = crypto
|
||||
.createVerify('SHA256')
|
||||
.update(verificationData)
|
||||
.verify(PEMString(attCert), attStmt.sig);
|
||||
|
||||
// TODO: Check 'attestationChallenge' field in extension of cert matches hash(clientDataJSON)
|
||||
|
||||
return {
|
||||
valid: isValid,
|
||||
publicKey: publicKeyData
|
||||
};
|
||||
}
|
||||
},
|
||||
// what a stupid attestation
|
||||
'android-safetynet': {
|
||||
verify({
|
||||
attStmt,
|
||||
authenticatorData,
|
||||
clientDataHash,
|
||||
publicKey,
|
||||
rpIdHash,
|
||||
credentialId
|
||||
}: {
|
||||
attStmt: any,
|
||||
authenticatorData: Buffer,
|
||||
clientDataHash: Buffer,
|
||||
publicKey: Map<number, any>;
|
||||
rpIdHash: Buffer,
|
||||
credentialId: Buffer,
|
||||
}) {
|
||||
const verificationData = hash(
|
||||
Buffer.concat([authenticatorData, clientDataHash])
|
||||
);
|
||||
|
||||
const jwsParts = attStmt.response.toString('utf-8').split('.');
|
||||
|
||||
const header = JSON.parse(base64URLDecode(jwsParts[0]).toString('utf-8'));
|
||||
const response = JSON.parse(
|
||||
base64URLDecode(jwsParts[1]).toString('utf-8')
|
||||
);
|
||||
const signature = jwsParts[2];
|
||||
|
||||
if (!verificationData.equals(Buffer.from(response.nonce, 'base64'))) {
|
||||
throw new Error('invalid nonce');
|
||||
}
|
||||
|
||||
const certificateChain = header.x5c
|
||||
.map((key: any) => PEMString(key))
|
||||
.concat([GSR2]);
|
||||
|
||||
if (getCertSubject(certificateChain[0]).CN != 'attest.android.com') {
|
||||
throw new Error('invalid common name');
|
||||
}
|
||||
|
||||
if (!verifyCertificateChain(certificateChain)) {
|
||||
throw new Error('Invalid certificate chain!');
|
||||
}
|
||||
|
||||
const signatureBase = Buffer.from(
|
||||
jwsParts[0] + '.' + jwsParts[1],
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
const valid = crypto
|
||||
.createVerify('sha256')
|
||||
.update(signatureBase)
|
||||
.verify(certificateChain[0], base64URLDecode(signature));
|
||||
|
||||
const negTwo = publicKey.get(-2);
|
||||
|
||||
if (!negTwo || negTwo.length != 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length != 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
const publicKeyData = Buffer.concat(
|
||||
[ECC_PRELUDE, negTwo, negThree],
|
||||
1 + 32 + 32
|
||||
);
|
||||
return {
|
||||
valid,
|
||||
publicKey: publicKeyData
|
||||
};
|
||||
}
|
||||
},
|
||||
packed: {
|
||||
verify({
|
||||
attStmt,
|
||||
authenticatorData,
|
||||
clientDataHash,
|
||||
publicKey,
|
||||
rpIdHash,
|
||||
credentialId
|
||||
}: {
|
||||
attStmt: any,
|
||||
authenticatorData: Buffer,
|
||||
clientDataHash: Buffer,
|
||||
publicKey: Map<number, any>;
|
||||
rpIdHash: Buffer,
|
||||
credentialId: Buffer,
|
||||
}) {
|
||||
const verificationData = Buffer.concat([
|
||||
authenticatorData,
|
||||
clientDataHash
|
||||
]);
|
||||
|
||||
if (attStmt.x5c) {
|
||||
const attCert = attStmt.x5c[0];
|
||||
|
||||
const validSignature = crypto
|
||||
.createVerify('SHA256')
|
||||
.update(verificationData)
|
||||
.verify(PEMString(attCert), attStmt.sig);
|
||||
|
||||
const negTwo = publicKey.get(-2);
|
||||
|
||||
if (!negTwo || negTwo.length != 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
const negThree = publicKey.get(-3);
|
||||
if (!negThree || negThree.length != 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
const publicKeyData = Buffer.concat(
|
||||
[ECC_PRELUDE, negTwo, negThree],
|
||||
1 + 32 + 32
|
||||
);
|
||||
|
||||
return {
|
||||
valid: validSignature,
|
||||
publicKey: publicKeyData
|
||||
};
|
||||
} else if (attStmt.ecdaaKeyId) {
|
||||
// https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-ecdaa-algorithm-v2.0-id-20180227.html#ecdaa-verify-operation
|
||||
throw new Error('ECDAA-Verify is not supported');
|
||||
} else {
|
||||
if (attStmt.alg != -7) throw new Error('alg mismatch');
|
||||
|
||||
throw new Error('self attestation is not supported');
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
'fido-u2f': {
|
||||
verify({
|
||||
attStmt,
|
||||
authenticatorData,
|
||||
clientDataHash,
|
||||
publicKey,
|
||||
rpIdHash,
|
||||
credentialId
|
||||
}: {
|
||||
attStmt: any,
|
||||
authenticatorData: Buffer,
|
||||
clientDataHash: Buffer,
|
||||
publicKey: Map<number, any>,
|
||||
rpIdHash: Buffer,
|
||||
credentialId: Buffer
|
||||
}) {
|
||||
const x5c: Buffer[] = attStmt.x5c;
|
||||
if (x5c.length != 1) {
|
||||
throw new Error('x5c length does not match expectation');
|
||||
}
|
||||
|
||||
const attCert = x5c[0];
|
||||
|
||||
// TODO: make sure attCert is an Elliptic Curve (EC) public key over the P-256 curve
|
||||
|
||||
const negTwo: Buffer = publicKey.get(-2);
|
||||
|
||||
if (!negTwo || negTwo.length != 32) {
|
||||
throw new Error('invalid or no -2 key given');
|
||||
}
|
||||
const negThree: Buffer = publicKey.get(-3);
|
||||
if (!negThree || negThree.length != 32) {
|
||||
throw new Error('invalid or no -3 key given');
|
||||
}
|
||||
|
||||
const publicKeyU2F = Buffer.concat(
|
||||
[ECC_PRELUDE, negTwo, negThree],
|
||||
1 + 32 + 32
|
||||
);
|
||||
|
||||
const verificationData = Buffer.concat([
|
||||
NULL_BYTE,
|
||||
rpIdHash,
|
||||
clientDataHash,
|
||||
credentialId,
|
||||
publicKeyU2F
|
||||
]);
|
||||
|
||||
const validSignature = crypto
|
||||
.createVerify('SHA256')
|
||||
.update(verificationData)
|
||||
.verify(PEMString(attCert), attStmt.sig);
|
||||
|
||||
return {
|
||||
valid: validSignature,
|
||||
publicKey: publicKeyU2F
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,7 +1,5 @@
|
|||
import { Not, IsNull } from 'typeorm';
|
||||
import define from '../../../define';
|
||||
import { deleteFile } from '../../../../../services/drive/delete-file';
|
||||
import { DriveFiles } from '../../../../../models';
|
||||
import { createCleanRemoteFilesJob } from '../../../../../queue';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
@ -11,12 +9,5 @@ export const meta = {
|
|||
};
|
||||
|
||||
export default define(meta, async (ps, me) => {
|
||||
const files = await DriveFiles.find({
|
||||
userHost: Not(IsNull()),
|
||||
isLink: false,
|
||||
});
|
||||
|
||||
for (const file of files) {
|
||||
deleteFile(file, true);
|
||||
}
|
||||
createCleanRemoteFilesJob();
|
||||
});
|
||||
|
|
26
src/server/api/endpoints/admin/send-email.ts
Normal file
26
src/server/api/endpoints/admin/send-email.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { sendEmail } from '../../../../services/send-email';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
to: {
|
||||
validator: $.str,
|
||||
},
|
||||
subject: {
|
||||
validator: $.str,
|
||||
},
|
||||
text: {
|
||||
validator: $.str,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps) => {
|
||||
await sendEmail(ps.to, ps.subject, ps.text);
|
||||
});
|
33
src/server/api/endpoints/admin/vacuum.ts
Normal file
33
src/server/api/endpoints/admin/vacuum.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { getConnection } from 'typeorm';
|
||||
|
||||
export const meta = {
|
||||
tags: ['admin'],
|
||||
|
||||
requireCredential: true,
|
||||
requireModerator: true,
|
||||
|
||||
params: {
|
||||
full: {
|
||||
validator: $.bool,
|
||||
},
|
||||
analyze: {
|
||||
validator: $.bool,
|
||||
},
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps) => {
|
||||
const params: string[] = [];
|
||||
|
||||
if (ps.full) {
|
||||
params.push('FULL');
|
||||
}
|
||||
|
||||
if (ps.analyze) {
|
||||
params.push('ANALYZE');
|
||||
}
|
||||
|
||||
getConnection().query('VACUUM ' + params.join(' '));
|
||||
});
|
|
@ -10,7 +10,7 @@ import { Users, Notes } from '../../../../models';
|
|||
import { Note } from '../../../../models/entities/note';
|
||||
import { User } from '../../../../models/entities/user';
|
||||
import { fetchMeta } from '../../../../misc/fetch-meta';
|
||||
import { validActor } from '../../../../remote/activitypub/type';
|
||||
import { validActor, validPost } from '../../../../remote/activitypub/type';
|
||||
|
||||
export const meta = {
|
||||
tags: ['federation'],
|
||||
|
@ -145,7 +145,7 @@ async function fetchAny(uri: string) {
|
|||
};
|
||||
}
|
||||
|
||||
if (['Note', 'Question', 'Article'].includes(object.type)) {
|
||||
if (validPost.includes(object.type)) {
|
||||
const note = await createNote(object.id, undefined, true);
|
||||
return {
|
||||
type: 'Note',
|
||||
|
|
|
@ -4,7 +4,6 @@ import define from '../../define';
|
|||
import { Apps } from '../../../../models';
|
||||
import { genId } from '../../../../misc/gen-id';
|
||||
import { unique } from '../../../../prelude/array';
|
||||
import { types, bool } from '../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
tags: ['app'],
|
||||
|
@ -53,8 +52,8 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'App',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -3,7 +3,6 @@ import { ID } from '../../../../misc/cafy-id';
|
|||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Apps } from '../../../../models';
|
||||
import { types, bool } from '../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
tags: ['app'],
|
||||
|
@ -15,8 +14,8 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'App',
|
||||
},
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@ import define from '../../../define';
|
|||
import { ApiError } from '../../../error';
|
||||
import { Apps, AuthSessions } from '../../../../../models';
|
||||
import { genId } from '../../../../../misc/gen-id';
|
||||
import { types, bool } from '../../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
tags: ['auth'],
|
||||
|
@ -28,17 +27,17 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
token: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'セッションのトークン'
|
||||
},
|
||||
url: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
format: 'url',
|
||||
description: 'セッションのURL'
|
||||
},
|
||||
|
|
|
@ -3,7 +3,6 @@ import define from '../../../define';
|
|||
import { ApiError } from '../../../error';
|
||||
import { Apps, AuthSessions, AccessTokens, Users } from '../../../../../models';
|
||||
import { ensure } from '../../../../../prelude/ensure';
|
||||
import { types, bool } from '../../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
tags: ['auth'],
|
||||
|
@ -29,18 +28,18 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
accessToken: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
description: 'ユーザーのアクセストークン',
|
||||
},
|
||||
|
||||
user: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'User',
|
||||
description: '認証したユーザー'
|
||||
},
|
||||
|
|
|
@ -3,7 +3,6 @@ import { ID } from '../../../../misc/cafy-id';
|
|||
import define from '../../define';
|
||||
import { Blockings } from '../../../../models';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { types, bool } from '../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -33,11 +32,11 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Blocking',
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import define from '../define';
|
||||
import { fetchMeta } from '../../../misc/fetch-meta';
|
||||
import { DriveFiles } from '../../../models';
|
||||
import { types, bool } from '../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -16,16 +15,16 @@ export const meta = {
|
|||
kind: 'read:drive',
|
||||
|
||||
res: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
capacity: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'number' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
},
|
||||
usage: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'number' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ import { ID } from '../../../../misc/cafy-id';
|
|||
import define from '../../define';
|
||||
import { DriveFiles } from '../../../../models';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { types, bool } from '../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -42,11 +41,11 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'DriveFile',
|
||||
}
|
||||
},
|
||||
|
|
|
@ -3,7 +3,6 @@ import { ID } from '../../../../../misc/cafy-id';
|
|||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { DriveFiles, Notes } from '../../../../../models';
|
||||
import { types, bool } from '../../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
stability: 'stable',
|
||||
|
@ -30,11 +29,11 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Note',
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { DriveFiles } from '../../../../../models';
|
||||
import { types, bool } from '../../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -25,8 +24,8 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.boolean,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'boolean' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ import define from '../../../define';
|
|||
import { apiLogger } from '../../../logger';
|
||||
import { ApiError } from '../../../error';
|
||||
import { DriveFiles } from '../../../../../models';
|
||||
import { types, bool } from '../../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -57,8 +56,8 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'DriveFile',
|
||||
},
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../../define';
|
||||
import { DriveFiles } from '../../../../../models';
|
||||
import { types, bool } from '../../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -24,11 +23,11 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'DriveFile',
|
||||
}
|
||||
},
|
||||
|
|
|
@ -2,7 +2,6 @@ import $ from 'cafy';
|
|||
import { ID } from '../../../../../misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { DriveFiles } from '../../../../../models';
|
||||
import { types, bool } from '../../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
@ -26,11 +25,11 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'DriveFile',
|
||||
}
|
||||
},
|
||||
|
|
|
@ -4,7 +4,6 @@ import define from '../../../define';
|
|||
import { ApiError } from '../../../error';
|
||||
import { DriveFile } from '../../../../../models/entities/drive-file';
|
||||
import { DriveFiles } from '../../../../../models';
|
||||
import { types, bool } from '../../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
stability: 'stable',
|
||||
|
@ -39,8 +38,8 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'DriveFile',
|
||||
},
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import { ID } from '../../../../misc/cafy-id';
|
|||
import define from '../../define';
|
||||
import { DriveFolders } from '../../../../models';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { types, bool } from '../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -38,11 +37,11 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'DriveFolder',
|
||||
}
|
||||
},
|
||||
|
|
|
@ -2,7 +2,6 @@ import $ from 'cafy';
|
|||
import { ID } from '../../../../../misc/cafy-id';
|
||||
import define from '../../../define';
|
||||
import { DriveFolders } from '../../../../../models';
|
||||
import { types, bool } from '../../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
tags: ['drive'],
|
||||
|
@ -26,11 +25,11 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'DriveFolder',
|
||||
}
|
||||
},
|
||||
|
|
|
@ -3,7 +3,6 @@ import { ID } from '../../../../../misc/cafy-id';
|
|||
import define from '../../../define';
|
||||
import { ApiError } from '../../../error';
|
||||
import { DriveFolders } from '../../../../../models';
|
||||
import { types, bool } from '../../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
stability: 'stable',
|
||||
|
@ -30,8 +29,8 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'DriveFolder',
|
||||
},
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@ import { ID } from '../../../../misc/cafy-id';
|
|||
import define from '../../define';
|
||||
import { DriveFiles } from '../../../../models';
|
||||
import { makePaginationQuery } from '../../common/make-pagination-query';
|
||||
import { types, bool } from '../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
tags: ['drive'],
|
||||
|
@ -32,11 +31,11 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'DriveFile',
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { Hashtags } from '../../../../models';
|
||||
import { types, bool } from '../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
tags: ['hashtags'],
|
||||
|
@ -48,11 +47,11 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Hashtag',
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { Hashtags } from '../../../../models';
|
||||
import { types, bool } from '../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -38,11 +37,11 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
|
@ -2,7 +2,6 @@ import $ from 'cafy';
|
|||
import define from '../../define';
|
||||
import { ApiError } from '../../error';
|
||||
import { Hashtags } from '../../../../models';
|
||||
import { types, bool } from '../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
desc: {
|
||||
|
@ -24,8 +23,8 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'Hashtag',
|
||||
},
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import define from '../../define';
|
|||
import { fetchMeta } from '../../../../misc/fetch-meta';
|
||||
import { Notes } from '../../../../models';
|
||||
import { Note } from '../../../../models/entities/note';
|
||||
import { types, bool } from '../../../../misc/schema';
|
||||
|
||||
/*
|
||||
トレンドに載るためには「『直近a分間のユニーク投稿数が今からa分前~今からb分前の間のユニーク投稿数のn倍以上』のハッシュタグの上位5位以内に入る」ことが必要
|
||||
|
@ -24,27 +23,27 @@ export const meta = {
|
|||
requireCredential: false,
|
||||
|
||||
res: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
properties: {
|
||||
tag: {
|
||||
type: types.string,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'string' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
},
|
||||
chart: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'number' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
}
|
||||
},
|
||||
usersCount: {
|
||||
type: types.number,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'number' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import $ from 'cafy';
|
||||
import define from '../../define';
|
||||
import { Users } from '../../../../models';
|
||||
import { types, bool } from '../../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: false,
|
||||
|
@ -48,11 +47,11 @@ export const meta = {
|
|||
},
|
||||
|
||||
res: {
|
||||
type: types.array,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'array' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
items: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'User',
|
||||
}
|
||||
},
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import define from '../define';
|
||||
import { Users } from '../../../models';
|
||||
import { types, bool } from '../../../misc/schema';
|
||||
|
||||
export const meta = {
|
||||
stability: 'stable',
|
||||
|
@ -16,8 +15,8 @@ export const meta = {
|
|||
params: {},
|
||||
|
||||
res: {
|
||||
type: types.object,
|
||||
optional: bool.false, nullable: bool.false,
|
||||
type: 'object' as const,
|
||||
optional: false as const, nullable: false as const,
|
||||
ref: 'User',
|
||||
},
|
||||
};
|
||||
|
|
67
src/server/api/endpoints/i/2fa/getkeys.ts
Normal file
67
src/server/api/endpoints/i/2fa/getkeys.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
import $ from 'cafy';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import * as crypto from 'crypto';
|
||||
import define from '../../../define';
|
||||
import { UserProfiles, UserSecurityKeys, AttestationChallenges } from '../../../../../models';
|
||||
import { ensure } from '../../../../../prelude/ensure';
|
||||
import { promisify } from 'util';
|
||||
import { hash } from '../../../2fa';
|
||||
import { genId } from '../../../../../misc/gen-id';
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
secure: true,
|
||||
|
||||
params: {
|
||||
password: {
|
||||
validator: $.str
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const randomBytes = promisify(crypto.randomBytes);
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const profile = await UserProfiles.findOne(user.id).then(ensure);
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
|
||||
if (!same) {
|
||||
throw new Error('incorrect password');
|
||||
}
|
||||
|
||||
const keys = await UserSecurityKeys.find({
|
||||
userId: user.id
|
||||
});
|
||||
|
||||
if (keys.length === 0) {
|
||||
throw new Error('no keys found');
|
||||
}
|
||||
|
||||
// 32 byte challenge
|
||||
const entropy = await randomBytes(32);
|
||||
const challenge = entropy.toString('base64')
|
||||
.replace(/=/g, '')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
|
||||
const challengeId = genId();
|
||||
|
||||
await AttestationChallenges.save({
|
||||
userId: user.id,
|
||||
id: challengeId,
|
||||
challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
|
||||
createdAt: new Date(),
|
||||
registrationChallenge: false
|
||||
});
|
||||
|
||||
return {
|
||||
challenge,
|
||||
challengeId,
|
||||
securityKeys: keys.map(key => ({
|
||||
id: key.id
|
||||
}))
|
||||
};
|
||||
});
|
151
src/server/api/endpoints/i/2fa/key-done.ts
Normal file
151
src/server/api/endpoints/i/2fa/key-done.ts
Normal file
|
@ -0,0 +1,151 @@
|
|||
import $ from 'cafy';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { promisify } from 'util';
|
||||
import * as cbor from 'cbor';
|
||||
import define from '../../../define';
|
||||
import {
|
||||
UserProfiles,
|
||||
UserSecurityKeys,
|
||||
AttestationChallenges,
|
||||
Users
|
||||
} from '../../../../../models';
|
||||
import { ensure } from '../../../../../prelude/ensure';
|
||||
import config from '../../../../../config';
|
||||
import { procedures, hash } from '../../../2fa';
|
||||
import { publishMainStream } from '../../../../../services/stream';
|
||||
|
||||
const cborDecodeFirst = promisify(cbor.decodeFirst) as any;
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
secure: true,
|
||||
|
||||
params: {
|
||||
clientDataJSON: {
|
||||
validator: $.str
|
||||
},
|
||||
attestationObject: {
|
||||
validator: $.str
|
||||
},
|
||||
password: {
|
||||
validator: $.str
|
||||
},
|
||||
challengeId: {
|
||||
validator: $.str
|
||||
},
|
||||
name: {
|
||||
validator: $.str
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const rpIdHashReal = hash(Buffer.from(config.hostname, 'utf-8'));
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const profile = await UserProfiles.findOne(user.id).then(ensure);
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
|
||||
if (!same) {
|
||||
throw new Error('incorrect password');
|
||||
}
|
||||
|
||||
if (!profile.twoFactorEnabled) {
|
||||
throw new Error('2fa not enabled');
|
||||
}
|
||||
|
||||
const clientData = JSON.parse(ps.clientDataJSON);
|
||||
|
||||
if (clientData.type != 'webauthn.create') {
|
||||
throw new Error('not a creation attestation');
|
||||
}
|
||||
if (clientData.origin != config.scheme + '://' + config.host) {
|
||||
throw new Error('origin mismatch');
|
||||
}
|
||||
|
||||
const clientDataJSONHash = hash(Buffer.from(ps.clientDataJSON, 'utf-8'));
|
||||
|
||||
const attestation = await cborDecodeFirst(ps.attestationObject);
|
||||
|
||||
const rpIdHash = attestation.authData.slice(0, 32);
|
||||
if (!rpIdHashReal.equals(rpIdHash)) {
|
||||
throw new Error('rpIdHash mismatch');
|
||||
}
|
||||
|
||||
const flags = attestation.authData[32];
|
||||
|
||||
// tslint:disable-next-line:no-bitwise
|
||||
if (!(flags & 1)) {
|
||||
throw new Error('user not present');
|
||||
}
|
||||
|
||||
const authData = Buffer.from(attestation.authData);
|
||||
const credentialIdLength = authData.readUInt16BE(53);
|
||||
const credentialId = authData.slice(55, 55 + credentialIdLength);
|
||||
const publicKeyData = authData.slice(55 + credentialIdLength);
|
||||
const publicKey: Map<number, any> = await cborDecodeFirst(publicKeyData);
|
||||
if (publicKey.get(3) != -7) {
|
||||
throw new Error('alg mismatch');
|
||||
}
|
||||
|
||||
if (!(procedures as any)[attestation.fmt]) {
|
||||
throw new Error('unsupported fmt');
|
||||
}
|
||||
|
||||
const verificationData = (procedures as any)[attestation.fmt].verify({
|
||||
attStmt: attestation.attStmt,
|
||||
authenticatorData: authData,
|
||||
clientDataHash: clientDataJSONHash,
|
||||
credentialId,
|
||||
publicKey,
|
||||
rpIdHash
|
||||
});
|
||||
if (!verificationData.valid) throw new Error('signature invalid');
|
||||
|
||||
const attestationChallenge = await AttestationChallenges.findOne({
|
||||
userId: user.id,
|
||||
id: ps.challengeId,
|
||||
registrationChallenge: true,
|
||||
challenge: hash(clientData.challenge).toString('hex')
|
||||
});
|
||||
|
||||
if (!attestationChallenge) {
|
||||
throw new Error('non-existent challenge');
|
||||
}
|
||||
|
||||
await AttestationChallenges.delete({
|
||||
userId: user.id,
|
||||
id: ps.challengeId
|
||||
});
|
||||
|
||||
// Expired challenge (> 5min old)
|
||||
if (
|
||||
new Date().getTime() - attestationChallenge.createdAt.getTime() >=
|
||||
5 * 60 * 1000
|
||||
) {
|
||||
throw new Error('expired challenge');
|
||||
}
|
||||
|
||||
const credentialIdString = credentialId.toString('hex');
|
||||
|
||||
await UserSecurityKeys.save({
|
||||
userId: user.id,
|
||||
id: credentialIdString,
|
||||
lastUsed: new Date(),
|
||||
name: ps.name,
|
||||
publicKey: verificationData.publicKey.toString('hex')
|
||||
});
|
||||
|
||||
// Publish meUpdated event
|
||||
publishMainStream(user.id, 'meUpdated', await Users.pack(user.id, user, {
|
||||
detail: true,
|
||||
includeSecrets: true
|
||||
}));
|
||||
|
||||
return {
|
||||
id: credentialIdString,
|
||||
name: ps.name
|
||||
};
|
||||
});
|
60
src/server/api/endpoints/i/2fa/register-key.ts
Normal file
60
src/server/api/endpoints/i/2fa/register-key.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
import $ from 'cafy';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import define from '../../../define';
|
||||
import { UserProfiles, AttestationChallenges } from '../../../../../models';
|
||||
import { ensure } from '../../../../../prelude/ensure';
|
||||
import { promisify } from 'util';
|
||||
import * as crypto from 'crypto';
|
||||
import { genId } from '../../../../../misc/gen-id';
|
||||
import { hash } from '../../../2fa';
|
||||
|
||||
const randomBytes = promisify(crypto.randomBytes);
|
||||
|
||||
export const meta = {
|
||||
requireCredential: true,
|
||||
|
||||
secure: true,
|
||||
|
||||
params: {
|
||||
password: {
|
||||
validator: $.str
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default define(meta, async (ps, user) => {
|
||||
const profile = await UserProfiles.findOne(user.id).then(ensure);
|
||||
|
||||
// Compare password
|
||||
const same = await bcrypt.compare(ps.password, profile.password!);
|
||||
|
||||
if (!same) {
|
||||
throw new Error('incorrect password');
|
||||
}
|
||||
|
||||
if (!profile.twoFactorEnabled) {
|
||||
throw new Error('2fa not enabled');
|
||||
}
|
||||
|
||||
// 32 byte challenge
|
||||
const entropy = await randomBytes(32);
|
||||
const challenge = entropy.toString('base64')
|
||||
.replace(/=/g, '')
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_');
|
||||
|
||||
const challengeId = genId();
|
||||
|
||||
await AttestationChallenges.save({
|
||||
userId: user.id,
|
||||
id: challengeId,
|
||||
challenge: hash(Buffer.from(challenge, 'utf-8')).toString('hex'),
|
||||
createdAt: new Date(),
|
||||
registrationChallenge: true
|
||||
});
|
||||
|
||||
return {
|
||||
challengeId,
|
||||
challenge
|
||||
};
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue