diff --git a/README.md b/README.md new file mode 100644 index 0000000000000000000000000000000000000000..362af52a522d8b71fd943a3d1ea75403103083ff --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +# api-server + +## Running api-server + +1. start the docker environment: `npm run exec:env` +2. start the server: `npm run watch` +3. start the queue worker: `npm run watch:queue` +4. start NGrok: `ngrok http 7123 --hostname <myhostname>` \ No newline at end of file diff --git a/database/migrations/20210000000001-create-uploads-table.js b/database/migrations/20210000000001-create-uploads-table.js new file mode 100644 index 0000000000000000000000000000000000000000..6f3faab2c2cc1b88b08edd57de1a9c89441ebf35 --- /dev/null +++ b/database/migrations/20210000000001-create-uploads-table.js @@ -0,0 +1,69 @@ +module.exports = { + up: (migration, Types) => { + return migration.createTable('uploads', { + id: { + type: Types.UUID, + primaryKey: true, + defaultValue: Types.UUIDV4, + allowNull: false, + }, + user_id: { + type: Types.UUID, + allowNull: true, + references: { + model: 'users', + key: 'id', + }, + }, + provider: { + type: Types.TEXT, + allowNull: false, + }, + upload_url: { + type: Types.TEXT, + allowNull: false, + }, + request_params: { + type: Types.JSONB, + allowNull: false, + }, + expires_at: { + type: Types.DATE, + allowNull: false, + }, + status: { + type: Types.TEXT, + allowNull: 'false', + default: 'pending', + }, + status_reason: { + type: Types.TEXT, + allowNull: true, + }, + meta: { + type: Types.JSONB, + defaultValue: {}, + allowNull: false, + }, + created_at: { + type: Types.DATE, + defaultValue: Types.fn('now'), + allowNull: false, + }, + updated_at: { + type: Types.DATE, + defaultValue: Types.fn('now'), + allowNull: false, + }, + deleted_at: { + type: Types.DATE, + defaultValue: null, + allowNull: true, + }, + }) + }, + + down: (migration, Types) => { + return migration.dropTable('uploads') + }, +} diff --git a/package-lock.json b/package-lock.json index 87b04fea73525a04f6f05c88dccb2638d1ab7eb1..fec1df2f0c3a0e83cc946e826f01ae13133aaeda 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6345,9 +6345,9 @@ }, "dependencies": { "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", "requires": { "ms": "^2.1.1" } diff --git a/package.json b/package.json index 68592072e2e20c38b9881f58e622882e30a85663..79cbf7e16a60e8373b29abda69087e5ca573a158 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "cmd": "NODE_PATH=src node run", "sql": "NODE_PATH=src node scripts/npx-boot.js sequelize", "repl": "NODE_PATH=src node -e 'Object.entries(require(\"bootstrap\")).forEach(([key, value]) => Object.defineProperty(global, key, { value })); boot().then(() => console.log(\"Booted\"))' -i", - "prettier": "prettier server.js worker.js run.js src database --write" + "prettier": "prettier server.js worker.js run.js src database --write" }, "author": "Louis Capitanchik <louis@microhacks.co.uk>", "license": "GPL-3.0+", diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..4ba916fb3e6dea1378b96fc2f73dfae48b9c5fb8 --- /dev/null +++ b/public/logo.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6521c82a8519e2ae987ad3d4a6143681a4618b4fa3a5585f82aa65a778403928 +size 97893 diff --git a/public/main.css b/public/main.css new file mode 100644 index 0000000000000000000000000000000000000000..921ada56d0947bd5f698ac28978af3e0f556b2d0 --- /dev/null +++ b/public/main.css @@ -0,0 +1,50 @@ +* { + margin: 0; + padding: 0; +} + +html { + font-size: 16px; + font-family: Arial, sans-serif; +} + +.container { + display: flex; + flex-direction: column; + padding: 2rem; + align-items: center; +} + +.inner { + width: 100%; + max-width: 600px; +} + +.row { + display: flex; +} + +.header { + padding: 1rem 0; +} + +.text-container { + padding: 2rem; +} + +.centered { + padding: 1rem 0; +} + +.controls { + display: flex; + flex-direction: column; + flex: 1; + max-width: 300px; +} + +.controls input { + width: 100%; + padding: 5px; + margin: 5px 0; +} \ No newline at end of file diff --git a/src/app.js b/src/app.js index b717c76914829b9ac9c84c3404a72dadb30e97f0..72b1476d456a34b0433ff1dfd60239dbea2b691d 100644 --- a/src/app.js +++ b/src/app.js @@ -4,10 +4,13 @@ const logger = require('koa-logger') const bodyparser = require('koa-bodyparser') const etag = require('koa-etag') const session = require('koa-session') +const static = require('koa-static') const hbs = require('vendor/koa-handlebars') const { config } = require('bootstrap') +const pathutil = require('path') + const debughbs = require('debug')('server:templates') const debug = require('debug')('server:boot') const requestLog = require('debug')('server:request') @@ -28,6 +31,7 @@ module.exports = async function createApp(app = new Koa()) { app.use(etag({ weak: true })) app.use(bodyparser()) app.use(logger(s => requestLog(s))) + app.use(static(pathutil.resolve(__dirname + '/../public'))) app.use( session( diff --git a/src/core/services/serialise.js b/src/core/services/serialise.js new file mode 100644 index 0000000000000000000000000000000000000000..3e80e4d68ecf00ab3741db874cb99ba6fbeafb60 --- /dev/null +++ b/src/core/services/serialise.js @@ -0,0 +1,22 @@ +exports.serialise = async function serialise(pattern, data) { + const output = {} + + for (const [key, mapper] of Object.entries(pattern)) { + switch (typeof mapper) { + case 'string': + case 'number': + case 'symbol': + console.log(data[mapper]) + output[key] = data[mapper] + break + case 'function': + output[key] = await mapper(data, output) + break + case 'object': + output[key] = await serialise(mapper, data) + break + } + } + + return output +} diff --git a/src/core/utils/sym.js b/src/core/utils/sym.js new file mode 100644 index 0000000000000000000000000000000000000000..11b42a17df63fdc3cb0bc777b3128625f8206c84 --- /dev/null +++ b/src/core/utils/sym.js @@ -0,0 +1,10 @@ +const SYM = new Proxy( + {}, + { + get(target, p, receiver) { + return p + }, + }, +) + +module.exports = SYM diff --git a/src/database/models/Upload.js b/src/database/models/Upload.js new file mode 100644 index 0000000000000000000000000000000000000000..99aa08e7e3ecb14c20da935ed480e150201e5316 --- /dev/null +++ b/src/database/models/Upload.js @@ -0,0 +1,78 @@ +const timestamps = require('./properties/timestamps') +const BaseModel = require('./BaseModel') + +class Upload extends BaseModel { + static associate(models) { + this.belongsTo(models.User, { foreignKey: 'user_id' }) + } + + toJSON() { + return { + id: this.id, + provider: this.provider, + upload_url: this.upload_url, + request_params: this.request_params, + expires_at: this.expires_at, + status: this.status, + status_reason: this.status_reason, + meta: this.meta, + created_at: this.created_at, + updated_at: this.updated_at, + } + } +} + +function generatePublicUri(provider, file) { + return [ + 'https://storage.googleapis.com', + file.meta.bucket, + file.file_root, + file.file_name, + ].join('/') +} + +module.exports = (sequelize, DataTypes) => { + Upload.init( + Object.assign( + { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: DataTypes.UUIDV4, + validate: { + isUUID: 4, + }, + }, + provider: { + type: DataTypes.TEXT, + }, + upload_url: { + type: DataTypes.TEXT, + }, + request_params: { + type: DataTypes.JSONB, + }, + expires_at: { + type: DataTypes.DATE, + }, + status: { + type: DataTypes.TEXT, + }, + status_reason: { + type: DataTypes.TEXT, + }, + meta: { + type: DataTypes.JSONB, + }, + }, + timestamps(DataTypes), + ), + { + sequelize, + paranoid: true, + tableName: 'uploads', + }, + ) + + return Upload +} diff --git a/src/database/models/User.js b/src/database/models/User.js index 3a23fb1193a0df99289205181b2c49f91e66df09..8b9f3b0f9a64c303bab16483e57c3320d26672f5 100644 --- a/src/database/models/User.js +++ b/src/database/models/User.js @@ -8,6 +8,7 @@ class User extends BaseModel { this.hasMany(models.AccessToken, { foreignKey: 'user_id' }) this.hasMany(models.RefreshToken, { foreignKey: 'user_id' }) this.hasMany(models.File, { foreignKey: 'user_id' }) + this.hasMany(models.Upload, { foreignKey: 'user_id' }) this.belongsToMany(models.File, { as: 'likes', through: 'user_file_likes', @@ -166,6 +167,28 @@ class User extends BaseModel { updated_at: this.updated_at, } } + + serialise() { + const { serialise } = require('core/services/serialise') + const SYM = require('core/utils/sym') + + const { id, name, email, created_at, updated_at } = SYM + + return serialise( + { + id, + name, + email, + meta: user => ({ + ...user.meta, + dob: undefined, + }), + created_at, + updated_at, + }, + this, + ) + } } module.exports = (sequelize, DataTypes) => { diff --git a/src/domain/auth/AuthenticationService.js b/src/domain/auth/AuthenticationService.js index 7ea548c5defad19a3348c77093d6ff3c9be2158c..25fdb36f342b75871b38559ebe03ec08838ad697 100644 --- a/src/domain/auth/AuthenticationService.js +++ b/src/domain/auth/AuthenticationService.js @@ -68,7 +68,6 @@ module.exports = class AuthenticationService extends ContextualModule { } } else if (this.ctx.get('x-api-token')) { const token = this.ctx.get('x-api-token') - console.log(token, this.ctx.get('x-token-type')) try { const user = await User.fromToken(token, this.ctx.get('x-token-type')) if (user) { diff --git a/src/domain/data/MetricsService.js b/src/domain/data/MetricsService.js index 996ccf0b5d7e9ad1fd76d6393582832c5d0d8805..832087609270e732c08eeaf41141dc82bc82abc8 100644 --- a/src/domain/data/MetricsService.js +++ b/src/domain/data/MetricsService.js @@ -15,7 +15,7 @@ module.exports = class MetricsService extends ContextualModule { return ['queryAggregate', 'queryAll', 'recordMetric'] } - async recordMetric(value, type, location) { + async recordMetric(value, type, location, meta = {}, transaction) { const user = await this.ctx.services['core.auth'].getUser() const point = { type: 'Point', @@ -26,11 +26,15 @@ module.exports = class MetricsService extends ContextualModule { type, location: point, author_id: user ? user.id : null, + meta, } if (this.ctx.request.device) { - payload.meta = { device: this.ctx.request.device } + payload.meta = { + ...(payload.meta ?? {}), + device: this.ctx.request.device, + } } - return await Metric.create(payload) + return await Metric.create(payload, { transaction }) } async queryAggregate(pointBuffer, types, from, to) { diff --git a/src/http/controllers/api/auth.js b/src/http/controllers/api/auth.js index 126913a45670956e270b4259b7d2af05ea09c10c..08f9fa98862909836c546b9e01b8e2b71b519efb 100644 --- a/src/http/controllers/api/auth.js +++ b/src/http/controllers/api/auth.js @@ -55,8 +55,6 @@ exports.triggerPasswordReset = async ctx => { }) } - console.log(ctx.request.device) - await queue.dispatch('send-user-password-reset', { email, device: ctx.request.device, diff --git a/src/http/controllers/api/content.js b/src/http/controllers/api/content.js index b65487206a8c072e967154514e7c3eefd2a71a33..1f126df9f4a691a6499f3268974f72223aad764c 100644 --- a/src/http/controllers/api/content.js +++ b/src/http/controllers/api/content.js @@ -5,8 +5,52 @@ const moment = require('moment') exports.postMetric = async ctx => { const allowedTypes = new Set(Metric.getSupportedMetricTypes()) - const { value, type, location } = ctx.request.body + const { value, type, location, meta, batch } = ctx.request.body + let metric + let metrics + + if (batch) { + metrics = [] + try { + await sequelize.transaction(async t => { + for (const m of batch) { + const result = await save(m.value, m.type, m.location, m.meta, { + allowedTypes, + transaction: t, + ctx, + }) + + metrics.push(result) + } + }) + } catch (e) { + console.log(e) + + throw new HttpError({ + status: 400, + code: 'MTR-001', + title: 'Failed to save metric', + description: 'Something went wrong trying to save the metric', + }) + } + + ctx.body = { + metrics: metrics.map(metric => metric.toJSON()), + } + } else { + metric = await save(value, type, location, meta, { allowedTypes, ctx }) + ctx.body = { metric: metric.toJSON() } + } +} + +const save = async ( + value, + type, + location, + meta, + { allowedTypes, transaction, ctx }, +) => { if ( location == null || location.longitude == null || @@ -21,12 +65,13 @@ exports.postMetric = async ctx => { } if (allowedTypes.has(type)) { - const metric = await ctx.services['data.metrics'].recordMetric( + return await ctx.services['data.metrics'].recordMetric( value, type, location, + meta, + transaction, ) - ctx.body = { metric: metric.toJSON() } } else { throw new HttpError({ status: 400, diff --git a/src/http/controllers/api/user.js b/src/http/controllers/api/user.js index dbb3ee8fb1ff2064d4c3f5d40ecc1cef5b346804..7c100458b8f026af5fddecdeb2a4409b351fbf6f 100644 --- a/src/http/controllers/api/user.js +++ b/src/http/controllers/api/user.js @@ -3,10 +3,16 @@ const HttpError = require('core/errors/HttpError') exports.self = async ctx => { const user = await ctx.services['core.auth'].getUser() if (user) { - await user.handleIncludes(ctx.includes) + ctx.body = { + user: await user.serialise(), + } + } else { + throw new HttpError({ + status: 404, + title: 'No such user', + description: 'No user is currently logged in', + }) } - - ctx.body = { user } } exports.updateSelf = async ctx => { diff --git a/src/http/controllers/api/v2/uploads.js b/src/http/controllers/api/v2/uploads.js new file mode 100644 index 0000000000000000000000000000000000000000..62f03586532c700bf6cd8bb2ebcc8689f5f44a75 --- /dev/null +++ b/src/http/controllers/api/v2/uploads.js @@ -0,0 +1,30 @@ +exports.createUpload = async ctx => { + const { fs } = require('services') + const { v4: uuid } = require('uuid') + const { config } = require('bootstrap') + const HttpError = require('core/errors/HttpError') + + const { content_type, id } = ctx.request.body + const user = await ctx.services['core.auth'].getUser() + if (!user) { + throw new HttpError(403, 'You must be logged in to upload an image') + } + + const payload = await fs.createUploadUrl( + `${user.id}/${id ?? uuid()}.jpg`, + 60 * 60 * 2, + { + contentType: content_type, + }, + ) + + const upload = await user.createUpload({ + provider: config('fs.driver'), + upload_url: payload.url, + request_params: payload, + expires_at: payload.expires_at_ms, + status: 'pending', + }) + + ctx.body = { upload: payload } +} diff --git a/src/http/routers/routes_v2.js b/src/http/routers/routes_v2.js index cc55705e627026a13dedaf911a9fd8ada83a0b41..2ad5574c333bc6be51b0f11220f15b29e9f1a97f 100644 --- a/src/http/routers/routes_v2.js +++ b/src/http/routers/routes_v2.js @@ -1,77 +1,103 @@ const Router = require('@koa/router') const { apiMiddlewareGroup } = require('../middleware/groups') -const router = new Router() +const router = new Router({ prefix: '/v2' }) const controller = (path, handler) => require(`../controllers/${path}`)[handler] const param = name => require(`../params/${name}`) apiMiddlewareGroup.forEach(middleware => router.use(middleware)) -router.post('/auth/login') -router.post('/auth/register') -router.post('/auth/password-reset') - -router.get('/self') -router.get('/self/bundles') -router.get('/self/:property') - -router.get('/metrics') -router.post('/metrics') - -router.get('/images') -router.post('/images') -router.post('/images/:imageId/share') - -router.post('/an/ev', controller('api/analytics', 'track')) - -api.post('/metrics', controller('api/content', 'postMetric')) -api.get('/metrics', controller('api/content', 'getWithin')) - -api.get('/images', controller('api/storage', 'getFiles')) -api.post( - '/images', - upload.single('featured_image'), - controller('api/storage', 'saveFile'), +const noop = ctx => + (ctx.body = { + body: ctx.request.body, + headers: ctx.request.headers, + path: ctx.path, + query: ctx.query, + }) + +router.get( + '/', + ctx => + (ctx.body = { + name: 'Jetsam Data API', + prefix: ctx.path, + }), ) -api.post('/images/:imageId/feature', controller('api/storage', 'featureImage')) -/** @deprecated */ -api.post( - '/feature', - upload.single('featured_image'), - controller('api/storage', 'saveFile'), +router.post('/auth/login', controller('api/auth', 'login')) +router.post('/auth/register', controller('api/auth', 'register')) +router.post( + '/auth/password-reset', + controller('api/auth', 'triggerPasswordReset'), ) -api.get('/feed', controller('api/storage', 'feed')) -api.post('/feed/:fileId/like', controller('api/storage', 'like')) -api.post('/feed/:fileId/unlike', controller('api/storage', 'unlike')) - -api.post('/register', controller('api/auth', 'register')) -api.post('/login', controller('api/auth', 'login')) - -api.post('/auth/reset-token', controller('api/auth', 'triggerPasswordReset')) -api.post('/auth/reset-password', controller('api/auth', 'handlePasswordReset')) +router.get('/self', controller('api/user', 'self')) +router.get('/self/bundles', controller('api/app', 'getBundles')) +router.put('/self/:property', controller('api/user', 'updateOne')) -api.param('oauthClientId', param('oauth_client')) +router.get('/metrics', controller('api/content', 'getWithin')) +router.post('/metrics', controller('api/content', 'postMetric')) -api.get('/oauth/clients', controller('api/oauth', 'listClients')) -api.post('/oauth/clients', controller('api/oauth', 'createClient')) -api.post( - '/oauth/clients/:oauthClientId/redirects', - controller('api/oauth', 'addClientRedirect'), -) -api.delete( - '/oauth/clients/:oauthClientId/redirects', - controller('api/oauth', 'removeClientRedirect'), -) +router.get('/images', noop) +router.post('/images', noop) +router.post('/images/:imageId/share', noop) -api.get('/self', controller('api/user', 'self')) -api.get('/self/bundles', controller('api/app', 'getBundles')) -api.put('/self/:property', controller('api/user', 'updateOne')) +router.get('/uploads', noop) +router.post('/uploads', controller('api/v2/uploads', 'createUpload')) +router.get('/uploads/:upload_id', noop) +router.delete('/uploads/:upload_id', noop) +router.put('/uploads/:upload_id/:property', noop) -api.post('/an/id', async ctx => {}) -api.post('/an/ev', controller('api/analytics', 'track')) +router.post('/an/ev', controller('api/analytics', 'track')) -api.post('/feedback', controller('api/feedback', 'send')) +// api.post('/metrics', controller('api/content', 'postMetric')) +// api.get('/metrics', controller('api/content', 'getWithin')) +// +// api.get('/images', controller('api/storage', 'getFiles')) +// api.post( +// '/images', +// upload.single('featured_image'), +// controller('api/storage', 'saveFile'), +// ) +// api.post('/images/:imageId/feature', controller('api/storage', 'featureImage')) +// +// /** @deprecated */ +// api.post( +// '/feature', +// upload.single('featured_image'), +// controller('api/storage', 'saveFile'), +// ) +// +// api.get('/feed', controller('api/storage', 'feed')) +// api.post('/feed/:fileId/like', controller('api/storage', 'like')) +// api.post('/feed/:fileId/unlike', controller('api/storage', 'unlike')) +// +// api.post('/register', controller('api/auth', 'register')) +// api.post('/login', controller('api/auth', 'login')) +// +// api.post('/auth/reset-token', controller('api/auth', 'triggerPasswordReset')) +// api.post('/auth/reset-password', controller('api/auth', 'handlePasswordReset')) +// +// api.param('oauthClientId', param('oauth_client')) +// +// api.get('/oauth/clients', controller('api/oauth', 'listClients')) +// api.post('/oauth/clients', controller('api/oauth', 'createClient')) +// api.post( +// '/oauth/clients/:oauthClientId/redirects', +// controller('api/oauth', 'addClientRedirect'), +// ) +// api.delete( +// '/oauth/clients/:oauthClientId/redirects', +// controller('api/oauth', 'removeClientRedirect'), +// ) +// +// api.get('/self', controller('api/user', 'self')) +// api.get('/self/bundles', controller('api/app', 'getBundles')) +// api.put('/self/:property', controller('api/user', 'updateOne')) +// +// api.post('/an/id', async ctx => {}) +// api.post('/an/ev', controller('api/analytics', 'track')) +// +// api.post('/feedback', controller('api/feedback', 'send')) module.exports = router diff --git a/src/http/routes.js b/src/http/routes.js index 648d4c54d93c11a71dea923254616764dc7e2756..9573e0028dea83887d4c8e45d665d8ed6c195114 100644 --- a/src/http/routes.js +++ b/src/http/routes.js @@ -7,6 +7,7 @@ const { env, config } = require('bootstrap') const Router = require('@koa/router') const multer = require('@koa/multer') const upload = multer({ dest: '/tmp/' }) +const attach = require('koa-mount') const context = require('http/middleware/ThreadContextWrapper') const errors = require('http/middleware/ErrorHandler') @@ -16,6 +17,8 @@ const loaders = require('http/middleware/MountLoaders') const userGate = require('http/middleware/RequiresAuth') const device = require('http/middleware/DeviceProperties').extractDevice +const v2 = require('./routers/routes_v2') + const well_known = new Router({ prefix: '/.well-known' }) well_known.get('wk.jwks', '/jwks.json', async ctx => { const { getKeys } = require('core/utils/jwt') @@ -96,6 +99,7 @@ function mount(api) { ctx.body = { name: 'Jetsam Data API', version: pkg.version, + prefix: ctx.path, } }) @@ -154,6 +158,9 @@ function mount(api) { api.post('/an/ev', controller('api/analytics', 'track')) api.post('/feedback', controller('api/feedback', 'send')) + + api.use(v2.allowedMethods()) + api.use(v2.routes()) } mount(apiRouter) diff --git a/src/services/fs/gcs.js b/src/services/fs/gcs.js index 55ac411f66981e8334a7b6f61a5aed57b1abf814..dba7eb2bf82190d7a1d6fda1696f6f2ae17474af 100644 --- a/src/services/fs/gcs.js +++ b/src/services/fs/gcs.js @@ -66,9 +66,10 @@ class GCSFS extends FS { return files } async createUploadUrl(path, ttl, opts) { + const ttl_ms = ttl * 1000 const gopts = { version: 'v4', - expires: Date.now() + ttl * 1000, + expires: Date.now() + ttl_ms, action: 'write', contentType: getContentType(opts.headers ?? {}) ?? opts.contentType, } @@ -79,9 +80,9 @@ class GCSFS extends FS { url, expires_at_ms: gopts.expires, method: 'PUT', - headers: opts.headers - ? { 'Content-Type': getContentType(opts.headers) ?? opts.contentType } - : {}, + headers: { + 'Content-Type': gopts.contentType, + }, } } async createDownloadUrl(path, ttl, opts) { @@ -124,9 +125,10 @@ class GCSFS extends FS { function getContentType(obj) { return ( - obj.headers?.['Content-Type'] ?? - obj.headers?.['content-type'] ?? - obj.headers?.contentType + obj?.headers?.['Content-Type'] ?? + obj?.headers?.['content-type'] ?? + obj?.headers?.contentType ?? + null ) } diff --git a/views/auth/reset-password-error.hbs b/views/auth/reset-password-error.hbs index c4af0693152653c2da9c7d90fd50e90f0174c9b0..b1a54e3c26ff450c5b895495b065fa56d6eeb48e 100644 --- a/views/auth/reset-password-error.hbs +++ b/views/auth/reset-password-error.hbs @@ -12,33 +12,25 @@ <meta name="viewport" content="width=device-width, initial-scale=1"> - <link rel="shortcut icon" type="image/png" href="https://jetsam.tech/images/logo.png"> - <link rel="stylesheet" - href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" - integrity="sha256-l85OmPOjvil/SOvVt3HnSSjzF1TUMyT9eV0c2BzEGzU=" - crossorigin="anonymous" /> - <link rel="stylesheet" - href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" - integrity="sha256-2YQRJMXD7pIAPHiXr0s+vlRWA7GYJEK0ARns7k2sbHY=" - crossorigin="anonymous" /> - - <link rel="stylesheet" href="/css/main.css?v=2"> + <link rel="shortcut icon" type="image/png" href="https://app.jetsam.tech/logo.png"> + <link rel="stylesheet" href="/main.css?v=2"> </head> <body> -<main class="container" style="max-width: 600px"> - <header class="header"> - <img class="logo" src="https://jetsam.tech/images/logo.png" width="128px" height="128px"> - <div> - <h1>Jetsam</h1> - <h3>Your World; Cleaner</h3> +<main class="container"> + <div class="inner"> + <div class="row header"> + <img class="logo" src="/logo.png" width="128px" height="128px"> + <div class="text-container"> + <h1>Jetsam</h1> + <h3>Your World; Cleaner</h3> + </div> + </div> + <h3 class="centered">Password Reset Error</h3> + <p class="centered">{{ message }}</p> + <div class="row centered"> + <a href="{{ back_link }}">Go Back</a> </div> - </header> - - <h3 class="centered">Password Reset Error</h3> - <p class="centered">{{ message }}</p> - <div class="row centered"> - <a href="{{ back_link }}">Go Back</a> </div> </main> diff --git a/views/auth/reset-password-success.hbs b/views/auth/reset-password-success.hbs index 16dee630eabb53c88ca82323eef54442adfd270f..304ff98255d15900c05090fe035ff961eb30363d 100644 --- a/views/auth/reset-password-success.hbs +++ b/views/auth/reset-password-success.hbs @@ -12,35 +12,30 @@ <meta name="viewport" content="width=device-width, initial-scale=1"> - <link rel="shortcut icon" type="image/png" href="https://jetsam.tech/images/logo.png"> - <link rel="stylesheet" - href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" - integrity="sha256-l85OmPOjvil/SOvVt3HnSSjzF1TUMyT9eV0c2BzEGzU=" - crossorigin="anonymous" /> - <link rel="stylesheet" - href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" - integrity="sha256-2YQRJMXD7pIAPHiXr0s+vlRWA7GYJEK0ARns7k2sbHY=" - crossorigin="anonymous" /> + <link rel="shortcut icon" type="image/png" href="https://app.jetsam.tech/logo.png"> - <link rel="stylesheet" href="/css/main.css?v=2"> + <link rel="stylesheet" href="/main.css?v=2"> </head> <body> -<main class="container" style="max-width: 600px"> - <header class="header"> - <img class="logo" src="https://jetsam.tech/images/logo.png" width="128px" height="128px"> - <div> - <h1>Jetsam</h1> - <h3>Your World; Cleaner</h3> - </div> - </header> - <h3 class="centered">Password Reset Successful</h3> - <p class="centered">You successfully reset your password. You can log in to the Jetsam app with the password you just created. Now go and snap some pesky plastics!</p> - <div class="row centered"> - <a href="https://jetsam.tech">Go to the Jetsam website</a> +<main class="container"> + <div class="inner"> + <div class="row header"> + <img class="logo" src="/logo.png" width="128px" height="128px"> + <div class="text-container"> + <h1>Jetsam</h1> + <h3>Your World; Cleaner</h3> + </div> + </div> + <h3 class="centered">Password Reset Successful</h3> + <p class="centered">You successfully reset your password. You can log in to the Jetsam app with the password you just created. Now go and snap some pesky plastics!</p> + <div class="row centered"> + <a href="https://jetsam.tech">Go to the Jetsam website</a> + </div> </div> </main> </body> </html> + diff --git a/views/auth/reset-password.hbs b/views/auth/reset-password.hbs index e74337f344cc347175f04d64114baa10726633e8..0b6ee245d2a631928609f6c86c278880d0f68eb2 100644 --- a/views/auth/reset-password.hbs +++ b/views/auth/reset-password.hbs @@ -12,44 +12,46 @@ <meta name="viewport" content="width=device-width, initial-scale=1"> - <link rel="shortcut icon" type="image/png" href="https://jetsam.tech/images/logo.png"> - <link rel="stylesheet" - href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css" - integrity="sha256-l85OmPOjvil/SOvVt3HnSSjzF1TUMyT9eV0c2BzEGzU=" - crossorigin="anonymous" /> - <link rel="stylesheet" - href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css" - integrity="sha256-2YQRJMXD7pIAPHiXr0s+vlRWA7GYJEK0ARns7k2sbHY=" - crossorigin="anonymous" /> + <link rel="shortcut icon" type="image/png" href="https://app.jetsam.tech/logo.png"> +<!-- <link rel="stylesheet"--> +<!-- href="https://cdnjs.cloudflare.com/ajax/libs/normalize/8.0.1/normalize.min.css"--> +<!-- integrity="sha256-l85OmPOjvil/SOvVt3HnSSjzF1TUMyT9eV0c2BzEGzU="--> +<!-- crossorigin="anonymous" />--> +<!-- <link rel="stylesheet"--> +<!-- href="https://cdnjs.cloudflare.com/ajax/libs/skeleton/2.0.4/skeleton.min.css"--> +<!-- integrity="sha256-2YQRJMXD7pIAPHiXr0s+vlRWA7GYJEK0ARns7k2sbHY="--> +<!-- crossorigin="anonymous" />--> + + <link rel="stylesheet" href="/main.css?v=2"> - <link rel="stylesheet" href="/css/main.css?v=2"> </head> <body> -<main class="container" style="max-width: 600px"> - <header class="header"> - <img class="logo" src="https://jetsam.tech/images/logo.png" width="128px" height="128px"> - <div> - <h1>Jetsam</h1> - <h3>Your World; Cleaner</h3> +<main class="container"> + <div class="inner"> + <div class="row header"> + <img class="logo" src="/logo.png" width="128px" height="128px"> + <div class="text-container"> + <h1>Jetsam</h1> + <h3>Your World; Cleaner</h3> + </div> </div> - </header> <form method="POST" action="/reset-password" id="reset_password_form"> - <h3>Reset Password</h3> - <p> + <h3 class="centered">Reset Password</h3> + <p class="centered"> You've requested a reset of your password; please use this form to choose a new one. This link is valid for 1 hour from the time we send it to you, - so if you've gone to grab a cup of tea between requesting a new password and using this form, you may need to request another password reset! + so if you've gone to grab a cup of tea between requesting a new password and using this form, you may need to request another password reset. </p> <input id=reset_token name=reset_token required type=hidden class="u-full-width" value="{{token}}"> - <div class="row"> - <div class="twelve columns"> + <div class="row centered"> + <div class="controls"> <label for=new_password><sup class="required" title="Required">*</sup>New Password:</label> <input id=new_password name=new_password class="u-full-width" required type="password"> </div> </div> - <div class="row"> - <div class="twelve columns"> + <div class="row centered"> + <div class="controls"> <label for=confirm_password><sup class="required" title="Required">*</sup>Confirm Password:</label> <input id=confirm_password name=confirm_password class="u-full-width" required type="password"> </div> @@ -61,8 +63,13 @@ </div> <div id="formmessage" class="row" style="color: red"></div> </form> + + </div> </main> + + + <script async> (function() { var script = document.createElement('script');