Merge pull request #388 from codimd/refactor_user

Refactor User model & profile-parsing
This commit is contained in:
Sheogorath 2020-06-13 11:24:05 +02:00 committed by GitHub
commit 1945a73c11
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 152 additions and 160 deletions

View file

@ -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'

View file

@ -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()

View file

@ -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')

View file

@ -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<User> {
@ -85,77 +51,6 @@ export class User extends Model<User> {
@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<void> {
@ -173,37 +68,7 @@ export class User extends Model<User> {
})
}
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<boolean> {
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
}
}
}

View file

@ -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<string, UserSession>()
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

View file

@ -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
}
}

View file

@ -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

View file

@ -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<Profile> {
function parseProfile (data): Partial<PassportProfile> {
const username = extractProfileAttribute(data, config.oauth2.userProfileUsernameAttr)
const displayName = extractProfileAttribute(data, config.oauth2.userProfileDisplayNameAttr)
const email = extractProfileAttribute(data, config.oauth2.userProfileEmailAttr)

View file

@ -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 }[];
}

View file

@ -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'

View file

@ -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,

View file

@ -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)
}