diff --git a/src/lib/app.ts b/src/lib/app.ts index fbe1c0738..d4eaa8e90 100644 --- a/src/lib/app.ts +++ b/src/lib/app.ts @@ -25,7 +25,7 @@ import { errors } from './errors' import { logger } from './logger' import { Revision, sequelize } from './models' import { realtime } from './realtime' -import { handleTermSignals } from './utils' +import { handleTermSignals } from './utils/functions' import { AuthRouter, BaseRouter, HistoryRouter, ImageRouter, NoteRouter, StatusRouter, UserRouter } from './web/' import { tooBusy, checkURI, redirectWithoutTrailingSlashes, codiMDVersion } from './web/middleware' diff --git a/src/lib/models/note.ts b/src/lib/models/note.ts index 2cc56711f..c186ce108 100644 --- a/src/lib/models/note.ts +++ b/src/lib/models/note.ts @@ -31,7 +31,7 @@ import S from 'string' import { config } from '../config' import { logger } from '../logger' import ot from '../ot' -import { processData, stripNullByte } from '../utils' +import { processData, stripNullByte } from '../utils/functions' import { Author, Revision, User } from './index' const md = markdownIt() diff --git a/src/lib/models/revision.ts b/src/lib/models/revision.ts index bf0faa83e..074d5c241 100644 --- a/src/lib/models/revision.ts +++ b/src/lib/models/revision.ts @@ -4,7 +4,7 @@ import Sequelize from 'sequelize' import { BelongsTo, Column, DataType, Default, ForeignKey, Model, PrimaryKey, Table } from 'sequelize-typescript' // core import { logger } from '../logger' -import { processData, stripNullByte } from '../utils' +import { processData, stripNullByte } from '../utils/functions' import { Note } from './note' import async = require('async') import childProcess = require('child_process') diff --git a/src/lib/models/user.ts b/src/lib/models/user.ts index 2beb5a1c3..aecfbb740 100644 --- a/src/lib/models/user.ts +++ b/src/lib/models/user.ts @@ -1,4 +1,5 @@ -import { Note } from './note' +import scrypt from 'scrypt-kdf' +import { UUIDV4 } from 'sequelize' import { BeforeCreate, BeforeUpdate, @@ -12,42 +13,7 @@ import { Table, Unique } from 'sequelize-typescript' -import scrypt from 'scrypt-kdf' -import { generateAvatarURL } from '../letter-avatars' -import { logger } from '../logger' -import { UUIDV4 } from 'sequelize' - -// core - -export enum ProviderEnum { - facebook = 'facebook', - twitter = 'twitter', - github = 'github', - gitlab = 'gitlab', - dropbox = 'dropbox', - google = 'google', - ldap = 'ldap', - oauth2 = 'oauth2', - saml = 'saml', -} - -// ToDo Fix this 'any' mess -export type Profile = { - id: string; - username: string; - displayName: string; - emails: string[]; - avatarUrl: string; - profileUrl: string; - provider: ProviderEnum; - photos: { value: string }[]; -} - -export type PhotoProfile = { - name: string; - photo: string; - biggerphoto: string; -} +import { Note } from './note' @Table export class User extends Model { @@ -85,77 +51,6 @@ export class User extends Model { @HasMany(() => Note, { foreignKey: 'lastchangeuserId', constraints: false }) @HasMany(() => Note, { foreignKey: 'ownerId', constraints: false }) - static parsePhotoByProfile (profile: Profile, bigger: boolean): string { - let photo: string - switch (profile.provider) { - case ProviderEnum.facebook: - photo = 'https://graph.facebook.com/' + profile.id + '/picture' - if (bigger) { - photo += '?width=400' - } else { - photo += '?width=96' - } - break - case ProviderEnum.twitter: - photo = 'https://twitter.com/' + profile.username + '/profile_image' - if (bigger) { - photo += '?size=original' - } else { - photo += '?size=bigger' - } - break - case ProviderEnum.github: - photo = 'https://avatars.githubusercontent.com/u/' + profile.id - if (bigger) { - photo += '?s=400' - } else { - photo += '?s=96' - } - break - case ProviderEnum.gitlab: - photo = profile.avatarUrl - if (photo) { - if (bigger) { - photo = photo.replace(/(\?s=)\d*$/i, '$1400') - } else { - photo = photo.replace(/(\?s=)\d*$/i, '$196') - } - } else { - photo = generateAvatarURL(profile.username) - } - break - case ProviderEnum.dropbox: - photo = generateAvatarURL('', profile.emails[0], bigger) - break - case ProviderEnum.google: - photo = profile.photos[0].value - if (bigger) { - photo = photo.replace(/(\?sz=)\d*$/i, '$1400') - } else { - photo = photo.replace(/(\?sz=)\d*$/i, '$196') - } - break - case ProviderEnum.ldap: - photo = generateAvatarURL(profile.username, profile.emails[0], bigger) - break - case ProviderEnum.saml: - photo = generateAvatarURL(profile.username, profile.emails[0], bigger) - break - default: - photo = generateAvatarURL(profile.username) - break - } - return photo - } - - static parseProfileByEmail (email: string): PhotoProfile { - return { - name: email.substring(0, email.lastIndexOf('@')), - photo: generateAvatarURL('', email, false), - biggerphoto: generateAvatarURL('', email, true) - } - } - @BeforeUpdate @BeforeCreate static async updatePasswordHashHook (user: User): Promise { @@ -173,37 +68,7 @@ export class User extends Model { }) } - static getProfile (user: User): PhotoProfile | null { - if (!user) { - return null - } - - if (user.profile) { - return user.parseProfile(user.profile) - } else { - if (user.email) { - return User.parseProfileByEmail(user.email) - } else { - return null - } - } - } - verifyPassword (attempt: string): Promise { return scrypt.verify(Buffer.from(this.password, 'hex'), attempt) } - - parseProfile (profile: string): PhotoProfile | null { - try { - const parsedProfile: Profile = JSON.parse(profile) - return { - name: parsedProfile.displayName || parsedProfile.username, - photo: User.parsePhotoByProfile(parsedProfile, false), - biggerphoto: User.parsePhotoByProfile(parsedProfile, true) - } - } catch (err) { - logger.error(err) - return null - } - } } diff --git a/src/lib/realtime.ts b/src/lib/realtime.ts index f5eff89b1..9ba9c2626 100644 --- a/src/lib/realtime.ts +++ b/src/lib/realtime.ts @@ -11,9 +11,9 @@ import { History } from './history' import { logger } from './logger' import { Author, Note, Revision, User } from './models' import { NoteAuthorship } from './models/note' -import { PhotoProfile } from './models/user' +import { PhotoProfile } from './utils/PhotoProfile' import { EditorSocketIOServer } from './ot/editor-socketio-server' -import { mapToObject } from './utils' +import { mapToObject } from './utils/functions' export type SocketWithNoteId = Socket & { noteId: string } @@ -169,7 +169,7 @@ function updateNote (note: NoteSession, callback: (err, note) => void): void { } }).then(function (user) { if (!user) return callback(null, null) - note.lastchangeuserprofile = User.getProfile(user) + note.lastchangeuserprofile = PhotoProfile.fromUser(user) return finishUpdateNote(note, _note, callback) }).catch(function (err) { logger.error(err) @@ -626,10 +626,10 @@ function startConnection (socket: SocketWithNoteId): void { return failConnection(404, 'note not found', socket) } const owner = note.ownerId - const ownerprofile = note.owner ? User.getProfile(note.owner) : null + const ownerprofile = note.owner ? PhotoProfile.fromUser(note.owner) : null const lastchangeuser = note.lastchangeuserId - const lastchangeuserprofile = note.lastchangeuser ? User.getProfile(note.lastchangeuser) : null + const lastchangeuserprofile = note.lastchangeuser ? PhotoProfile.fromUser(note.lastchangeuser) : null const body = note.content const createtime = note.createdAt @@ -638,7 +638,7 @@ function startConnection (socket: SocketWithNoteId): void { const authors = new Map() for (const author of note.authors) { - const profile = User.getProfile(author.user) + const profile = PhotoProfile.fromUser(author.user) if (profile) { authors.set(author.userId, { userid: author.userId, @@ -767,7 +767,7 @@ setInterval(function () { function updateUserData (socket: Socket, user): void { // retrieve user data from passport if (socket.request.user && socket.request.user.logged_in) { - const profile = User.getProfile(socket.request.user) + const profile = PhotoProfile.fromUser(socket.request.user) user.photo = profile?.photo user.name = profile?.name user.userid = socket.request.user.id diff --git a/src/lib/utils/PhotoProfile.ts b/src/lib/utils/PhotoProfile.ts new file mode 100644 index 000000000..85c534dc8 --- /dev/null +++ b/src/lib/utils/PhotoProfile.ts @@ -0,0 +1,102 @@ +import { generateAvatarURL } from '../letter-avatars' +import { logger } from '../logger' +import { PassportProfile, ProviderEnum } from '../web/auth/utils' +import { User } from '../models' + +export class PhotoProfile { + name: string + photo: string + biggerphoto: string + + static fromUser (user: User): PhotoProfile | null { + if (!user) return null + if (user.profile) return PhotoProfile.fromJSON(user.profile) + if (user.email) return PhotoProfile.fromEmail(user.email) + return null + } + + private static fromJSON (jsonProfile: string): PhotoProfile | null { + try { + const parsedProfile: PassportProfile = JSON.parse(jsonProfile) + return { + name: parsedProfile.displayName || parsedProfile.username, + photo: PhotoProfile.generatePhotoURL(parsedProfile, false), + biggerphoto: PhotoProfile.generatePhotoURL(parsedProfile, true) + } + } catch (err) { + logger.error(err) + return null + } + } + + private static fromEmail (email: string): PhotoProfile { + return { + name: email.substring(0, email.lastIndexOf('@')), + photo: generateAvatarURL('', email, false), + biggerphoto: generateAvatarURL('', email, true) + } + } + + private static generatePhotoURL (profile: PassportProfile, bigger: boolean): string { + let photo: string + switch (profile.provider) { + case ProviderEnum.facebook: + photo = 'https://graph.facebook.com/' + profile.id + '/picture' + if (bigger) { + photo += '?width=400' + } else { + photo += '?width=96' + } + break + case ProviderEnum.twitter: + photo = 'https://twitter.com/' + profile.username + '/profile_image' + if (bigger) { + photo += '?size=original' + } else { + photo += '?size=bigger' + } + break + case ProviderEnum.github: + photo = 'https://avatars.githubusercontent.com/u/' + profile.id + if (bigger) { + photo += '?s=400' + } else { + photo += '?s=96' + } + break + case ProviderEnum.gitlab: + photo = profile.avatarUrl + if (photo) { + if (bigger) { + photo = photo.replace(/(\?s=)\d*$/i, '$1400') + } else { + photo = photo.replace(/(\?s=)\d*$/i, '$196') + } + } else { + photo = generateAvatarURL(profile.username) + } + break + case ProviderEnum.dropbox: + photo = generateAvatarURL('', profile.emails[0], bigger) + break + case ProviderEnum.google: + photo = profile.photos[0].value + if (bigger) { + photo = photo.replace(/(\?sz=)\d*$/i, '$1400') + } else { + photo = photo.replace(/(\?sz=)\d*$/i, '$196') + } + break + case ProviderEnum.ldap: + photo = generateAvatarURL(profile.username, profile.emails[0], bigger) + break + case ProviderEnum.saml: + photo = generateAvatarURL(profile.username, profile.emails[0], bigger) + break + default: + photo = generateAvatarURL(profile.username) + break + } + return photo + } +} diff --git a/src/lib/utils.ts b/src/lib/utils/functions.ts similarity index 93% rename from src/lib/utils.ts rename to src/lib/utils/functions.ts index 8509a1cef..45ff70a59 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils/functions.ts @@ -1,8 +1,8 @@ import fs from 'fs' -import { config } from './config' -import { logger } from './logger' -import { Revision } from './models' -import { realtime } from './realtime' +import { config } from '../config' +import { logger } from '../logger' +import { Revision } from '../models' +import { realtime } from '../realtime' /* Converts a map from string to something into a plain JS object for transmitting via a websocket diff --git a/src/lib/web/auth/oauth2/oauth2-custom-strategy.ts b/src/lib/web/auth/oauth2/oauth2-custom-strategy.ts index a6b80fcc3..a502efbb5 100644 --- a/src/lib/web/auth/oauth2/oauth2-custom-strategy.ts +++ b/src/lib/web/auth/oauth2/oauth2-custom-strategy.ts @@ -1,6 +1,6 @@ import { InternalOAuthError, Strategy as OAuth2Strategy } from 'passport-oauth2' import { config } from '../../../config' -import { Profile, ProviderEnum } from '../../../models/user' +import { PassportProfile, ProviderEnum } from '../utils' function extractProfileAttribute (data, path: string): string { // can handle stuff like `attrs[0].name` @@ -13,7 +13,7 @@ function extractProfileAttribute (data, path: string): string { return data } -function parseProfile (data): Partial { +function parseProfile (data): Partial { const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr) const displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr) const email = extractProfileAttribute(data, config.oauth2.userProfileEmailAttr) diff --git a/src/lib/web/auth/utils.ts b/src/lib/web/auth/utils.ts index 025b21478..4c957ef5c 100644 --- a/src/lib/web/auth/utils.ts +++ b/src/lib/web/auth/utils.ts @@ -1,6 +1,6 @@ import { Profile } from 'passport' -import { User } from '../../models' import { logger } from '../../logger' +import { User } from '../../models' export function passportGeneralCallback ( accessToken: string, @@ -48,3 +48,26 @@ export function passportGeneralCallback ( return done(err, undefined) }) } + +export enum ProviderEnum { + facebook = 'facebook', + twitter = 'twitter', + github = 'github', + gitlab = 'gitlab', + dropbox = 'dropbox', + google = 'google', + ldap = 'ldap', + oauth2 = 'oauth2', + saml = 'saml', +} + +export type PassportProfile = { + id: string; + username: string; + displayName: string; + emails: string[]; + avatarUrl: string; + profileUrl: string; + provider: ProviderEnum; + photos: { value: string }[]; +} diff --git a/src/lib/web/imageRouter/minio.ts b/src/lib/web/imageRouter/minio.ts index d3d3a66f0..42c113fd4 100644 --- a/src/lib/web/imageRouter/minio.ts +++ b/src/lib/web/imageRouter/minio.ts @@ -3,7 +3,7 @@ import fs from 'fs' import { Client } from 'minio' import { config } from '../../config' -import { getImageMimeType } from '../../utils' +import { getImageMimeType } from '../../utils/functions' import { logger } from '../../logger' import { UploadProvider } from './index' diff --git a/src/lib/web/note/util.ts b/src/lib/web/note/util.ts index b06a3c242..ea52f08af 100644 --- a/src/lib/web/note/util.ts +++ b/src/lib/web/note/util.ts @@ -5,7 +5,8 @@ import path from 'path' import { config } from '../../config' import { errors } from '../../errors' import { logger } from '../../logger' -import { Note, User } from '../../models' +import { Note } from '../../models' +import { PhotoProfile } from '../../utils/PhotoProfile' export function newNote (req, res: Response, body: string | null): void { let owner = null @@ -96,9 +97,9 @@ export function getPublishData (req: Request, res: Response, note, callback: (da theme: meta.slideOptions && isRevealTheme(meta.slideOptions.theme), meta: JSON.stringify(extracted.meta), owner: note.owner ? note.owner.id : null, - ownerprofile: note.owner ? User.getProfile(note.owner) : null, + ownerprofile: note.owner ? PhotoProfile.fromUser(note.owner) : null, lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null, - lastchangeuserprofile: note.lastchangeuser ? User.getProfile(note.lastchangeuser) : null, + lastchangeuserprofile: note.lastchangeuser ? PhotoProfile.fromUser(note.lastchangeuser) : null, robots: meta.robots || false, // default allow robots GA: meta.GA, disqus: meta.disqus, diff --git a/src/lib/web/userRouter.ts b/src/lib/web/userRouter.ts index c22c604f6..819573511 100644 --- a/src/lib/web/userRouter.ts +++ b/src/lib/web/userRouter.ts @@ -6,6 +6,7 @@ import { Note, User } from '../models' import { logger } from '../logger' import { generateAvatar } from '../letter-avatars' import { config } from '../config' +import { PhotoProfile } from '../utils/PhotoProfile' const UserRouter = Router() @@ -21,7 +22,7 @@ UserRouter.get('/me', function (req: Request, res: Response) { } }).then(function (user) { if (!user) { return errors.errorNotFound(res) } - const profile = User.getProfile(user) + const profile = PhotoProfile.fromUser(user) if (profile == null) { return errors.errorInternalError(res) }