From 3142d0a4be4525a7a63adc648730a0e746403572 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Tue, 5 Apr 2022 10:47:15 -0500 Subject: [PATCH] Add go links and minor refactoring --- Dockerfile | 1 - ex | 12 +++-- package.json | 3 +- pnpm-lock.yaml | 28 +++++++++-- src/app/configs/redis.config.ts | 8 +++ src/app/http/controllers/Feed.controller.ts | 16 ++++++ .../http/controllers/GoLinks.controller.ts | 22 +++++++++ src/app/http/controllers/Home.controller.ts | 25 +--------- .../http/controllers/Snippets.controller.ts | 13 +---- .../parameters/LoadFeedPosts.middleware.ts | 28 +++++++++++ .../parameters/LoadSnippet.middleware.ts | 40 +++++++++++++++ src/app/http/routes/app.routes.ts | 15 ++++-- ...Z_CreateGolinksTableMigration.migration.ts | 49 +++++++++++++++++++ src/app/models/GoLink.model.ts | 24 +++++++++ src/app/resources/views/welcome.pug | 19 ++----- 15 files changed, 238 insertions(+), 65 deletions(-) create mode 100644 src/app/configs/redis.config.ts create mode 100644 src/app/http/controllers/Feed.controller.ts create mode 100644 src/app/http/controllers/GoLinks.controller.ts create mode 100644 src/app/http/middlewares/parameters/LoadFeedPosts.middleware.ts create mode 100644 src/app/http/middlewares/parameters/LoadSnippet.middleware.ts create mode 100644 src/app/migrations/2022-04-04T19:22:30.795Z_CreateGolinksTableMigration.migration.ts create mode 100644 src/app/models/GoLink.model.ts diff --git a/Dockerfile b/Dockerfile index a42d87e..208ba88 100644 --- a/Dockerfile +++ b/Dockerfile @@ -8,7 +8,6 @@ COPY lib/ /app RUN rm -f /app/.env -COPY .env.docker /app/.env COPY package.json /app COPY pnpm-lock.yaml /app diff --git a/ex b/ex index 3ee08e1..9009206 100755 --- a/ex +++ b/ex @@ -1,4 +1,4 @@ -#!/bin/bash -e +#!/bin/bash ENV_NODE="$(which node)" ENV_PNPM="$(which pnpm)" @@ -134,7 +134,13 @@ if [ ! -d "./node_modules" ]; then printf "\033[32m✓\033[39m Looks like you're all set up! Run this command again to access the Extollo CLI.\n" else start_spinner "Building your app..." - "$ENV_PNPM" run build > /dev/null - stop_spinner 0 + BUILD_OUTPUT="$($ENV_PNPM run build 2>&1)" + BUILD_EC=$? + stop_spinner $BUILD_EC + if [ $BUILD_EC -ne 0 ]; then + printf "\033[31m✘\033[39m Uh, oh! Looks like your application failed to build. (exit: $BUILD_EC)" + echo "$BUILD_OUTPUT" + exit $BUILD_EC + fi "$ENV_NODE" --experimental-repl-await ./lib/cli.js $@ fi diff --git a/package.json b/package.json index 754d6a7..a295b3f 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,9 @@ "lib": "lib" }, "dependencies": { - "@extollo/lib": "^0.9.21", + "@extollo/lib": "^0.9.28", "copyfiles": "^2.4.1", + "feed": "^4.2.2", "rimraf": "^3.0.2", "ts-expose-internals": "^4.5.4", "ts-patch": "^2.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5baaac1..d0c5809 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -2,8 +2,9 @@ lockfileVersion: 5.3 specifiers: '@extollo/cc': ^0.6.0 - '@extollo/lib': ^0.9.21 + '@extollo/lib': ^0.9.28 copyfiles: ^2.4.1 + feed: ^4.2.2 rimraf: ^3.0.2 ts-expose-internals: ^4.5.4 ts-patch: ^2.0.1 @@ -12,8 +13,9 @@ specifiers: zod: ^3.11.6 dependencies: - '@extollo/lib': 0.9.21 + '@extollo/lib': 0.9.28 copyfiles: 2.4.1 + feed: 4.2.2 rimraf: 3.0.2 ts-expose-internals: 4.5.4 ts-patch: 2.0.1_typescript@4.3.2 @@ -110,8 +112,8 @@ packages: - supports-color dev: true - /@extollo/lib/0.9.21: - resolution: {integrity: sha512-JKgUQWS9/lEho9ardayvZO8pkE4ZoPs4xuo6KAQDqp7wq4PjGjrGBKeHX7ViHeaFm/XelskaG9eu9Ox9RvirFQ==} + /@extollo/lib/0.9.28: + resolution: {integrity: sha512-lMI2FWOTKbRsqJrOAvZofzfz0DaNwWecYsDX98XkNbktaUjhaGc7xr1OVxscxUC/Edapyj/p02MLdsUoIsARxA==} dependencies: '@atao60/fse-cli': 0.1.7 '@extollo/ui': 0.1.0_@types+node@14.18.12 @@ -987,6 +989,13 @@ packages: dependencies: reusify: 1.0.4 + /feed/4.2.2: + resolution: {integrity: sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==} + engines: {node: '>=0.4.0'} + dependencies: + xml-js: 1.6.11 + dev: false + /fetch-blob/3.1.5: resolution: {integrity: sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==} engines: {node: ^12.20 || >= 14.13} @@ -2001,6 +2010,10 @@ packages: /safer-buffer/2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + /sax/1.2.4: + resolution: {integrity: sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==} + dev: false + /semver/5.7.1: resolution: {integrity: sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==} hasBin: true @@ -2577,6 +2590,13 @@ packages: /wrappy/1.0.2: resolution: {integrity: sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=} + /xml-js/1.6.11: + resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==} + hasBin: true + dependencies: + sax: 1.2.4 + dev: false + /xtend/4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} diff --git a/src/app/configs/redis.config.ts b/src/app/configs/redis.config.ts new file mode 100644 index 0000000..a28878d --- /dev/null +++ b/src/app/configs/redis.config.ts @@ -0,0 +1,8 @@ +import {env, RedisOptions} from '@extollo/lib' + +export default { + connection: { + port: env('REDIS_PORT', 6379), + host: env('REDIS_HOST', '127.0.0.1'), + } as RedisOptions +} diff --git a/src/app/http/controllers/Feed.controller.ts b/src/app/http/controllers/Feed.controller.ts new file mode 100644 index 0000000..03b0e50 --- /dev/null +++ b/src/app/http/controllers/Feed.controller.ts @@ -0,0 +1,16 @@ +import {Controller, view, Injectable, Collection} from '@extollo/lib' +import {FeedPost} from '../../models/FeedPost.model' + +/** + * Feed Controller + * ------------------------------------ + * Backend for routes related to my post feed. + */ +@Injectable() +export class Feed extends Controller { + public async feed(feedPosts: Collection) { + return view('feed', { + feedPosts: feedPosts.toArray(), + }) + } +} diff --git a/src/app/http/controllers/GoLinks.controller.ts b/src/app/http/controllers/GoLinks.controller.ts new file mode 100644 index 0000000..d0d46a9 --- /dev/null +++ b/src/app/http/controllers/GoLinks.controller.ts @@ -0,0 +1,22 @@ +import {Controller, view, Inject, Injectable, HTTPStatus, http, redirect} from '@extollo/lib' +import {GoLink} from '../../models/GoLink.model' + +/** + * GoLinks Controller + */ +@Injectable() +export class GoLinks extends Controller { + async launch() { + const short = this.request.safe('short').string() + const link = await GoLink.query() + .where('active', '=', true) + .where('short', '=', short) + .first() + + if ( !link ) { + return http(HTTPStatus.http404, 'Invalid or expired link.') + } + + return redirect(link.url) + } +} diff --git a/src/app/http/controllers/Home.controller.ts b/src/app/http/controllers/Home.controller.ts index c8e58fa..30355ad 100644 --- a/src/app/http/controllers/Home.controller.ts +++ b/src/app/http/controllers/Home.controller.ts @@ -13,9 +13,8 @@ export class Home extends Controller { @Inject() protected readonly routing!: Routing - public async welcome() { + public async welcome(feedPosts: Collection) { const workItems = await this.getWorkItems() - const feedPosts = await this.getFeedPosts() return view('welcome', { feedPosts: feedPosts.toArray(), @@ -30,13 +29,6 @@ export class Home extends Controller { }) } - public async feed() { - const feedPosts = await this.getFeedPosts(true) - return view('feed', { - feedPosts: feedPosts.toArray(), - }) - } - public technical() { const isOptOut = this.request.cookies.has(this.config.get('app.analytics.optOutCookie')) @@ -73,21 +65,6 @@ export class Home extends Controller { }) } - protected async getFeedPosts(all = false): Promise> { - const query = FeedPost.query() - .orderByDescending('posted_at') - - if ( !all ) { - query.limit(6) - } - - if ( !this.security.hasUser() ) { - query.where('visible', '=', true) - } - - return query.get().collect() - } - protected async getWorkItems(): Promise> { const query = WorkItem.query() .orderByDescending('start_date') diff --git a/src/app/http/controllers/Snippets.controller.ts b/src/app/http/controllers/Snippets.controller.ts index 72d5c50..2ff360f 100644 --- a/src/app/http/controllers/Snippets.controller.ts +++ b/src/app/http/controllers/Snippets.controller.ts @@ -4,23 +4,14 @@ import {Snippet} from '../../models/Snippet.model' /** * Snippets Controller * ------------------------------------ - * Put some description here. + * Backend for routes that deal with code snippets. */ @Injectable() export class Snippets extends Controller { @Inject() protected readonly logging!: Logging - public async viewSnippet() { - const slug = this.request.safe('slug').string() - const snippet = await Snippet.query() - .where('slug', '=', slug) - .first() - - if ( !snippet ) { - return http(HTTPStatus.http404, 'Snippet URL is invalid.') - } - + public async viewSnippet(snippet: Snippet) { const needsAccessKey = snippet?.accessKey && snippet.accessKey !== this.request.input('accessKey') const needsConfirm = snippet?.singleAccessOnly && !this.request.input('confirmSingleAccess') diff --git a/src/app/http/middlewares/parameters/LoadFeedPosts.middleware.ts b/src/app/http/middlewares/parameters/LoadFeedPosts.middleware.ts new file mode 100644 index 0000000..4775d85 --- /dev/null +++ b/src/app/http/middlewares/parameters/LoadFeedPosts.middleware.ts @@ -0,0 +1,28 @@ +import {Injectable, ParameterMiddleware, Collection, SecurityContext, Either, ResponseObject, right, Inject} from '@extollo/lib' +import {FeedPost} from '../../../models/FeedPost.model' + +/** + * LoadFeedPosts Middleware + * -------------------------------------------- + * Load feed posts visible to the user as a handler parameter. + */ +@Injectable() +export class LoadFeedPosts extends ParameterMiddleware, [] | [{ all: boolean }]> { + @Inject() + protected readonly security!: SecurityContext + + async handle({ all = false } = {}): Promise>> { + const query = FeedPost.query() + .orderByDescending('posted_at') + + if ( !all ) { + query.limit(6) + } + + if ( !this.security.hasUser() ) { + query.where('visible', '=', true) + } + + return right(await query.get().collect()) + } +} diff --git a/src/app/http/middlewares/parameters/LoadSnippet.middleware.ts b/src/app/http/middlewares/parameters/LoadSnippet.middleware.ts new file mode 100644 index 0000000..8ccf715 --- /dev/null +++ b/src/app/http/middlewares/parameters/LoadSnippet.middleware.ts @@ -0,0 +1,40 @@ +import { + Either, + http, + HTTPStatus, + Inject, + Injectable, + left, + ParameterMiddleware, + ResponseObject, right, + SecurityContext, +} from '@extollo/lib' +import {Snippet} from '../../../models/Snippet.model' + +/** + * LoadSnippet Middleware + * -------------------------------------------- + * Look up the Snippet instance from the request parameters. + */ +@Injectable() +export class LoadSnippet extends ParameterMiddleware { + @Inject() + protected readonly security!: SecurityContext + + async handle(): Promise> { + const slug = String(this.request.input('slug') || '') + if ( !slug ) { + return left(http(HTTPStatus.http404)) + } + + const snippet = await Snippet.query() + .where('slug', '=', slug) + .first() + + if ( !snippet || snippet.usersOnly && !this.security.hasUser() ) { + return left(http(HTTPStatus.http404)) + } + + return right(snippet) + } +} diff --git a/src/app/http/routes/app.routes.ts b/src/app/http/routes/app.routes.ts index ec30d7e..cdb5c67 100644 --- a/src/app/http/routes/app.routes.ts +++ b/src/app/http/routes/app.routes.ts @@ -2,10 +2,15 @@ import {Route, SessionAuthMiddleware} from '@extollo/lib' import {Home} from '../controllers/Home.controller' import {PageView} from '../middlewares/PageView.middleware' import {Snippets} from '../controllers/Snippets.controller' +import {GoLinks} from '../controllers/GoLinks.controller' +import {Feed} from '../controllers/Feed.controller' +import {LoadSnippet} from '../middlewares/parameters/LoadSnippet.middleware' +import {LoadFeedPosts} from '../middlewares/parameters/LoadFeedPosts.middleware' Route .group('/', () => { Route.get('/') + .parameterMiddleware(LoadFeedPosts) .calls(Home, home => home.welcome) .alias('home') @@ -21,16 +26,16 @@ Route .alias('opt-out') Route.get('/feed') - .calls(Home, home => home.feed) + .parameterMiddleware(LoadFeedPosts, {all: true}) + .calls(Feed, feed => feed.feed) .alias('feed') Route.get('/snippet/:slug') + .parameterMiddleware(LoadSnippet) .calls(Snippets, snippets => snippets.viewSnippet) - Route.get('/test') - .handledBy(() => { - return 'Hello, World!' - }) + Route.any('/go/:short') + .calls(GoLinks, go => go.launch) }) .pre(SessionAuthMiddleware) .pre(PageView) diff --git a/src/app/migrations/2022-04-04T19:22:30.795Z_CreateGolinksTableMigration.migration.ts b/src/app/migrations/2022-04-04T19:22:30.795Z_CreateGolinksTableMigration.migration.ts new file mode 100644 index 0000000..1ae2c10 --- /dev/null +++ b/src/app/migrations/2022-04-04T19:22:30.795Z_CreateGolinksTableMigration.migration.ts @@ -0,0 +1,49 @@ +import {DatabaseService, FieldType, Inject, Injectable, Migration} from '@extollo/lib' + +/** + * CreateGolinksTableMigration + * ---------------------------------- + * Create Table to store URL redirections + */ +@Injectable() +export default class CreateGolinksTableMigration extends Migration { + @Inject() + protected readonly db!: DatabaseService + + /** + * Apply the migration. + */ + async up(): Promise { + const schema = this.db.get().schema() + const table = await schema.table('go_links') + + table.primaryKey('go_link_id').required() + + table.column('active') + .type(FieldType.bool) + .default(true) + + table.column('short') + .type(FieldType.varchar) + .required() + .unique() + + table.column('url') + .type(FieldType.text) + .required() + + await schema.commit(table) + } + + /** + * Undo the migration. + */ + async down(): Promise { + const schema = this.db.get().schema() + const table = await schema.table('go_links') + + table.dropIfExists() + + await schema.commit(table) + } +} diff --git a/src/app/models/GoLink.model.ts b/src/app/models/GoLink.model.ts new file mode 100644 index 0000000..7f4c2a7 --- /dev/null +++ b/src/app/models/GoLink.model.ts @@ -0,0 +1,24 @@ +import {Field, FieldType, Injectable, Model} from '@extollo/lib' + +/** + * GoLink Model + * ----------------------------------- + * A shortened URL. + */ +@Injectable() +export class GoLink extends Model { + protected static table = 'go_links' + protected static key = 'go_link_id' + + @Field(FieldType.serial, 'go_link_id') + protected id!: number + + @Field(FieldType.bool) + public active = true + + @Field(FieldType.varchar) + public short!: string + + @Field(FieldType.text) + public url!: string +} diff --git a/src/app/resources/views/welcome.pug b/src/app/resources/views/welcome.pug index e8357cd..3cd717a 100644 --- a/src/app/resources/views/welcome.pug +++ b/src/app/resources/views/welcome.pug @@ -64,19 +64,6 @@ block content p.text !{item.body} .bottom .stamp permalink  |  #{item.postedAt.toLocaleString()} - // each item in feed_items - // .feed-item - // div.feed-category(id='feedPostTag_' + item.id) - // .tag #{item.tag} - // span - // if item.draft - // p.text (draft) - // p.text !{item.text} - // .bottom - // if user && can_access.feed_delete && can_access.feed_edit - // .stamp edit | delete - // .stamp permalink  |  #{item.date.toLocaleString()} - if feed_overflow - .row.mt-4 - .col-12.text-center - a.button(href="/feed") view more + .row.mt-4 + .col-12.text-center + a.button(href="/feed") view all