Add switches to enable some more privacy, fix all errors in osmAuth

This commit is contained in:
Pieter Vander Vennet 2024-05-06 18:58:19 +02:00
parent a856d8edc9
commit fbf23b6e18
6 changed files with 128 additions and 132 deletions

View file

@ -484,6 +484,41 @@
}
]
},
{
"id": "more_privacy_theme_override",
"mappings": [
{
"if": "__featureSwitchMorePrivacy=true",
"then": {
"en": "This theme is sensitive. Making changes will not indicate if you were nearby explicitly."
}
}
]
},
{
"id": "more_privacy",
"question":
{
"en": "When making changes, should a rough indication be given how far away you were from the object?"
},
"questionHint": {
"en": "If you make a change to one or more objects and you enabled your location, a rough indication of where you made will be saved: it is indicated if you were closer then 25m, 500m, 5km or further away then 5km. This helps mappers understand your context when making changes, but gives an indication of where you were at this time. "
},
"mappings": [
{
"if": "mapcomplete-more_privacy=yes",
"then": {
"en": "When making changes to OpenStreetMap, do not indicate how far away you were from the changed objects."
}
},
{
"if": "mapcomplete-more_privacy=no",
"then": {
"en": "When making changes to OpenStreetMap, roughly indicate how far away you were from the changed objects. This helps other contributors to understand how you made the change"
}
}
]
},
{
"id": "mangrove-keys",
"render": {

29
package-lock.json generated
View file

@ -55,7 +55,7 @@
"monaco-editor": "^0.46.0",
"npm": "^10.7.0",
"opening_hours": "^3.6.0",
"osm-auth": "^2.2.0",
"osm-auth": "^2.5.0",
"osmtogeojson": "^3.0.0-beta.5",
"panzoom": "^9.4.3",
"papaparse": "^5.3.1",
@ -14631,13 +14631,11 @@
}
},
"node_modules/osm-auth": {
"version": "2.2.0",
"license": "ISC",
"dependencies": {
"store": "~2.0.12"
},
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/osm-auth/-/osm-auth-2.5.0.tgz",
"integrity": "sha512-w3NnYbt+0PIih2Kwr1sLfQWehdLbcA3gZNJhX4VOBfeRtvm30iZA3nURphuZDokZ8Kmdv4LWB+AiIng2b+KvIA==",
"engines": {
"node": ">=16"
"node": ">=18.18"
}
},
"node_modules/osm-polygon-features": {
@ -17081,13 +17079,6 @@
"version": "3.3.2",
"license": "MIT"
},
"node_modules/store": {
"version": "2.0.12",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/stream-to-string": {
"version": "1.2.1",
"license": "MIT",
@ -29202,10 +29193,9 @@
}
},
"osm-auth": {
"version": "2.2.0",
"requires": {
"store": "~2.0.12"
}
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/osm-auth/-/osm-auth-2.5.0.tgz",
"integrity": "sha512-w3NnYbt+0PIih2Kwr1sLfQWehdLbcA3gZNJhX4VOBfeRtvm30iZA3nURphuZDokZ8Kmdv4LWB+AiIng2b+KvIA=="
},
"osm-polygon-features": {
"version": "0.9.2"
@ -30803,9 +30793,6 @@
"std-env": {
"version": "3.3.2"
},
"store": {
"version": "2.0.12"
},
"stream-to-string": {
"version": "1.2.1",
"requires": {

View file

@ -173,7 +173,7 @@
"monaco-editor": "^0.46.0",
"npm": "^10.7.0",
"opening_hours": "^3.6.0",
"osm-auth": "^2.2.0",
"osm-auth": "^2.5.0",
"osmtogeojson": "^3.0.0-beta.5",
"panzoom": "^9.4.3",
"papaparse": "^5.3.1",

View file

@ -1,4 +1,3 @@
// @ts-ignore
import { osmAuth } from "osm-auth"
import { Store, Stores, UIEventSource } from "../UIEventSource"
import { OsmPreferences } from "./OsmPreferences"
@ -6,7 +5,18 @@ import { Utils } from "../../Utils"
import { LocalStorageSource } from "../Web/LocalStorageSource"
import { AuthConfig } from "./AuthConfig"
import Constants from "../../Models/Constants"
import OSMAuthInstance = OSMAuth.OSMAuthInstance
interface OsmUserInfo {
id: number
display_name: string
account_created: string
description: string
contributor_terms: { agreed: boolean }
roles: []
changesets: { count: number }
traces: { count: number }
blocks: { received: { count: number; active: number } }
}
export default class UserDetails {
public loggedIn = false
@ -31,7 +41,7 @@ export default class UserDetails {
export type OsmServiceState = "online" | "readonly" | "offline" | "unknown" | "unreachable"
export class OsmConnection {
public auth: OSMAuthInstance
public auth: osmAuth
public userDetails: UIEventSource<UserDetails>
public isLoggedIn: Store<boolean>
public gpxServiceIsOnline: UIEventSource<OsmServiceState> = new UIEventSource<OsmServiceState>(
@ -49,7 +59,7 @@ export class OsmConnection {
private readonly _dryRun: Store<boolean>
private readonly fakeUser: boolean
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = []
private readonly _iframeMode: Boolean | boolean
private readonly _iframeMode: boolean
private readonly _singlePage: boolean
private isChecking = false
private readonly _doCheckRegularly
@ -99,20 +109,19 @@ export class OsmConnection {
ud.languages = ["en"]
this.loadingStatus.setData("logged-in")
}
const self = this
this.UpdateCapabilities()
this.isLoggedIn = this.userDetails.map(
(user) =>
user.loggedIn &&
(self.apiIsOnline.data === "unknown" || self.apiIsOnline.data === "online"),
(this.apiIsOnline.data === "unknown" || this.apiIsOnline.data === "online"),
[this.apiIsOnline]
)
this.isLoggedIn.addCallback((isLoggedIn) => {
if (self.userDetails.data.loggedIn == false && isLoggedIn == true) {
if (this.userDetails.data.loggedIn == false && isLoggedIn == true) {
// We have an inconsistency: the userdetails say we _didn't_ log in, but this actor says we do
// This means someone attempted to toggle this; so we attempt to login!
self.AttemptLogin()
this.AttemptLogin()
}
})
@ -120,17 +129,16 @@ export class OsmConnection {
this.updateAuthObject()
if (!this.fakeUser) {
self.CheckForMessagesContinuously()
this.CheckForMessagesContinuously()
}
this.preferencesHandler = new OsmPreferences(this.auth, this, this.fakeUser)
if (options.oauth_token?.data !== undefined) {
console.log(options.oauth_token.data)
const self = this
this.auth.bootstrapToken(options.oauth_token.data, (err, result) => {
console.log("Bootstrap token called back", err, result)
self.AttemptLogin()
this.AttemptLogin()
})
options.oauth_token.setData(undefined)
@ -142,15 +150,15 @@ export class OsmConnection {
}
}
public GetPreference(
public GetPreference<T extends string = string>(
key: string,
defaultValue: string = undefined,
options?: {
documentation?: string
prefix?: string
}
): UIEventSource<string> {
return this.preferencesHandler.GetPreference(key, defaultValue, options)
): UIEventSource<T | undefined> {
return <UIEventSource<T>>this.preferencesHandler.GetPreference(key, defaultValue, options)
}
public GetLongPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource<string> {
@ -192,7 +200,7 @@ export class OsmConnection {
console.log("AttemptLogin called, but ignored as fakeUser is set")
return
}
const self = this
console.log("Trying to log in...")
this.updateAuthObject()
@ -202,33 +210,33 @@ export class OsmConnection {
this.auth.xhr(
{
method: "GET",
path: "/api/0.6/user/details",
path: "/api/0.6/user/details"
},
function (err, details: XMLDocument) {
(err, details: XMLDocument) => {
if (err != null) {
console.log("Could not login due to:", err)
self.loadingStatus.setData("error")
this.loadingStatus.setData("error")
if (err.status == 401) {
console.log("Clearing tokens...")
// Not authorized - our token probably got revoked
self.auth.logout()
self.LogOut()
this.auth.logout()
this.LogOut()
} else {
console.log("Other error. Status:", err.status)
self.apiIsOnline.setData("unreachable")
this.apiIsOnline.setData("unreachable")
}
return
}
if (details == null) {
self.loadingStatus.setData("error")
this.loadingStatus.setData("error")
return
}
// details is an XML DOM of user details
let userInfo = details.getElementsByTagName("user")[0]
const userInfo = details.getElementsByTagName("user")[0]
let data = self.userDetails.data
const data = this.userDetails.data
data.loggedIn = true
console.log("Login completed, userinfo is ", userInfo)
data.name = userInfo.getAttribute("display_name")
@ -261,18 +269,18 @@ export class OsmConnection {
data.home = { lat: lat, lon: lon }
}
self.loadingStatus.setData("logged-in")
this.loadingStatus.setData("logged-in")
const messages = userInfo
.getElementsByTagName("messages")[0]
.getElementsByTagName("received")[0]
data.unreadMessages = parseInt(messages.getAttribute("unread"))
data.totalMessages = parseInt(messages.getAttribute("count"))
self.userDetails.ping()
for (const action of self._onLoggedIn) {
action(self.userDetails.data)
this.userDetails.ping()
for (const action of this._onLoggedIn) {
action(this.userDetails.data)
}
self._onLoggedIn = []
this._onLoggedIn = []
}
)
}
@ -289,11 +297,11 @@ export class OsmConnection {
public async interact(
path: string,
method: "GET" | "POST" | "PUT" | "DELETE",
header?: Record<string, string | number>,
header?: Record<string, string>,
content?: string,
allowAnonymous: boolean = false
): Promise<string> {
let connection: OSMAuthInstance = this.auth
const connection: osmAuth = this.auth
if (allowAnonymous && !this.auth.authenticated()) {
const possibleResult = await Utils.downloadAdvanced(
`${this.Backend()}/api/0.6/${path}`,
@ -310,15 +318,13 @@ export class OsmConnection {
return new Promise((ok, error) => {
connection.xhr(
<any>{
{
method,
options: {
header,
},
headers: header,
content,
path: `/api/0.6/${path}`,
path: `/api/0.6/${path}`
},
function (err, response) {
function(err, response) {
if (err !== null) {
error(err)
} else {
@ -329,32 +335,32 @@ export class OsmConnection {
})
}
public async post(
public async post<T extends string>(
path: string,
content?: string,
header?: Record<string, string | number>,
header?: Record<string, string>,
allowAnonymous: boolean = false
): Promise<any> {
return await this.interact(path, "POST", header, content, allowAnonymous)
): Promise<T> {
return <T> await this.interact(path, "POST", header, content, allowAnonymous)
}
public async put(
public async put<T extends string>(
path: string,
content?: string,
header?: Record<string, string | number>
): Promise<any> {
return await this.interact(path, "PUT", header, content)
header?: Record<string, string>
): Promise<T> {
return <T> await this.interact(path, "PUT", header, content)
}
public async get(
path: string,
header?: Record<string, string | number>,
header?: Record<string, string>,
allowAnonymous: boolean = false
): Promise<string> {
return await this.interact(path, "GET", header, undefined, allowAnonymous)
}
public closeNote(id: number | string, text?: string): Promise<void> {
public closeNote(id: number | string, text?: string): Promise<string> {
let textSuffix = ""
if ((text ?? "") !== "") {
textSuffix = "?text=" + encodeURIComponent(text)
@ -362,17 +368,17 @@ export class OsmConnection {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually closing note ", id, " with text ", text)
return new Promise((ok) => {
ok()
ok("")
})
}
return this.post(`notes/${id}/close${textSuffix}`)
}
public reopenNote(id: number | string, text?: string): Promise<void> {
public reopenNote(id: number | string, text?: string): Promise<string> {
if (this._dryRun.data) {
console.warn("Dryrun enabled - not actually reopening note ", id, " with text ", text)
return new Promise((ok) => {
ok()
return new Promise(resolve => {
resolve("")
})
}
let textSuffix = ""
@ -398,7 +404,7 @@ export class OsmConnection {
"notes.json",
content,
{
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
"Content-Type": "application/x-www-form-urlencoded; charset=UTF-8"
},
true
)
@ -439,7 +445,7 @@ export class OsmConnection {
file: gpx,
description: options.description,
tags: options.labels?.join(",") ?? "",
visibility: options.visibility,
visibility: options.visibility
}
if (!contents.description) {
@ -447,9 +453,9 @@ export class OsmConnection {
}
const extras = {
file:
'; filename="' +
"; filename=\"" +
(options.filename ?? "gpx_track_mapcomplete_" + new Date().toISOString()) +
'"\r\nContent-Type: application/gpx+xml',
"\"\r\nContent-Type: application/gpx+xml"
}
const boundary = "987654"
@ -457,7 +463,7 @@ export class OsmConnection {
let body = ""
for (const key in contents) {
body += "--" + boundary + "\r\n"
body += 'Content-Disposition: form-data; name="' + key + '"'
body += "Content-Disposition: form-data; name=\"" + key + "\""
if (extras[key] !== undefined) {
body += extras[key]
}
@ -468,7 +474,7 @@ export class OsmConnection {
const response = await this.post("gpx/create", body, {
"Content-Type": "multipart/form-data; boundary=" + boundary,
"Content-Length": body.length,
"Content-Length": ""+body.length
})
const parsed = JSON.parse(response)
console.log("Uploaded GPX track", parsed)
@ -491,9 +497,9 @@ export class OsmConnection {
{
method: "POST",
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`,
path: `/api/0.6/notes/${id}/comment?text=${encodeURIComponent(text)}`
},
function (err, _) {
function(err) {
if (err !== null) {
error(err)
} else {
@ -508,7 +514,7 @@ export class OsmConnection {
* To be called by land.html
*/
public finishLogin(callback: (previousURL: string) => void) {
this.auth.authenticate(function () {
this.auth.authenticate(function() {
// Fully authed at this point
console.log("Authentication successful!")
const previousLocation = LocalStorageSource.Get("location_before_login")
@ -517,28 +523,6 @@ export class OsmConnection {
}
private updateAuthObject() {
let pwaStandAloneMode = false
try {
if (Utils.runningFromConsole) {
pwaStandAloneMode = true
} else if (
window.matchMedia("(display-mode: standalone)").matches ||
window.matchMedia("(display-mode: fullscreen)").matches
) {
pwaStandAloneMode = true
}
} catch (e) {
console.warn(
"Detecting standalone mode failed",
e,
". Assuming in browser and not worrying furhter"
)
}
const standalone = this._iframeMode || pwaStandAloneMode || !this._singlePage
// In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
// Same for an iframe...
this.auth = new osmAuth({
client_id: this._oauth_config.oauth_client_id,
url: this._oauth_config.url,
@ -546,23 +530,22 @@ export class OsmConnection {
redirect_uri: Utils.runningFromConsole
? "https://mapcomplete.org/land.html"
: window.location.protocol + "//" + window.location.host + "/land.html",
singlepage: true,
auto: true,
singlepage: true, // We always use 'singlePage', it is the most stable - including in PWA
auto: true
})
}
private CheckForMessagesContinuously() {
const self = this
if (this.isChecking) {
return
}
Stores.Chronic(3 * 1000).addCallback((_) => {
if (!(self.apiIsOnline.data === "unreachable" || self.apiIsOnline.data === "offline")) {
Stores.Chronic(3 * 1000).addCallback(() => {
if (!(this.apiIsOnline.data === "unreachable" || this.apiIsOnline.data === "offline")) {
return
}
try {
console.log("Api is offline - trying to reconnect...")
self.AttemptLogin()
this.AttemptLogin()
} catch (e) {
console.log("Could not login due to", e)
}
@ -571,10 +554,10 @@ export class OsmConnection {
if (!this._doCheckRegularly) {
return
}
Stores.Chronic(60 * 5 * 1000).addCallback((_) => {
if (self.isLoggedIn.data) {
Stores.Chronic(60 * 5 * 1000).addCallback(() => {
if (this.isLoggedIn.data) {
try {
self.AttemptLogin()
this.AttemptLogin()
} catch (e) {
console.log("Could not login due to", e)
}
@ -592,19 +575,9 @@ export class OsmConnection {
})
}
private readonly _userInfoCache: Record<number, any> = {}
private readonly _userInfoCache: Record<number, OsmUserInfo> = {}
public async getInformationAboutUser(id: number): Promise<{
id: number
display_name: string
account_created: string
description: string
contributor_terms: { agreed: boolean }
roles: []
changesets: { count: number }
traces: { count: number }
blocks: { received: { count: number; active: number } }
}> {
public async getInformationAboutUser(id: number): Promise<OsmUserInfo> {
if (id === undefined) {
return undefined
}

View file

@ -43,6 +43,7 @@ export default class UserRelatedState {
public readonly fixateNorth: UIEventSource<undefined | "yes">
public readonly a11y: UIEventSource<undefined | "always" | "never" | "default">
public readonly homeLocation: FeatureSource
public readonly morePrivacy: UIEventSource<undefined | "yes" | "no">
/**
* The language as saved into the preferences of the user, if logged in.
* Note that this is _different_ from the languages a user can set via the osm.org interface here: https://www.openstreetmap.org/preferences
@ -106,12 +107,12 @@ export default class UserRelatedState {
})
)
this.language = this.osmConnection.GetPreference("language")
this.showTags = <UIEventSource<any>>this.osmConnection.GetPreference("show_tags")
this.showCrosshair = <UIEventSource<any>>this.osmConnection.GetPreference("show_crosshair")
this.fixateNorth = <UIEventSource<"yes">>this.osmConnection.GetPreference("fixate-north")
this.a11y = <UIEventSource<"always" | "never" | "default">>(
this.osmConnection.GetPreference("a11y")
)
this.showTags = this.osmConnection.GetPreference("show_tags")
this.showCrosshair = this.osmConnection.GetPreference("show_crosshair")
this.fixateNorth = this.osmConnection.GetPreference("fixate-north")
this.morePrivacy = this.osmConnection.GetPreference("more_privacy", "no")
this.a11y = this.osmConnection.GetPreference("a11y")
this.mangroveIdentity = new MangroveIdentity(
this.osmConnection.GetLongPreference("identity", "mangrove"),

View file

@ -564,5 +564,5 @@ export interface LayerConfigJson {
* ifunset: Write 'change_within_x_m' as usual and if GPS is enabled
* iftrue: Do not write 'change_within_x_m' and do not indicate that this was done by survey
*/
enableMorePrivacy: boolean
enableMorePrivacy?: boolean
}