Skip to content

Commit

Permalink
Placename search settings for web and mobile
Browse files Browse the repository at this point in the history
  • Loading branch information
newmanw committed Dec 13, 2023
1 parent 2e2962c commit fcc949f
Show file tree
Hide file tree
Showing 39 changed files with 1,088 additions and 127 deletions.
83 changes: 83 additions & 0 deletions service/src/adapters/settings/adapters.settings.controllers.web.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import express from 'express'
import { WebAppRequestFactory } from '../adapters.controllers.web'
import { GetSettingsServices, UpdateMapSettingsRequest, UpdateSettingsServices } from '../../app.api/settings/app.api.settings'
import { MobileSearchType, WebSearchType } from '../../entities/settings/entities.settings'
import { URL } from 'url'

export interface SettingsAppLayer {
getMapSettings: GetSettingsServices,
updateMapSettings: UpdateSettingsServices
}

function validateUrl(url: string): boolean {
try {
new URL(url)
return true
} catch (e) { return false }
}

export function SettingsRoutes(app: SettingsAppLayer, createAppRequest: WebAppRequestFactory) {
const routes = express.Router()
routes.use(express.json())

routes.route('/map')
.get(async (req, res) => {
const appReq = createAppRequest(req)
const appRes = await app.getMapSettings(appReq)
if (appRes.success) {
return res.json(appRes.success)
} else {
return res.sendStatus(404)
}
})

routes.route('/map')
.post(async (req, res) => {
const {
webSearchType: webSearchTypeParameter,
mobileSearchType: mobileSearchTypeParameter,
webNominatimUrl,
mobileNominatimUrl
} = req.body

const webSearchType = WebSearchType[webSearchTypeParameter as keyof typeof WebSearchType]
if (!webSearchType) {
return res.status(400).json({ message: 'Web search option is required.' })
}

const mobileSearchType = MobileSearchType[mobileSearchTypeParameter as keyof typeof MobileSearchType]
if (!mobileSearchType) {
return res.status(400).json({ message: 'Mobile search option is required.' })
}

if (webSearchType === WebSearchType.NOMINATIM) {
if (!validateUrl(webNominatimUrl)) {
return res.status(400).json({ message: 'Web Nominatim URL is required and must be a valid URL.' })
}
}

if (mobileSearchType === MobileSearchType.NOMINATIM) {
if (!validateUrl(mobileNominatimUrl)) {
return res.status(400).json({ message: 'Mobile Nominatim URL is required and must be a vaild URL.' })
}
}

const settings: UpdateMapSettingsRequest['settings'] = {
webSearchType: webSearchType,
webNominatimUrl: req.body.webNominatimUrl,
mobileSearchType: mobileSearchType,
mobileNominatimUrl: req.body.mobileNominatimUrl
}

const appReq: UpdateMapSettingsRequest = createAppRequest(req, { settings })

const appRes = await app.updateMapSettings(appReq)
if (appRes.success) {
return res.json(appRes.success)
} else {
return res.sendStatus(400)
}
})

return routes
}
33 changes: 33 additions & 0 deletions service/src/adapters/settings/adapters.settings.db.mongoose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { BaseMongooseRepository } from '../base/adapters.base.db.mongoose'
import * as legacy from '../../models/setting'
import _ from 'lodash'
import mongoose from 'mongoose'
import { MapSettings, SettingRepository } from '../../entities/settings/entities.settings'

export type SettingsDocument = legacy.SettingsDocument
export type SettingsModel = mongoose.Model<SettingsDocument>
export const SettingsSchema = legacy.Model.schema

export class MongooseSettingsRepository extends BaseMongooseRepository<SettingsDocument, SettingsModel, MapSettings> implements SettingRepository {

constructor(model: mongoose.Model<SettingsDocument>) {
super(model, {
docToEntity: doc => {
const json = doc.toJSON()
return {
...json
}
}
})
}

async getMapSettings(): Promise<MapSettings | null> {
const document = await this.model.findOne({ type: 'map' })
return document?.settings
}

async updateMapSettings(settings: MapSettings): Promise<MapSettings | null> {
const document = await this.model.findOneAndUpdate({ type: 'map' }, { settings }, { new: true, upsert: true })
return document?.settings
}
}
20 changes: 20 additions & 0 deletions service/src/app.api/settings/app.api.settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { MapSettings } from '../../entities/settings/entities.settings'
import { PermissionDeniedError } from '../app.api.errors'
import { AppRequest, AppRequestContext, AppResponse } from '../app.api.global'

export interface UpdateMapSettingsRequest extends AppRequest {
settings: MapSettings
}

export interface GetSettingsServices {
(req: AppRequest): Promise<AppResponse<MapSettings | null, PermissionDeniedError>>
}

export interface UpdateSettingsServices {
(req: UpdateMapSettingsRequest): Promise<AppResponse<MapSettings | null, PermissionDeniedError>>
}

export interface SettingsPermissionService {
ensureFetchMapSettingsPermissionFor(context: AppRequestContext): Promise<PermissionDeniedError | null>
ensureUpdateMapSettingsPermissionFor(context: AppRequestContext): Promise<PermissionDeniedError | null>
}
26 changes: 26 additions & 0 deletions service/src/app.impl/settings/app.impl.settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import * as api from '../../app.api/settings/app.api.settings'
import { AppRequest, KnownErrorsOf, withPermission } from '../../app.api/app.api.global'
import { MapSettings, SettingRepository } from '../../entities/settings/entities.settings'
import { UpdateMapSettingsRequest } from '../../app.api/settings/app.api.settings'

export function FetchMapSettings(settingRepo: SettingRepository, permissionService: api.SettingsPermissionService): api.GetSettingsServices {
return async function getMapSettings(req: AppRequest): ReturnType<api.GetSettingsServices> {
return await withPermission<MapSettings | null, KnownErrorsOf<api.GetSettingsServices>>(
permissionService.ensureFetchMapSettingsPermissionFor(req.context),
async (): Promise<MapSettings | null> => {
return await settingRepo.getMapSettings()
}
)
}
}

export function UpdateMapSettings(settingRepo: SettingRepository, permissionService: api.SettingsPermissionService): api.UpdateSettingsServices {
return async function updateMapSettings(req: UpdateMapSettingsRequest): ReturnType<api.UpdateSettingsServices> {
return await withPermission<MapSettings | null, KnownErrorsOf<api.UpdateSettingsServices>>(
permissionService.ensureUpdateMapSettingsPermissionFor(req.context),
async (): Promise<MapSettings | null> => {
return await settingRepo.updateMapSettings(req.settings)
}
)
}
}
40 changes: 39 additions & 1 deletion service/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ import { FileSystemAttachmentStoreInitError, intializeAttachmentStore } from './
import { AttachmentStoreToken, ObservationRepositoryToken } from './plugins.api/plugins.api.observations'
import { GetDbConnection, MongooseDbConnectionToken } from './plugins.api/plugins.api.db'
import { EventEmitter } from 'events'
import { SettingsAppLayer, SettingsRoutes } from './adapters/settings/adapters.settings.controllers.web'
import { MongooseSettingsRepository, SettingsModel } from './adapters/settings/adapters.settings.db.mongoose'
import { FetchMapSettings, UpdateMapSettings } from './app.impl/settings/app.impl.settings'
import { RoleBasedMapPermissionService } from './permissions/permissions.settings'
import { SettingRepository } from './entities/settings/entities.settings'


export interface MageService {
Expand Down Expand Up @@ -217,6 +222,9 @@ type DatabaseLayer = {
},
users: {
user: UserModel
},
settings: {
setting: SettingsModel
}
}

Expand Down Expand Up @@ -252,7 +260,8 @@ type AppLayer = {
deleteFeed: feedsApi.DeleteFeed
},
icons: StaticIconsAppLayer,
users: UsersAppLayer
users: UsersAppLayer,
settings: SettingsAppLayer
}

async function initDatabase(): Promise<DatabaseLayer> {
Expand Down Expand Up @@ -296,6 +305,9 @@ async function initDatabase(): Promise<DatabaseLayer> {
},
users: {
user: require('./models/user').Model
},
settings: {
setting: require('./models/setting').Model
}
}
}
Expand All @@ -318,6 +330,9 @@ type Repositories = {
},
users: {
userRepo: UserRepository
},
settings: {
settingRepo: SettingRepository
}
}

Expand Down Expand Up @@ -346,6 +361,7 @@ async function initRepositories(models: DatabaseLayer, config: BootConfig): Prom
new FileSystemIconContentStore(),
[ new PluginUrlScheme(config.plugins?.servicePlugins || []) ])
const userRepo = new MongooseUserRepository(models.users.user)
const settingRepo = new MongooseSettingsRepository(models.settings.setting)
const attachmentStore = await intializeAttachmentStore(environment.attachmentBaseDirectory)
if (attachmentStore instanceof FileSystemAttachmentStoreInitError) {
throw attachmentStore
Expand All @@ -366,6 +382,9 @@ async function initRepositories(models: DatabaseLayer, config: BootConfig): Prom
},
users: {
userRepo
},
settings: {
settingRepo
}
}
}
Expand All @@ -376,12 +395,14 @@ async function initAppLayer(repos: Repositories): Promise<AppLayer> {
const icons = await initIconsAppLayer(repos)
const feeds = await initFeedsAppLayer(repos)
const users = await initUsersAppLayer(repos)
const settings = await initSettingsAppLayer(repos)
return {
events,
observations,
feeds,
icons,
users,
settings
}
}

Expand Down Expand Up @@ -463,6 +484,16 @@ function initFeedsAppLayer(repos: Repositories): AppLayer['feeds'] {
}
}

async function initSettingsAppLayer(repos: Repositories): Promise<AppLayer['settings']> {
const mapPermissions = new RoleBasedMapPermissionService()
const getMapSettings = FetchMapSettings(repos.settings.settingRepo, mapPermissions)
const updateMapSettings = UpdateMapSettings(repos.settings.settingRepo, mapPermissions)
return {
getMapSettings,
updateMapSettings
}
}

interface MageEventRequestContext extends AppRequestContext<UserDocument> {
event: MageEventDocument | MageEvent | undefined
}
Expand All @@ -483,6 +514,13 @@ async function initWebLayer(repos: Repositories, app: AppLayer, webUIPlugins: st
}
}
const bearerAuth = webAuth.passport.authenticate('bearer')

const settingsRoutes = SettingsRoutes(app.settings, appRequestFactory)
webController.use('/api/settings', [
bearerAuth,
settingsRoutes
])

const usersRoutes = UsersRoutes(app.users, appRequestFactory)
/*
TODO: cannot mount at /api/users/search because the /api/users/:userId route
Expand Down
4 changes: 3 additions & 1 deletion service/src/entities/authorization/entities.permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ export enum TeamPermission {

export enum SettingPermission {
READ_SETTINGS = 'READ_SETTINGS',
UPDATE_SETTINGS = 'UPDATE_SETTINGS'
UPDATE_SETTINGS = 'UPDATE_SETTINGS',
MAP_SETTINGS_READ = 'MAP_SETTINGS_READ',
MAP_SETTINGS_UPDATE = 'MAP_SETTINGS_UPDATE',
}

export enum FeedsPermission {
Expand Down
22 changes: 22 additions & 0 deletions service/src/entities/settings/entities.settings.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
export enum WebSearchType {
NONE = "NONE",
NOMINATIM = "NOMINATIM"
}

export enum MobileSearchType {
NONE = "NONE",
NATIVE = "NATIVE",
NOMINATIM = "NOMINATIM"
}

export interface MapSettings {
webSearchType: WebSearchType
webNominatimUrl: string | null
mobileSearchType: MobileSearchType
mobileNominatimUrl: string | null
}

export interface SettingRepository {
getMapSettings(): Promise<MapSettings | null>
updateMapSettings(settings: MapSettings): Promise<MapSettings | null>
}
40 changes: 40 additions & 0 deletions service/src/migrations/030-map-search-settings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
exports.id = 'map-search-settings';

exports.up = async function (done) {

const roles = this.db.collection('roles');
try {
await roles.update({name: 'USER_ROLE'}, { $push: { permissions: 'MAP_SETTINGS_READ' } });
} catch (e) { done(e) }

try {
await roles.update({ name: 'USER_NO_EDIT_ROLE' }, { $push: { permissions: 'MAP_SETTINGS_READ' } });
} catch (e) { done(e) }


try {
await roles.update({ name: 'EVENT_MANAGER_ROLE' }, { $push: { permissions: 'MAP_SETTINGS_READ' } });
} catch (e) { done(e) }

try {
await roles.update({ name: 'ADMIN_ROLE' }, { $push: { permissions: { $each: ['MAP_SETTINGS_READ', 'MAP_SETTINGS_UPDATE'] } } });
} catch (e) { done(e) }

const settings = this.db.collection('settings');
try {
await settings.insertOne({
type: 'map',
settings: {
webSearchType: "NOMINATIM",
webNominatimUrl: "https://nominatim.openstreetmap.org",
mobileSearchType: "NATIVE",
}
});
} catch (e) { done(e) }

done();
};

exports.down = function (done) {
done();
};
9 changes: 9 additions & 0 deletions service/src/models/setting.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import mongoose from 'mongoose'

export interface SettingsDocument extends mongoose.Document {
_id: mongoose.Types.ObjectId
type: string
settings: any
}

export declare const Model: mongoose.Model<SettingsDocument>
1 change: 1 addition & 0 deletions service/src/models/setting.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ SettingSchema.set("toJSON", {

// Creates the Model for the Setting Schema
const Setting = mongoose.model('Setting', SettingSchema);
exports.Model = Setting;

exports.getSettings = function () {
return Setting.find({}).exec();
Expand Down
1 change: 1 addition & 0 deletions service/src/permissions/permissions.role-based.base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ export function ensureContextUserHasPermission(context: AppRequestContext<UserWi
if (role.permissions.includes(permission)) {
return null
}

return permissionDenied(permission, user.username)
}
Loading

0 comments on commit fcc949f

Please sign in to comment.