Finished adding JSDoc for src/utils
This commit is contained in:
parent
2303c89bcf
commit
7b8c44158f
|
|
@ -20,11 +20,21 @@ const config = require('../../config.json');
|
|||
//NPM imports
|
||||
const { createChallenge, verifySolution } = require('altcha-lib');
|
||||
|
||||
//Create empty array to hold cache of spent payloades to protect against replay attacks
|
||||
/**
|
||||
* Create empty array to hold cache of spent payloads to protect against replay attacks
|
||||
*/
|
||||
const spent = [];
|
||||
//Captcha lifetime in minutes
|
||||
/**
|
||||
* Captcha lifetime in minutes
|
||||
*/
|
||||
const lifetime = 2;
|
||||
|
||||
/**
|
||||
* Generates captcha challenges to send down to the browser
|
||||
* @param {Number} difficulty - Challange Difficulty (x100K internally)
|
||||
* @param {String} uniqueSecret - Secret to salt the challange hash with
|
||||
* @returns {String} Altcha Challenge hash
|
||||
*/
|
||||
module.exports.genCaptcha = async function(difficulty = 2, uniqueSecret = ''){
|
||||
//Set altcha expiration date
|
||||
const expiration = new Date();
|
||||
|
|
@ -40,8 +50,14 @@ module.exports.genCaptcha = async function(difficulty = 2, uniqueSecret = ''){
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies completed altcha challenges handed over from the user
|
||||
* @param {String} payload - Completed Altcha Payload
|
||||
* @param {String} uniqueSecret - Server-side Unique Secret to verify payload came from server-generated challenge
|
||||
* @returns {boolean} True if payload is a valid and unique altcha challenge which originated from this server
|
||||
*/
|
||||
module.exports.verify = async function(payload, uniqueSecret = ''){
|
||||
//If we already checked this payload
|
||||
//If this payload is already spent
|
||||
if(spent.indexOf(payload) != -1){
|
||||
//Fuck off and die
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,10 @@ const loggerUtil = require('./loggerUtils');
|
|||
//NPM Imports
|
||||
const validator = require('validator');//We need validators for express-less code too!
|
||||
|
||||
|
||||
/**
|
||||
* Basic security check which runs on startup.
|
||||
* Warns server admin against unsafe config options.
|
||||
*/
|
||||
module.exports.securityCheck = function(){
|
||||
//Check Protocol
|
||||
if(config.protocol.toLowerCase() != 'https'){
|
||||
|
|
|
|||
|
|
@ -23,15 +23,33 @@ const crypto = require('node:crypto');
|
|||
//NPM Imports
|
||||
const bcrypt = require('bcrypt');
|
||||
|
||||
/**
|
||||
* Sitewide function for hashing passwords
|
||||
* @param {String} pass - Password to hash
|
||||
* @returns {String} Hashed/Salted password
|
||||
*/
|
||||
module.exports.hashPassword = function(pass){
|
||||
const salt = bcrypt.genSaltSync();
|
||||
return bcrypt.hashSync(pass, salt);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sitewide password for authenticating/comparing passwords agianst hashes
|
||||
* @param {String} pass - Plaintext Password
|
||||
* @param {String} hash - Salty Hash
|
||||
* @returns {Boolean} True if authentication success
|
||||
*/
|
||||
module.exports.comparePassword = function(pass, hash){
|
||||
return bcrypt.compareSync(pass, hash);
|
||||
}
|
||||
|
||||
/**
|
||||
* Site-wide IP hashing/salting function
|
||||
*
|
||||
* Provides a basic level of privacy by only logging salted hashes of IP's
|
||||
* @param {String} ip - IP to hash
|
||||
* @returns {String} Hashed/Salted IP Adress
|
||||
*/
|
||||
module.exports.hashIP = function(ip){
|
||||
//Create hash object
|
||||
const hashObj = crypto.createHash('md5');
|
||||
|
|
|
|||
|
|
@ -18,8 +18,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
|||
const validator = require('validator');//No express here, so regular validator it is!
|
||||
|
||||
//Create link cache
|
||||
/**
|
||||
* Basic RAM-Based cache of links, so we don't have to re-pull things after we get them
|
||||
*/
|
||||
module.exports.cache = new Map();
|
||||
|
||||
/**
|
||||
* Validates links and returns a marked link object that can be returned to the client to format/embed accordingly
|
||||
* @param {String} link - URL to Validate
|
||||
* @returns {Object} Marked link object
|
||||
*/
|
||||
module.exports.markLink = async function(link){
|
||||
//Check link cache for the requested link
|
||||
const cachedLink = module.exports.cache.get(link);
|
||||
|
|
|
|||
|
|
@ -17,6 +17,15 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
|||
//Config
|
||||
const config = require('../../config.json');
|
||||
|
||||
/**
|
||||
* Creates and returns a custom exception, tagged as a 'custom' exception, using the 'custom' boolean property.
|
||||
* This is used to denote that this error was generated on purpose, with a human readable message, that can be securely sent to the client.
|
||||
* Unexpected exceptions should only be logged internally, however, as they may contain sensitive data.
|
||||
*
|
||||
* @param {String} msg - Error message to send the client
|
||||
* @param {String} type - Error type to send back to the client
|
||||
* @returns {Error} The exception to smith
|
||||
*/
|
||||
module.exports.exceptionSmith = function(msg, type){
|
||||
//Create the new error with the given message
|
||||
const exception = new Error(msg);
|
||||
|
|
@ -31,6 +40,14 @@ module.exports.exceptionSmith = function(msg, type){
|
|||
return exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main error handling function
|
||||
* @param {Express.Response} res - Response being sent out to the client who caused the issue
|
||||
* @param {String} msg - Error message to send the client
|
||||
* @param {String} type - Error type to send back to the client
|
||||
* @param {Number} status - HTTP(s) Status Code to send back to the client
|
||||
* @returns {Express.Response} If we have a usable Express Response object, return it back after it's been cashed
|
||||
*/
|
||||
module.exports.errorHandler = function(res, msg, type = "Generic", status = 400){
|
||||
//Some controllers do things after sending headers, for those, we should remain silent
|
||||
if(!res.headersSent){
|
||||
|
|
@ -39,6 +56,10 @@ module.exports.errorHandler = function(res, msg, type = "Generic", status = 400)
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles local exceptions which where not directly created by user interaction
|
||||
* @param {Error} err - Exception to handle
|
||||
*/
|
||||
module.exports.localExceptionHandler = function(err){
|
||||
//If we're being verbose
|
||||
if(config.verbose){
|
||||
|
|
@ -47,6 +68,11 @@ module.exports.localExceptionHandler = function(err){
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles exceptions which where directly the fault of user action >:(
|
||||
* @param {Express.Response} res - Express Response object to bitch at
|
||||
* @param {Error} err - Error created by the jerk in question
|
||||
*/
|
||||
module.exports.exceptionHandler = function(res, err){
|
||||
//If this is a self-made problem
|
||||
if(err.custom){
|
||||
|
|
@ -60,14 +86,27 @@ module.exports.exceptionHandler = function(res, err){
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Basic error-handling for socket.io so we don't just silently swallow errors.
|
||||
* @param {Socket} socket - Socket error originated from
|
||||
* @param {String} msg - Error message to send the client
|
||||
* @param {String} type - Error type to send back to the client
|
||||
* @returns {Boolean} - Passthrough from socket.emit
|
||||
*/
|
||||
module.exports.socketErrorHandler = function(socket, msg, type = "Generic"){
|
||||
return socket.emit("error", {errors: [{type, msg, date: new Date()}]});
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates error messages for simple errors generated by socket.io interaction
|
||||
* @param {Socket} socket - Socket error originated from
|
||||
* @param {Error} err - Error created by the jerk in question
|
||||
* @returns {Boolean} - Passthrough from socket.emit
|
||||
*/
|
||||
module.exports.socketExceptionHandler = function(socket, err){
|
||||
//If this is a self made problem
|
||||
if(err.custom){
|
||||
//at the browser for fucking up, and tell it what it did wrong.
|
||||
//yell at the browser for fucking up, and tell it what it did wrong.
|
||||
return module.exports.socketErrorHandler(socket, err.message, err.type);
|
||||
}else{
|
||||
//Locally handle the exception
|
||||
|
|
@ -78,6 +117,12 @@ module.exports.socketExceptionHandler = function(socket, err){
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates error messages and drops connection for critical errors caused by socket.io interaction
|
||||
* @param {Socket} socket - Socket error originated from
|
||||
* @param {Error} err - Error created by the jerk in question
|
||||
* @returns {Boolean} - Passthrough from socket.disconnect
|
||||
*/
|
||||
module.exports.socketCriticalExceptionHandler = function(socket, err){
|
||||
//If this is a self made problem
|
||||
if(err.custom){
|
||||
|
|
@ -93,11 +138,21 @@ module.exports.socketCriticalExceptionHandler = function(socket, err){
|
|||
return socket.disconnect();
|
||||
}
|
||||
|
||||
/**
|
||||
* Prints warning text to server console
|
||||
* @param {String} string - String to print to console
|
||||
*/
|
||||
module.exports.consoleWarn = function(string){
|
||||
console.warn('\x1b[31m\x1b[4m%s\x1b[0m',string);
|
||||
}
|
||||
|
||||
//Basic error-handling middleware to ensure we're not dumping stack traces
|
||||
/**
|
||||
* Basic error-handling middleware to ensure we're not dumping stack traces to the client, as that would be insecure
|
||||
* @param {Error} err - Error to handle
|
||||
* @param {Express.Request} req - Express Request
|
||||
* @param {Express.Response} res - Express Response
|
||||
* @param {Function} next - Next function in the Express middleware chain (Not that it's getting called XP)
|
||||
*/
|
||||
module.exports.errorMiddleware = function(err, req, res, next){
|
||||
//Set generic error
|
||||
var reason = "Server Error";
|
||||
|
|
|
|||
|
|
@ -21,6 +21,9 @@ const config = require('../../config.json');
|
|||
const nodeMailer = require("nodemailer");
|
||||
|
||||
//Setup mail transport
|
||||
/**
|
||||
* nodemailer transport object, generated from options specific in our config file
|
||||
*/
|
||||
const transporter = nodeMailer.createTransport({
|
||||
host: config.mail.host,
|
||||
port: config.mail.port,
|
||||
|
|
@ -31,6 +34,14 @@ const transporter = nodeMailer.createTransport({
|
|||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Sends an email as tokebot to the requested user w/ the requested body and signature
|
||||
* @param {String} to - String containing the email address to send to
|
||||
* @param {String} subject - Subject line of the email to send
|
||||
* @param {String} body - Body contents, either HTML or Plaintext
|
||||
* @param {Boolean} htmlBody - Whether or not Body contents should be sent as HTML or Plaintext
|
||||
* @returns {Object} Sent mail info
|
||||
*/
|
||||
module.exports.mailem = async function(to, subject, body, htmlBody = false){
|
||||
//Create mail object
|
||||
const mailObj = {
|
||||
|
|
@ -56,6 +67,12 @@ module.exports.mailem = async function(to, subject, body, htmlBody = false){
|
|||
return sentMail;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends address verification email
|
||||
* @param {Mongoose.Document} requestDB - DB Document Object for the current email change request token
|
||||
* @param {Mongoose.Document} userDB - DB Document Object for the user we're verifying email against
|
||||
* @param {String} newEmail - New email address to send to
|
||||
*/
|
||||
module.exports.sendAddressVerification = async function(requestDB, userDB, newEmail){
|
||||
//Send the reset url via email
|
||||
await module.exports.mailem(
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ const media = require('../../app/channel/media/media.js');
|
|||
const regexUtils = require('../regexUtils.js');
|
||||
const loggerUtils = require('../loggerUtils.js')
|
||||
|
||||
/**
|
||||
* Pulls metadate for a given archive.org item
|
||||
* @param {String} fullID - Full path of the requested upload
|
||||
* @param {String} title - Title to add to media object
|
||||
* @returns {Array} Generated list of media objects from given upload path
|
||||
*/
|
||||
module.exports.fetchMetadata = async function(fullID, title){
|
||||
//Split fullID by first slash
|
||||
const [itemID, requestedPath] = decodeURIComponent(fullID).split(/\/(.*)/);
|
||||
|
|
|
|||
|
|
@ -22,6 +22,12 @@ const validator = require('validator');//No express here, so regular validator i
|
|||
const iaUtil = require('./internetArchiveUtils');
|
||||
const ytdlpUtil = require('./ytdlpUtils');
|
||||
|
||||
/**
|
||||
* Checks a given URL and runs the proper metadata fetching function to create a media object from any supported URL
|
||||
* @param {String} url - URL to yank media against
|
||||
* @param {String} title - Title to apply to yanked media
|
||||
* @returns
|
||||
*/
|
||||
module.exports.yankMedia = async function(url, title){
|
||||
//Get pull type
|
||||
const pullType = await this.getMediaType(url);
|
||||
|
|
@ -50,6 +56,13 @@ module.exports.yankMedia = async function(url, title){
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes raw links on relevant media objects
|
||||
*
|
||||
* Useful for sources like youtube, who only provide expiring raw links
|
||||
* @param {ScheduledMedia} mediaObj - Media Object to refresh
|
||||
* @returns {ScheduledMedia} Refreshed media object
|
||||
*/
|
||||
module.exports.refreshRawLink = async function(mediaObj){
|
||||
switch(mediaObj.type){
|
||||
case 'yt':
|
||||
|
|
@ -76,10 +89,16 @@ module.exports.refreshRawLink = async function(mediaObj){
|
|||
//Return null to tell the calling function there is no refresh required for this media type
|
||||
return null;
|
||||
}
|
||||
|
||||
//I'd be lying if this didn't take at least some inspiration/regex patterns from extractQueryParam() in cytube/forest's browser-side 'util.js'
|
||||
//Still this has some improvements like url pre-checks and the fact that it's handled serverside, recuing possibility of bad requests.
|
||||
//Some of the regex expressions for certain services have also been improved, such as youtube, and the fore.st-unique archive.org
|
||||
/**
|
||||
* Detects media type by URL
|
||||
*
|
||||
* I'd be lying if this didn't take at least some inspiration/regex patterns from extractQueryParam() in cytube/forest's browser-side 'util.js'
|
||||
* Still this has some improvements like url pre-checks and the fact that it's handled serverside, recuing possibility of bad requests.
|
||||
* Some of the regex expressions for certain services have also been improved, such as youtube, and the fore.st-unique archive.org
|
||||
*
|
||||
* @param {String} url - URL to determine media type of
|
||||
* @returns {Object} containing URL type and clipped ID string
|
||||
*/
|
||||
module.exports.getMediaType = async function(url){
|
||||
//Check if we have a valid url, encode it on the fly in case it's too humie-friendly
|
||||
if(!validator.isURL(encodeURI(url))){
|
||||
|
|
|
|||
|
|
@ -29,6 +29,12 @@ const media = require('../../app/channel/media/media.js');
|
|||
const regexUtils = require('../regexUtils.js');
|
||||
const loggerUtils = require('../loggerUtils.js')
|
||||
|
||||
/**
|
||||
* Pulls metadata for a single youtube video via YT-DLP
|
||||
* @param {String} id - Youtube Video ID
|
||||
* @param {String} title - Title to add to the given media object
|
||||
* @returns {Media} Media object containing relevant metadata
|
||||
*/
|
||||
module.exports.fetchYoutubeMetadata = async function(id, title){
|
||||
try{
|
||||
//Try to pull media from youtube id
|
||||
|
|
@ -50,6 +56,12 @@ module.exports.fetchYoutubeMetadata = async function(id, title){
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pulls metadata for a playlist of youtube videos via YT-DLP
|
||||
* @param {String} id - Youtube Playlist ID
|
||||
* @param {String} title - Title to add to the given media objects
|
||||
* @returns {Array} Array of Media objects containing relevant metadata
|
||||
*/
|
||||
module.exports.fetchYoutubePlaylistMetadata = async function(id, title){
|
||||
try{
|
||||
//Try to pull media from youtube id
|
||||
|
|
@ -71,15 +83,23 @@ module.exports.fetchYoutubePlaylistMetadata = async function(id, title){
|
|||
}
|
||||
}
|
||||
|
||||
/* This requires HLS embeds which, in-turn, require daily motion to add us to their CORS exception list
|
||||
* Not gonna happen, so we need to use their API for this, or proxy the video
|
||||
module.exports.fetchDailymotionMetadata = async function(id, title){
|
||||
//Pull media from dailymotion link
|
||||
const media = await fetchVideoMetadata(`https://dailymotion.com/video/${id}`, title, 'dm');
|
||||
|
||||
//Return found media;
|
||||
return media;
|
||||
}
|
||||
}*/
|
||||
|
||||
//Generic single video YTDLP function meant to be used by service-sepecific fetchers which will then be used to fetch video metadata
|
||||
/**
|
||||
* Generic single video YTDLP function meant to be used by service-sepecific fetchers which will then be used to fetch video metadata
|
||||
* @param {String} link - Link to video in question
|
||||
* @param {String} title - Title to add to the given media objects
|
||||
* @param {String} type - Link type to attach to the resulting media object
|
||||
* @returns {Array} Array of Media objects containing relevant metadata
|
||||
*/
|
||||
async function fetchVideoMetadata(link, title, type, format = 'b'){
|
||||
//Create media list
|
||||
const mediaList = [];
|
||||
|
|
@ -110,6 +130,12 @@ async function fetchVideoMetadata(link, title, type, format = 'b'){
|
|||
}*/
|
||||
|
||||
//Wrapper function for YT-DLP NPM package with pre-set cli-flags
|
||||
/**
|
||||
* Basic async YT-DLP Fetch wrapper, ensuring config
|
||||
* @param {String} link - Link to fetch using YT-DLP
|
||||
* @param {String} format - Format string to hand YT-DLP, defaults to 'b'
|
||||
* @returns {Object} Metadata dump from YT-DLP
|
||||
*/
|
||||
async function ytdlpFetch(link, format = 'b'){
|
||||
//return promise from ytdlp
|
||||
return ytdlp(link, {
|
||||
|
|
|
|||
|
|
@ -14,10 +14,15 @@ GNU Affero General Public License for more details.
|
|||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.*/
|
||||
|
||||
/**
|
||||
* I won't lie this line was whole-sale ganked from stack overflow like a fucking skid
|
||||
* In my defense I only did it because js-runtime-devs are taking fucking eons to implement RegExp.escape()
|
||||
* This should be replaced once that function becomes available in mainline versions of node.js:
|
||||
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape
|
||||
*
|
||||
* @param {String} string - Regex string to escape
|
||||
* @returns {String} The Escaped String
|
||||
*/
|
||||
module.exports.escapeRegex = function(string){
|
||||
/* I won't lie this line was whole-sale ganked from stack overflow like a fucking skid
|
||||
In my defense I only did it because js-runtime-devs are taking fucking eons to implement RegExp.escape()
|
||||
This should be replaced once that function becomes available in mainline versions of node.js:
|
||||
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/escape */
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
}
|
||||
|
|
@ -26,6 +26,9 @@ const channelModel = require('../schemas/channel/channelSchema');
|
|||
const sessionUtils = require('./sessionUtils');
|
||||
const { email } = require('../validators/accountValidator');
|
||||
|
||||
/**
|
||||
* Schedules all timed jobs accross the server
|
||||
*/
|
||||
module.exports.schedule = function(){
|
||||
//Process hashed IP Records that haven't been recorded in a week or more
|
||||
cron.schedule('0 0 * * *', ()=>{userModel.processAgedIPRecords()},{scheduled: true, timezone: "UTC"});
|
||||
|
|
@ -41,6 +44,9 @@ module.exports.schedule = function(){
|
|||
cron.schedule('0 0 * * *', ()=>{emailChangeModel.processExpiredRequests()},{scheduled: true, timezone: "UTC"});
|
||||
}
|
||||
|
||||
/**
|
||||
* Kicks off first run of scheduled functions before scheduling functions for regular callback
|
||||
*/
|
||||
module.exports.kickoff = function(){
|
||||
//Process Hashed IP Records that haven't been recorded in a week or more
|
||||
userModel.processAgedIPRecords();
|
||||
|
|
|
|||
|
|
@ -19,15 +19,33 @@ const config = require('../../config.json');
|
|||
const {userModel} = require('../schemas/user/userSchema.js');
|
||||
const userBanModel = require('../schemas/user/userBanSchema.js')
|
||||
const altchaUtils = require('../utils/altchaUtils.js');
|
||||
const loggerUtils = require('../utils/loggerUtils.js')
|
||||
const loggerUtils = require('../utils/loggerUtils.js');
|
||||
|
||||
//Create failed sign-in cache since it's easier and more preformant to implement it this way than adding extra burdon to the database
|
||||
//Server restarts are far and few between. It would take multiple during a single bruteforce attempt for this to become an issue.
|
||||
/**
|
||||
* Create failed sign-in cache since it's easier and more preformant to implement it this way than adding extra burdon to the database
|
||||
* Server restarts are far and few between. It would take multiple during a single bruteforce attempt for this to become an issue.
|
||||
*/
|
||||
const failedAttempts = new Map();
|
||||
|
||||
/**
|
||||
* How many failed attempts required to throttle with altcha
|
||||
*/
|
||||
const throttleAttempts = 5;
|
||||
|
||||
/**
|
||||
* How many attempts to lock user account out for the day
|
||||
*/
|
||||
const maxAttempts = 200;
|
||||
|
||||
//this module is good for keeping wrappers for userModel and other shit in that does more session handling than database access/modification.
|
||||
/**
|
||||
* Sole and Singular Session Authentication method.
|
||||
* All logins should happen through here, all other site-wide authentication should happen by sessions authenticated by this model.
|
||||
* This is important, as reducing authentication endpoints reduces attack surface.
|
||||
* @param {String} user - Username to login as
|
||||
* @param {String} pass - Password to authenticat session with
|
||||
* @param {express.Request} req - Express request object w/ session to authenticate
|
||||
* @returns Username of authticated user upon success
|
||||
*/
|
||||
module.exports.authenticateSession = async function(user, pass, req){
|
||||
//Fuck you yoda
|
||||
try{
|
||||
|
|
@ -137,15 +155,27 @@ module.exports.authenticateSession = async function(user, pass, req){
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs user out and destroys all server-side traces of a given session
|
||||
* @param {express-session.session} session
|
||||
*/
|
||||
module.exports.killSession = async function(session){
|
||||
session.destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns how many failed login attempts within the past day or so since the last login has occured for a given user
|
||||
* @param {String} user - User to check map against
|
||||
* @returns {Number} of failed login attempts
|
||||
*/
|
||||
module.exports.getLoginAttempts = function(user){
|
||||
//Read the code, i'm not explaining this
|
||||
return failedAttempts.get(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* Nightly Function Call which iterates through the failed login attempts map, removing any which haven't been attempted in over a da yeahy
|
||||
*/
|
||||
module.exports.processExpiredAttempts = function(){
|
||||
for(user of failedAttempts.keys()){
|
||||
//Get attempt by user
|
||||
|
|
|
|||
Loading…
Reference in a new issue